Skip to content

Commit

Permalink
Implement invite user functionality in organization settings
Browse files Browse the repository at this point in the history
  • Loading branch information
hansiboy1999 committed Feb 12, 2025
1 parent dc062df commit 4f3a656
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 114 deletions.
4 changes: 3 additions & 1 deletion services/backend/src/collections/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { systemModelsApi } from "./system_models/api";
import { systemsApi } from "./systems/api";
import { usersApi } from "./users/api";
import { usersToOrganizationsApi } from "./users_to_organizations/api";
import { invitesApi } from "./invites/api";

export const api = new Elysia({ prefix: "/api" })
.use(authApi)
Expand All @@ -18,4 +19,5 @@ export const api = new Elysia({ prefix: "/api" })
.use(systemsApi)
.use(usersToOrganizationsApi)
.use(systemModelsApi)
.use(usersApi);
.use(usersApi)
.use(invitesApi);
39 changes: 39 additions & 0 deletions services/backend/src/collections/invites/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { authMiddleware } from "$auth/middleware";
import { Queries } from "$collections/queries";
import Elysia, { error, t } from "elysia";
import { insertInvitesSchema } from "./schema";
import { Schema } from "$collections/schema";

export const invitesApi = new Elysia({ prefix: "invites" })
.use(authMiddleware)
Expand All @@ -15,6 +17,43 @@ export const invitesApi = new Elysia({ prefix: "invites" })
isOrganization: true,
},
)
.post(
"/",
async ({ user, body, relation }) => {

// TODO: Send email to user with link to sign up
return await Queries.invites.create({
inviter_id: user.id,
organization_id: relation.organization_id,
...body
});
},
{
body: t.Object({
email: Schema.insert.invites.email,
role: Schema.insert.invites.role

}),
isOrganizationAdmin: true,
},
)
.patch(
"/",
async ({ user, body, relation }) => {
return await Queries.invites.update({
organization_id: relation.organization_id,
...body
});
},
{
body: t.Object({
email: Schema.insert.invites.email,
role: Schema.insert.invites.role

}),
isOrganizationAdmin: true,
},
)
.delete(
"/",
async ({ user, body, relation }) => {
Expand Down
4 changes: 2 additions & 2 deletions services/backend/src/collections/invites/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const invitesQueries = {
.where(
and(
eq(Table.invites.organization_id, values.organization_id),
eq(Table.invites.inviter_id, values.email),
eq(Table.invites.email, values.email),
),
)
.returning()
Expand All @@ -28,7 +28,7 @@ export const invitesQueries = {
.where(
and(
eq(Table.invites.organization_id, values.organization_id),
eq(Table.invites.inviter_id, values.email),
eq(Table.invites.email, values.email),
),
)
.returning()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
<h1 class="mb-6 text-3xl font-bold">Settings</h1>

<div class="grid grid-cols-1 gap-6 md:grid-cols-2">

<UserSettings />
<OrganizationSettings />

Expand Down Expand Up @@ -60,13 +59,7 @@
<Table.Row>
<Table.Cell>{system.name}</Table.Cell>
<Table.Cell class="text-right">
<Button
variant="destructive"
size="sm"
onclick={() => systemStore.delete(system.id)}
>
Remove
</Button>
<Button variant="destructive" size="sm">Remove</Button>
</Table.Cell>
</Table.Row>
{/each}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,71 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Alert from '$lib/components/ui/alert';
import * as Table from '$lib/components/ui/table';
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import * as Alert from '$lib/components/ui/alert';
import * as Table from '$lib/components/ui/table';
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import Copy from 'lucide-svelte/icons/copy';
import AlertCircle from 'lucide-svelte/icons/circle-alert';
import * as Avatar from '$lib/components/ui/avatar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Avatar from '$lib/components/ui/avatar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { api } from '@/api';
type User = NonNullable<Awaited<ReturnType<typeof api.users.onOrganization.get>>['data']>[number];
type Invite = NonNullable<
Awaited<ReturnType<typeof api.invites.onOrganization.get>>['data']
>[number];
type User = NonNullable<Awaited<ReturnType<typeof api.users.onOrganization.get>>['data']>[number];
let users = $state<User[]>([]);
let invites = $state<Invite[]>([]);
let newUserEmail = $state('');
let users = $state<User[]>([]);
async function getUsers() {
let { data, error } = await api.users.onOrganization.get();
async function getUsers() {
let {data, error} = await api.users.onOrganization.get();
if (data) {
users = data;
}
if (data) {
users = data;
};
if (error) {
console.error(error);
}
}
async function removeUser(id: string) {
let { data, error } = await api.users.index.delete({ id });
getUsers();
}
if (error) {
console.error(error);
}
}
async function getInvites() {
let { data, error } = await api.invites.onOrganization.get();
async function removeUser(id: string) {
let {data, error} = await api.users.index.delete({id});
if (data) {
invites = data;
}
getUsers()
}
if (error) {
console.error(error);
}
}
async function inviteUser() {
let { data, error } = await api.invites.index.post({ email: newUserEmail });
if (error) {
console.error(error);
return;
}
console.log('Successfully invited user');
getInvites();
}
async function updateInvite() {}
let newUserEmail = $state('');
function resendOnboardingEmail(email: string) {
console.log('Resend onboarding email to:', email);
Expand All @@ -55,84 +79,73 @@
});
}
getUsers();
getUsers();
getInvites();
</script>

<Card.Root class="col-span-1 md:col-span-2">
<Card.Header>
<Card.Title>User Management</Card.Title>
</Card.Header>
<Card.Content>
<div class="mb-6">
<Label for="new-user">Invite New User</Label>
<div class="flex gap-2">
<Input id="new-user" type="email" placeholder="Enter email" bind:value={newUserEmail} />
<Button onclick={addUser}>Invite & Onboard</Button>
</div>
</div>

<Table.Root>
<Table.Caption>Users and Onboarding Status</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[300px]">User</Table.Head>
<Table.Head>Role</Table.Head>
<Table.Head>Onboarding Status</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each users as person}
<Table.Row>
<Table.Cell>
<div class="flex items-center space-x-4">
<Avatar.Root>
<Avatar.Image src={person.image ?? ''} alt={person.name} />
<Avatar.Fallback class="font-semibold uppercase">
{person.name.slice(0, 2)}
</Avatar.Fallback>
</Avatar.Root>
<div>
<div class="font-bold">{person.name}</div>
<div class="text-sm text-muted-foreground">{person.email}</div>
</div>
</div>
</Table.Cell>
<Table.Cell>{person.role}</Table.Cell>
<Table.Cell>{true}</Table.Cell>
<Table.Cell class="text-right">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" size="icon">
<Ellipsis class="h-4 w-4" />
<span class="sr-only">Open menu</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item
onclick={() =>
updateUserRole(person.id, person.role === 'Admin' ? 'User' : 'Admin')}
>
{person.role === 'Admin' ? 'Demote to User' : 'Promote to Admin'}
</DropdownMenu.Item>
{#if person.onboardingStatus === 'pending'}
<DropdownMenu.Item onclick={() => resendOnboardingEmail(person.email)}>
Resend Onboarding Email
</DropdownMenu.Item>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={() => removeUser(person.id)} class="text-red-600">
Remove User
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
<Card.Header>
<Card.Title>User Management</Card.Title>
</Card.Header>
<Card.Content>
<div class="mb-6">
<Label for="new-user">Invite New User</Label>
<div class="flex gap-2">
<Input id="new-user" type="email" placeholder="Enter email" bind:value={newUserEmail} />
<Button onclick={inviteUser}>Invite & Onboard</Button>
</div>
</div>

<Table.Root>
<Table.Caption>Users and Onboarding Status</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[300px]">User</Table.Head>
<Table.Head>Role</Table.Head>
<Table.Head>Onboarding Status</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each users as person}
<Table.Row>
<Table.Cell>
<div class="flex items-center space-x-4">
<Avatar.Root>
<Avatar.Image src={person.image ?? ''} alt={person.name} />
<Avatar.Fallback class="font-semibold uppercase">
{person.name.slice(0, 2)}
</Avatar.Fallback>
</Avatar.Root>
<div>
<div class="font-bold">{person.name}</div>
<div class="text-sm text-muted-foreground">{person.email}</div>
</div>
</div>
</Table.Cell>
<Table.Cell>{person.role}</Table.Cell>
<Table.Cell>{true}</Table.Cell>
<Table.Cell class="text-right">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" size="icon">
<Ellipsis class="h-4 w-4" />
<span class="sr-only">Open menu</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={() => removeUser(person.id)} class="text-red-600">
Remove User
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>

0 comments on commit 4f3a656

Please sign in to comment.