team join with link and member admin
This commit is contained in:
@@ -38,8 +38,9 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op
|
|||||||
- [x] Allow users to upgrade their accounts fron individual accounts to multi-user accounts (Teams).
|
- [x] Allow users to upgrade their accounts fron individual accounts to multi-user accounts (Teams).
|
||||||
- [x] Allow users to switch between Teams and view/edit data from the selected Team.
|
- [x] Allow users to switch between Teams and view/edit data from the selected Team.
|
||||||
- [x] All features, billing and limits is controlled at the Account (Team) level (not the user level)
|
- [x] All features, billing and limits is controlled at the Account (Team) level (not the user level)
|
||||||
- [ ] Team administrators and owners can administer the permissions (roles) of other team members on the Accounts page
|
- [x] Gen/Regen an invite link to allow users to join a team
|
||||||
- [ ] Gen/Regen an invite link to allow users to join a team
|
- [x] Team administrators and owners can accept pending invites
|
||||||
|
- [x] Team administrators and owners can administer the permissions (roles) of other team members on the Accounts page
|
||||||
|
|
||||||
### Plans and Pricing
|
### Plans and Pricing
|
||||||
- [x] Manage multiple Plans each with specific Feature flags and Plan limits
|
- [x] Manage multiple Plans each with specific Feature flags and Plan limits
|
||||||
|
|||||||
@@ -37,8 +37,9 @@
|
|||||||
<!-- Account Switching -->
|
<!-- Account Switching -->
|
||||||
<p v-if="(dbUser?.dbUser?.memberships) && (dbUser.dbUser.memberships.length > 1)">
|
<p v-if="(dbUser?.dbUser?.memberships) && (dbUser.dbUser.memberships.length > 1)">
|
||||||
<span>Switch Account.. </span>
|
<span>Switch Account.. </span>
|
||||||
<button v-for="membership in dbUser?.dbUser.memberships" @click="authStore.changeActiveMembership(((membership as unknown) as MembershipWithAccount))"> <!-- This cast is infuriating -->
|
<button :disabled="membership.pending" v-for="membership in dbUser?.dbUser.memberships" @click="authStore.changeActiveMembership(((membership as unknown) as MembershipWithAccount))"> <!-- This cast is infuriating -->
|
||||||
{{ membership.account.name }}
|
{{ membership.account.name }}
|
||||||
|
<span v-if="membership.pending">(pending)</span>
|
||||||
<span v-if="membership.account_id === activeMembership?.account_id">*</span>
|
<span v-if="membership.account_id === activeMembership?.account_id">*</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ACCOUNT_ACCESS } from '@prisma/client';
|
import { ACCOUNT_ACCESS } from '@prisma/client';
|
||||||
import prisma_client from '~~/prisma/prisma.client';
|
import prisma_client from '~~/prisma/prisma.client';
|
||||||
import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types';
|
import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types';
|
||||||
|
import generator from 'generate-password-ts';
|
||||||
|
|
||||||
export default class AccountService {
|
export default class AccountService {
|
||||||
async getAccountById(account_id: number): Promise<AccountWithMembers> {
|
async getAccountById(account_id: number): Promise<AccountWithMembers> {
|
||||||
@@ -10,6 +11,13 @@ export default class AccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccountByJoinPassword(join_password: string): Promise<AccountWithMembers> {
|
||||||
|
return prisma_client.account.findFirstOrThrow({
|
||||||
|
where: { join_password },
|
||||||
|
...accountWithMembers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getAccountMembers(account_id: number): Promise<MembershipWithUser[]> {
|
async getAccountMembers(account_id: number): Promise<MembershipWithUser[]> {
|
||||||
return prisma_client.membership.findMany({
|
return prisma_client.membership.findMany({
|
||||||
where: { account_id },
|
where: { account_id },
|
||||||
@@ -62,7 +70,29 @@ export default class AccountService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinUserToAccount(user_id: number, account_id: number): Promise<MembershipWithAccount> {
|
async acceptPendingMembership(account_id: number, membership_id: number): Promise<MembershipWithAccount> {
|
||||||
|
const membership = prisma_client.membership.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: membership_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if((await membership).account_id != account_id){
|
||||||
|
throw new Error(`Membership does not belong to current account`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma_client.membership.update({
|
||||||
|
where: {
|
||||||
|
id: membership_id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
pending: false
|
||||||
|
},
|
||||||
|
...membershipWithAccount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinUserToAccount(user_id: number, account_id: number, pending: boolean ): Promise<MembershipWithAccount> {
|
||||||
const account = await prisma_client.account.findUnique({
|
const account = await prisma_client.account.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: account_id,
|
id: account_id,
|
||||||
@@ -77,11 +107,20 @@ export default class AccountService {
|
|||||||
throw new Error(`Too Many Members, Account only permits ${account?.max_members} members.`);
|
throw new Error(`Too Many Members, Account only permits ${account?.max_members} members.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(account?.members){
|
||||||
|
for(const member of account.members){
|
||||||
|
if(member.user_id === user_id){
|
||||||
|
throw new Error(`User is already a member`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return prisma_client.membership.create({
|
return prisma_client.membership.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
account_id,
|
account_id,
|
||||||
access: ACCOUNT_ACCESS.READ_ONLY
|
access: ACCOUNT_ACCESS.READ_ONLY,
|
||||||
|
pending
|
||||||
},
|
},
|
||||||
...membershipWithAccount
|
...membershipWithAccount
|
||||||
});
|
});
|
||||||
@@ -108,6 +147,16 @@ export default class AccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rotateJoinPassword(account_id: number) {
|
||||||
|
const join_password: string = generator.generate({
|
||||||
|
length: 10,
|
||||||
|
numbers: true
|
||||||
|
});
|
||||||
|
return prisma_client.account.update({
|
||||||
|
where: { id: account_id},
|
||||||
|
data: { join_password }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Claim ownership of an account.
|
// Claim ownership of an account.
|
||||||
// User must already be an ADMIN for the Account
|
// User must already be an ADMIN for the Account
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ACCOUNT_ACCESS } from '@prisma/client';
|
|||||||
import prisma_client from '~~/prisma/prisma.client';
|
import prisma_client from '~~/prisma/prisma.client';
|
||||||
import { fullDBUser, FullDBUser } from './service.types';
|
import { fullDBUser, FullDBUser } from './service.types';
|
||||||
import { UtilService } from './util.service';
|
import { UtilService } from './util.service';
|
||||||
|
import generator from 'generate-password-ts';
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ export default class AuthService {
|
|||||||
|
|
||||||
async createUser( supabase_uid: string, display_name: string, email: string ): Promise<FullDBUser | null> {
|
async createUser( supabase_uid: string, display_name: string, email: string ): Promise<FullDBUser | null> {
|
||||||
const trialPlan = await prisma_client.plan.findFirstOrThrow({ where: { name: config.initialPlanName}});
|
const trialPlan = await prisma_client.plan.findFirstOrThrow({ where: { name: config.initialPlanName}});
|
||||||
|
const join_password: string = generator.generate({
|
||||||
|
length: 10,
|
||||||
|
numbers: true
|
||||||
|
});
|
||||||
return prisma_client.user.create({
|
return prisma_client.user.create({
|
||||||
data:{
|
data:{
|
||||||
supabase_uid: supabase_uid,
|
supabase_uid: supabase_uid,
|
||||||
@@ -38,6 +43,7 @@ export default class AuthService {
|
|||||||
max_notes: trialPlan.max_notes,
|
max_notes: trialPlan.max_notes,
|
||||||
max_members: trialPlan.max_members,
|
max_members: trialPlan.max_members,
|
||||||
plan_name: trialPlan.name,
|
plan_name: trialPlan.name,
|
||||||
|
join_password: join_password,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
access: ACCOUNT_ACCESS.OWNER
|
access: ACCOUNT_ACCESS.OWNER
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default defineNuxtConfig({
|
|||||||
initialPlanActiveMonths: 1,
|
initialPlanActiveMonths: 1,
|
||||||
public: {
|
public: {
|
||||||
debugMode: true,
|
debugMode: true,
|
||||||
|
siteRootUrl: 'http://localhost:3000',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
43
package-lock.json
generated
43
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"@pinia/nuxt": "^0.4.6",
|
"@pinia/nuxt": "^0.4.6",
|
||||||
"@trpc/client": "^10.9.0",
|
"@trpc/client": "^10.9.0",
|
||||||
"@trpc/server": "^10.9.0",
|
"@trpc/server": "^10.9.0",
|
||||||
|
"generate-password-ts": "^1.6.3",
|
||||||
"pinia": "^2.0.30",
|
"pinia": "^2.0.30",
|
||||||
"stripe": "^11.12.0",
|
"stripe": "^11.12.0",
|
||||||
"superjson": "^1.12.2",
|
"superjson": "^1.12.2",
|
||||||
@@ -3971,6 +3972,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/generate-password-ts": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/generate-password-ts/-/generate-password-ts-1.6.3.tgz",
|
||||||
|
"integrity": "sha512-6piPG6qvW3B40NLKz6VfK9IgVl0MXRZgGt1SW3zUTZXln2DVno84cAwx0Z6OvA9uJKXRjRRvPz1CUDDsWXJZwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"js-crypto-random": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -4631,6 +4640,19 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-crypto-env": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-crypto-env/-/js-crypto-env-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-b7WdjaX4csatMPfZ/mQ94yb/XTKe3o6qt0jPBVbKmaiOH97e+FlmIANoFEMrhxQM1xxKfA2QYLjgqL/YtdMm9g=="
|
||||||
|
},
|
||||||
|
"node_modules/js-crypto-random": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-crypto-random/-/js-crypto-random-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-bJ31lo2YrxOJIRnT/XPzUu2O43/7uGWAzAtXmAi09Oq6Jmrd2uVy8wII/WH3thezhZvjdU85fVtySSeD1Y2/dQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"js-crypto-env": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -11535,6 +11557,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"generate-password-ts": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/generate-password-ts/-/generate-password-ts-1.6.3.tgz",
|
||||||
|
"integrity": "sha512-6piPG6qvW3B40NLKz6VfK9IgVl0MXRZgGt1SW3zUTZXln2DVno84cAwx0Z6OvA9uJKXRjRRvPz1CUDDsWXJZwA==",
|
||||||
|
"requires": {
|
||||||
|
"js-crypto-random": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gensync": {
|
"gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -12004,6 +12034,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
|
||||||
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
|
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg=="
|
||||||
},
|
},
|
||||||
|
"js-crypto-env": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-crypto-env/-/js-crypto-env-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-b7WdjaX4csatMPfZ/mQ94yb/XTKe3o6qt0jPBVbKmaiOH97e+FlmIANoFEMrhxQM1xxKfA2QYLjgqL/YtdMm9g=="
|
||||||
|
},
|
||||||
|
"js-crypto-random": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-crypto-random/-/js-crypto-random-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-bJ31lo2YrxOJIRnT/XPzUu2O43/7uGWAzAtXmAi09Oq6Jmrd2uVy8wII/WH3thezhZvjdU85fVtySSeD1Y2/dQ==",
|
||||||
|
"requires": {
|
||||||
|
"js-crypto-env": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@pinia/nuxt": "^0.4.6",
|
"@pinia/nuxt": "^0.4.6",
|
||||||
"@trpc/client": "^10.9.0",
|
"@trpc/client": "^10.9.0",
|
||||||
"@trpc/server": "^10.9.0",
|
"@trpc/server": "^10.9.0",
|
||||||
|
"generate-password-ts": "^1.6.3",
|
||||||
"pinia": "^2.0.30",
|
"pinia": "^2.0.30",
|
||||||
"stripe": "^11.12.0",
|
"stripe": "^11.12.0",
|
||||||
"superjson": "^1.12.2",
|
"superjson": "^1.12.2",
|
||||||
|
|||||||
@@ -24,21 +24,35 @@
|
|||||||
if(!date){ return ""; }
|
if(!date){ return ""; }
|
||||||
return new Intl.DateTimeFormat('default', {dateStyle: 'long'}).format(date);
|
return new Intl.DateTimeFormat('default', {dateStyle: 'long'}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function joinURL(){
|
||||||
|
return `${config.public.siteRootUrl}/join/${activeMembership.value?.account.join_password}`;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h3>Account</h3>
|
<h3>Account</h3>
|
||||||
<p>Name: {{ activeMembership?.account.name }} <span v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access !== ACCOUNT_ACCESS.ADMIN)"><input v-model="newAccountName" placeholder="Enter New Name"/><button @click.prevent="accountStore.changeAccountName(newAccountName)">Change Name</button></span></p>
|
<p>Name: {{ activeMembership?.account.name }} <span v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"><input v-model="newAccountName" placeholder="Enter New Name"/><button @click.prevent="accountStore.changeAccountName(newAccountName)">Change Name</button></span></p>
|
||||||
<p>Current Period Ends: {{ formatDate(activeMembership?.account.current_period_ends) }}</p>
|
<p>Current Period Ends: {{ formatDate(activeMembership?.account.current_period_ends) }}</p>
|
||||||
<p>Permitted Features: {{ activeMembership?.account.features }}</p>
|
<p>Permitted Features: {{ activeMembership?.account.features }}</p>
|
||||||
<p>Maximum Notes: {{ activeMembership?.account.max_notes }}</p>
|
<p>Maximum Notes: {{ activeMembership?.account.max_notes }}</p>
|
||||||
<p>Maximum Members: {{ activeMembership?.account.max_members }}</p>
|
<p>Maximum Members: {{ activeMembership?.account.max_members }}</p>
|
||||||
<p>Access Level: {{ activeMembership?.access }}</p>
|
<p>Access Level: {{ activeMembership?.access }} <span v-if="activeMembership && activeMembership.access === ACCOUNT_ACCESS.ADMIN"><button @click.prevent="accountStore.claimOwnershipOfAccount()">Claim Ownership of Account</button></span></p>
|
||||||
<p>Plan: {{ activeMembership?.account.plan_name }}</p>
|
<p>Plan: {{ activeMembership?.account.plan_name }}</p>
|
||||||
|
<p>Join Link: <pre>{{ joinURL() }}</pre> <span v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"><button @click.prevent="accountStore.rotateJoinPassword()">Generate New Join Link</button></span></p>
|
||||||
|
|
||||||
<h4>Members</h4>
|
<h4>Members</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="accountMember in activeAccountMembers">{{ accountMember.user.display_name }}</li>
|
<li v-for="accountMember in activeAccountMembers">
|
||||||
|
{{ accountMember.user.display_name }}
|
||||||
|
({{ accountMember.user.email }})
|
||||||
|
[{{ accountMember.access }}]
|
||||||
|
<span v-if="accountMember.pending">(pending)</span>
|
||||||
|
<span v-if="accountMember.pending && activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN)"><button @click.prevent="accountStore.acceptPendingMembership(accountMember.id)">Accept Pending Membership</button></span>
|
||||||
|
<span v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN) && accountMember.access === ACCOUNT_ACCESS.READ_ONLY"><button @click.prevent="accountStore.changeUserAccessWithinAccount(accountMember.user.id, ACCOUNT_ACCESS.READ_WRITE)">Promote to Read/Write</button></span>
|
||||||
|
<span v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.OWNER || activeMembership.access === ACCOUNT_ACCESS.ADMIN) && accountMember.access === ACCOUNT_ACCESS.READ_WRITE"><button @click.prevent="accountStore.changeUserAccessWithinAccount(accountMember.user.id, ACCOUNT_ACCESS.ADMIN)">Promote to Admin</button></span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<template v-if="config.public.debugMode">
|
<template v-if="config.public.debugMode">
|
||||||
@@ -51,10 +65,7 @@
|
|||||||
<p>User Id: {{ activeMembership?.user_id }}</p>
|
<p>User Id: {{ activeMembership?.user_id }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button @click.prevent="accountStore.changeAccountPlan(2)">Change active Account Plan to 2</button>
|
|
||||||
<button @click.prevent="accountStore.joinUserToAccount(4)">Join user 4 to active account</button>
|
|
||||||
<button @click.prevent="accountStore.changeUserAccessWithinAccount(4, 'OWNER')">Change user 4 access within account 5 to OWNER (SHOULD FAIL)</button>
|
|
||||||
<button @click.prevent="accountStore.changeUserAccessWithinAccount(4, 'ADMIN')">Change user 4 access within account 5 to ADMIN</button>
|
|
||||||
<button @click.prevent="accountStore.claimOwnershipOfAccount()">Claim Ownership of current account for current user</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
44
pages/join/[join_password].vue
Normal file
44
pages/join/[join_password].vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AccountWithMembers } from '~~/lib/services/service.types';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const {join_password} : {join_password?: string} = route.params;
|
||||||
|
|
||||||
|
const { $client } = useNuxtApp();
|
||||||
|
|
||||||
|
// this could probably be an elegant destructure here but I lost patience
|
||||||
|
let account: AccountWithMembers | undefined;
|
||||||
|
if(join_password){
|
||||||
|
const result = await $client.account.getAccountByJoinPassword.useQuery({join_password});
|
||||||
|
account = result.data.value?.account;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: dbUser } = await $client.auth.getDBUser.useQuery();
|
||||||
|
|
||||||
|
async function doJoin(){
|
||||||
|
if(dbUser.value?.dbUser && account){
|
||||||
|
await $client.account.joinUserToAccountPending.useQuery({account_id: account.id, user_id: dbUser.value.dbUser.id});
|
||||||
|
} else {
|
||||||
|
console.log(`Unable to Join`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="account">
|
||||||
|
<h3>Join {{ account?.name }}</h3>
|
||||||
|
|
||||||
|
<div v-if="dbUser?.dbUser">
|
||||||
|
<button @click.prevent="doJoin()">Join</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<NuxtLink to="/signup">Sign up to Join team</NuxtLink>
|
||||||
|
<p>or</p>
|
||||||
|
<NuxtLink to="/signin">Sign in to Join team</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h3>This does not appear to be a valid Join Link. Please ask a Team administrator to re-generate and resend the Join link.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -35,6 +35,7 @@ model Membership {
|
|||||||
account Account @relation(fields: [account_id], references: [id])
|
account Account @relation(fields: [account_id], references: [id])
|
||||||
user User @relation(fields: [user_id], references: [id])
|
user User @relation(fields: [user_id], references: [id])
|
||||||
access ACCOUNT_ACCESS @default(READ_ONLY)
|
access ACCOUNT_ACCESS @default(READ_ONLY)
|
||||||
|
pending Boolean @default(false)
|
||||||
|
|
||||||
@@map("membership")
|
@@map("membership")
|
||||||
@@unique([user_id, account_id])
|
@@unique([user_id, account_id])
|
||||||
@@ -54,6 +55,7 @@ model Account {
|
|||||||
stripe_subscription_id String?
|
stripe_subscription_id String?
|
||||||
stripe_customer_id String?
|
stripe_customer_id String?
|
||||||
max_members Int @default(1)
|
max_members Int @default(1)
|
||||||
|
join_password String @unique
|
||||||
|
|
||||||
@@map("account")
|
@@map("account")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ export async function createContext(event: H3Event){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!user || !dbUser) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: `Unable to fetch user data at this time. Missing ->[user:${(!user)},dbUser:${(!dbUser)}]`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
dbUser,
|
dbUser,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { router, adminProcedure } from '../trpc'
|
import { router, adminProcedure, publicProcedure } from '../trpc'
|
||||||
import { ACCOUNT_ACCESS } from '@prisma/client';
|
import { ACCOUNT_ACCESS } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import AccountService from '~~/lib/services/account.service';
|
import AccountService from '~~/lib/services/account.service';
|
||||||
@@ -15,11 +15,21 @@ export const accountRouter = router({
|
|||||||
account,
|
account,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
changeAccountPlan: adminProcedure
|
rotateJoinPassword: adminProcedure
|
||||||
.input(z.object({ account_id: z.number(), plan_id: z.number() }))
|
.input(z.object({ account_id: z.number() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const accountService = new AccountService();
|
const accountService = new AccountService();
|
||||||
const account = await accountService.changeAccountPlan(input.account_id, input.plan_id);
|
const account = await accountService.rotateJoinPassword(input.account_id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getAccountByJoinPassword: publicProcedure
|
||||||
|
.input(z.object({ join_password: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const accountService = new AccountService();
|
||||||
|
const account = await accountService.getAccountByJoinPassword(input.join_password);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
account,
|
||||||
@@ -29,7 +39,25 @@ export const accountRouter = router({
|
|||||||
.input(z.object({ account_id: z.number(), user_id: z.number() }))
|
.input(z.object({ account_id: z.number(), user_id: z.number() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const accountService = new AccountService();
|
const accountService = new AccountService();
|
||||||
const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.joinUserToAccount(input.user_id, input.account_id):null;
|
const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.joinUserToAccount(input.user_id, input.account_id, false):null;
|
||||||
|
return {
|
||||||
|
membership,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
joinUserToAccountPending: publicProcedure
|
||||||
|
.input(z.object({ account_id: z.number(), user_id: z.number() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const accountService = new AccountService();
|
||||||
|
const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.joinUserToAccount(input.user_id, input.account_id, true):null;
|
||||||
|
return {
|
||||||
|
membership,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
acceptPendingMembership: adminProcedure
|
||||||
|
.input(z.object({ account_id: z.number(), membership_id: z.number() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const accountService = new AccountService();
|
||||||
|
const membership: MembershipWithAccount| null = (ctx.dbUser?.id)?await accountService.acceptPendingMembership(input.account_id, input.membership_id):null;
|
||||||
return {
|
return {
|
||||||
membership,
|
membership,
|
||||||
}
|
}
|
||||||
@@ -48,7 +76,7 @@ export const accountRouter = router({
|
|||||||
.input(z.object({ account_id: z.number() }))
|
.input(z.object({ account_id: z.number() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const accountService = new AccountService();
|
const accountService = new AccountService();
|
||||||
const membership = await accountService.claimOwnershipOfAccount(ctx.dbUser.id, input.account_id);
|
const membership = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, input.account_id); // adminProcedure errors if ctx.dbUser is null so bang is ok here
|
||||||
|
|
||||||
return {
|
return {
|
||||||
membership,
|
membership,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { protectedProcedure, router } from '../trpc'
|
import { publicProcedure, router } from '../trpc'
|
||||||
|
|
||||||
export const authRouter = router({
|
export const authRouter = router({
|
||||||
getDBUser: protectedProcedure
|
getDBUser: publicProcedure
|
||||||
.query(({ ctx }) => {
|
.query(({ ctx }) => {
|
||||||
return {
|
return {
|
||||||
dbUser: ctx.dbUser,
|
dbUser: ctx.dbUser,
|
||||||
|
|||||||
@@ -33,11 +33,25 @@ export const useAccountStore = defineStore('account', {
|
|||||||
authStore.activeMembership.account = account.value.account;
|
authStore.activeMembership.account = account.value.account;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async changeAccountPlan(plan_id: number){
|
async acceptPendingMembership(membership_id: number){
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
if(!authStore.activeMembership) { return; }
|
if(!authStore.activeMembership) { return; }
|
||||||
const { $client } = useNuxtApp();
|
const { $client } = useNuxtApp();
|
||||||
const { data: account } = await $client.account.changeAccountPlan.useQuery({account_id: authStore.activeMembership.account_id, plan_id});
|
const { data: membership } = await $client.account.acceptPendingMembership.useQuery({account_id: authStore.activeMembership.account_id, membership_id});
|
||||||
|
|
||||||
|
if(membership.value && membership.value.membership?.pending === false){
|
||||||
|
for(const m of this.activeAccountMembers){
|
||||||
|
if(m.id === membership_id){
|
||||||
|
m.pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async rotateJoinPassword(){
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if(!authStore.activeMembership) { return; }
|
||||||
|
const { $client } = useNuxtApp();
|
||||||
|
const { data: account } = await $client.account.rotateJoinPassword.useQuery({account_id: authStore.activeMembership.account_id});
|
||||||
if(account.value?.account){
|
if(account.value?.account){
|
||||||
authStore.activeMembership.account = account.value.account;
|
authStore.activeMembership.account = account.value.account;
|
||||||
}
|
}
|
||||||
@@ -57,7 +71,11 @@ export const useAccountStore = defineStore('account', {
|
|||||||
const { $client } = useNuxtApp();
|
const { $client } = useNuxtApp();
|
||||||
const { data: membership } = await $client.account.changeUserAccessWithinAccount.useQuery({account_id: authStore.activeMembership.account_id, user_id, access});
|
const { data: membership } = await $client.account.changeUserAccessWithinAccount.useQuery({account_id: authStore.activeMembership.account_id, user_id, access});
|
||||||
if(membership.value?.membership){
|
if(membership.value?.membership){
|
||||||
authStore.activeMembership = membership.value.membership;
|
for(const m of this.activeAccountMembers){
|
||||||
|
if(m.id === membership.value?.membership.id){
|
||||||
|
m.access = membership.value?.membership.access;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async claimOwnershipOfAccount(){
|
async claimOwnershipOfAccount(){
|
||||||
@@ -66,7 +84,12 @@ export const useAccountStore = defineStore('account', {
|
|||||||
const { $client } = useNuxtApp();
|
const { $client } = useNuxtApp();
|
||||||
const { data: membership } = await $client.account.claimOwnershipOfAccount.useQuery({account_id: authStore.activeMembership.account_id});
|
const { data: membership } = await $client.account.claimOwnershipOfAccount.useQuery({account_id: authStore.activeMembership.account_id});
|
||||||
if(membership.value?.membership){
|
if(membership.value?.membership){
|
||||||
authStore.activeMembership = membership.value.membership;
|
authStore.activeMembership.access = membership.value.membership.access;
|
||||||
|
for(const m of this.activeAccountMembers){
|
||||||
|
if(m.id === membership.value?.membership.id){
|
||||||
|
m.access = membership.value?.membership.access;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
|
|
||||||
if(dbUser){
|
if(dbUser){
|
||||||
this.dbUser = dbUser;
|
this.dbUser = dbUser;
|
||||||
if(dbUser.memberships.length > 0){
|
const activeMemberships = dbUser.memberships.filter(m => !m.pending)
|
||||||
this.activeMembership = dbUser.memberships[0];
|
if(activeMemberships.length > 0){
|
||||||
|
this.activeMembership = activeMemberships[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user