From 2ae59c063ccbbec951fb75d0f7b3b98422ec4d25 Mon Sep 17 00:00:00 2001 From: ashwin Date: Sat, 24 May 2025 12:56:05 +0530 Subject: [PATCH] Refactor user management pages: consolidate user addition and organization details, remove redundant files, and enhance user role editing functionality. --- src/app.d.ts | 6 +- src/hooks.server.js | 9 +- src/routes/(app)/app/users/+page.server.js | 224 +++++++++++++- src/routes/(app)/app/users/+page.svelte | 275 +++++++++++++++--- .../(app)/app/users/new/+page.server.js | 91 ------ src/routes/(app)/app/users/new/+page.svelte | 96 ------ .../(no-layout)/org/[org_id]/+page.server.js | 215 -------------- .../(no-layout)/org/[org_id]/+page.svelte | 247 ---------------- 8 files changed, 472 insertions(+), 691 deletions(-) delete mode 100644 src/routes/(app)/app/users/new/+page.server.js delete mode 100644 src/routes/(app)/app/users/new/+page.svelte delete mode 100644 src/routes/(no-layout)/org/[org_id]/+page.server.js delete mode 100644 src/routes/(no-layout)/org/[org_id]/+page.svelte diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..e4ad273 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -3,7 +3,11 @@ declare global { namespace App { // interface Error {} - // interface Locals {} + interface Locals { + user?: any; // You might want to replace 'any' with a more specific type for user + org?: any; // You might want to replace 'any' with a more specific type for org + org_name?: string; + } // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/src/hooks.server.js b/src/hooks.server.js index 1a623db..ddbefa1 100644 --- a/src/hooks.server.js +++ b/src/hooks.server.js @@ -46,8 +46,13 @@ export async function handle({ event, resolve }) { event.locals.org_name = userOrg.organization.name; } } else { - // User doesn't have access to this organization, redirect to logout - throw redirect(307, '/logout'); + // User doesn't have access to this organization or orgId is stale. + // Clear the invalid org cookies. + event.cookies.delete('org', { path: '/' }); + event.cookies.delete('org_name', { path: '/' }); + // Redirect to the organization selection page. + // The user is still authenticated. + throw redirect(307, '/org'); } } } diff --git a/src/routes/(app)/app/users/+page.server.js b/src/routes/(app)/app/users/+page.server.js index c797d09..8c5e812 100644 --- a/src/routes/(app)/app/users/+page.server.js +++ b/src/routes/(app)/app/users/+page.server.js @@ -1,4 +1,222 @@ +import prisma from '$lib/prisma' +import { fail, redirect } from '@sveltejs/kit'; + /** @type {import('./$types').PageServerLoad} */ -export async function load() { - return {}; -}; \ No newline at end of file +export async function load({ params, locals }) { + const org_id = locals.org.id; // Changed from params.org_id + const user = locals.user; + + // Check if user is admin of the organization + const userOrg = await prisma.userOrganization.findFirst({ + where: { + userId: user.id, + organizationId: org_id, + role: 'ADMIN' + } + }); + if (!userOrg) { + return { + error: { + name: 'You do not have permission to access this organization' + } + }; + } + // Fetch organization details + const organization = await prisma.organization.findUnique({ + where: { + id: org_id // Changed from params.org_id + } + }); + + // fetch all users in the organization + const users = await prisma.userOrganization.findMany({ + where: { + organizationId: org_id + }, + include: { + user: true + } + }); + // Pass logged-in user id to page for UI logic + return { organization, users, user: { id: user.id } }; +}; + +/** @type {import('./$types').Actions} */ +export const actions = { + update: async ({ request, params, locals }) => { + const org_id = locals.org.id; // Changed from params.org_id + const user = locals.user; + if (!user) return fail(401, { error: 'Unauthorized' }); + + // Only ADMIN can update + const userOrg = await prisma.userOrganization.findFirst({ + where: { + userId: user.id, + organizationId: org_id, + role: 'ADMIN' + } + }); + if (!userOrg) return fail(403, { error: 'Forbidden' }); + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const domain = formData.get('domain')?.toString().trim(); + const description = formData.get('description')?.toString().trim(); + + if (!name) return fail(400, { error: 'Name is required' }); + + try { + await prisma.organization.update({ + where: { id: org_id }, + data: { + name, + domain, + description + } + }); + // Update locals for the current request so layout reloads with new name + if (name) { + if (locals.org) { + locals.org.name = name; + } + locals.org_name = name; + } + return { success: true }; + } catch (err) { + return fail(500, { error: 'Failed to update organization' }); + } + }, + + add_user: async ({ request, params, locals }) => { + const org_id = locals.org.id; // Changed from params.org_id + const user = locals.user; + if (!user) return fail(401, { error: 'Unauthorized' }); + + // Only ADMIN can add + const userOrg = await prisma.userOrganization.findFirst({ + where: { + userId: user.id, + organizationId: org_id, + role: 'ADMIN' + } + }); + if (!userOrg) return fail(403, { error: 'Forbidden' }); + + const formData = await request.formData(); + const email = formData.get('email')?.toString().trim().toLowerCase(); + const role = formData.get('role')?.toString(); + if (!email || !role) return fail(400, { error: 'Email and role are required' }); + + // Find user by email + const foundUser = await prisma.user.findUnique({ where: { email } }); + if (!foundUser) return fail(404, { error: 'No user found with that email' }); + + // Check if already in org + const already = await prisma.userOrganization.findFirst({ + where: { userId: foundUser.id, organizationId: org_id } + }); + if (already) return fail(400, { error: 'User already in organization' }); + + // Add user to org + await prisma.userOrganization.create({ + data: { + userId: foundUser.id, + organizationId: org_id, + role + } + }); + return { success: true }; + }, + + edit_role: async ({ request, params, locals }) => { + const org_id = locals.org.id; // Changed from params.org_id + const user = locals.user; + if (!user) return fail(401, { error: 'Unauthorized' }); + + // Only ADMIN can edit + const userOrg = await prisma.userOrganization.findFirst({ + where: { + userId: user.id, + organizationId: org_id, + role: 'ADMIN' + } + }); + if (!userOrg) return fail(403, { error: 'Forbidden' }); + + const formData = await request.formData(); + const user_id = formData.get('user_id')?.toString(); + const role = formData.get('role')?.toString(); + if (!user_id || !role) return fail(400, { error: 'User and role are required' }); + + // Don't allow editing own role (prevent lockout) + if (user_id === user.id) return fail(400, { error: 'You cannot change your own role' }); + + // Don't allow editing role of the only remaining admin + if (role !== 'ADMIN') { + // Count number of admins in org + const adminCount = await prisma.userOrganization.count({ + where: { + organizationId: org_id, + role: 'ADMIN' + } + }); + // If target user is admin and only one admin left, prevent demotion + const target = await prisma.userOrganization.findUnique({ + where: { userId_organizationId: { userId: user_id, organizationId: org_id } } + }); + if (target && target.role === 'ADMIN' && adminCount === 1) { + return fail(400, { error: 'Organization must have at least one admin' }); + } + } + + await prisma.userOrganization.update({ + where: { userId_organizationId: { userId: user_id, organizationId: org_id } }, + data: { role } + }); + return { success: true }; + }, + + remove_user: async ({ request, params, locals }) => { + const org_id = locals.org.id; // Changed from params.org_id + const user = locals.user; + if (!user) return fail(401, { error: 'Unauthorized' }); + + // Only ADMIN can remove + const userOrg = await prisma.userOrganization.findFirst({ + where: { + userId: user.id, + organizationId: org_id, + role: 'ADMIN' + } + }); + if (!userOrg) return fail(403, { error: 'Forbidden' }); + + const formData = await request.formData(); + const user_id = formData.get('user_id')?.toString(); + if (!user_id) return fail(400, { error: 'User is required' }); + + // Don't allow removing self (prevent lockout) + if (user_id === user.id) return fail(400, { error: 'You cannot remove yourself' }); + + // Don't allow removing the only remaining admin + const target = await prisma.userOrganization.findUnique({ + where: { userId_organizationId: { userId: user_id, organizationId: org_id } } + }); + if (target && target.role === 'ADMIN') { + const adminCount = await prisma.userOrganization.count({ + where: { + organizationId: org_id, + role: 'ADMIN' + } + }); + if (adminCount === 1) { + return fail(400, { error: 'Organization must have at least one admin' }); + } + } + + await prisma.userOrganization.delete({ + where: { userId_organizationId: { userId: user_id, organizationId: org_id } } + }); + return { success: true }; + } +}; diff --git a/src/routes/(app)/app/users/+page.svelte b/src/routes/(app)/app/users/+page.svelte index c87ef85..f289553 100644 --- a/src/routes/(app)/app/users/+page.svelte +++ b/src/routes/(app)/app/users/+page.svelte @@ -1,52 +1,255 @@ -
-

Users

-
- - Add User +
+ + -
+ +
+ + {#if editing} +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + + +
+ + +
+ +
+ + +
+
+ {:else} +
+

+ + {org.name} +

+
+ + + {org.domain || '—'} + + + + {org.industry} + +
+ {#if org.description} +

{org.description}

+ {/if} +
+ {/if} +
+ + +
+

+ Users +

+ +
+
+ + +
+
+ + +
+ +
+
- + - - - - + + + + + - - {#each users as user} - - - - - + {#each users as user, i} + + + + + + {/each}
NameEmailRoleActionsNameEmailRoleJoined
{user.name}{user.email}{user.role} - - +
+
+ {#if user.avatar} + {user.name} + {:else} +
+ +
+ {/if} + {user.name} +
+
{user.email} + {#if user.isSelf} + + + {user.role} + + {:else} + {#if user.editingRole} +
+ + + + +
+ {:else} + + + {user.role} + + + {/if} + {/if} +
{user.joined} + {#if user.isSelf} + Remove + {:else} +
confirm('Remove this user from organization?') || event.preventDefault()}> + + +
+ {/if}
-
\ No newline at end of file +
+
diff --git a/src/routes/(app)/app/users/new/+page.server.js b/src/routes/(app)/app/users/new/+page.server.js deleted file mode 100644 index 738dc65..0000000 --- a/src/routes/(app)/app/users/new/+page.server.js +++ /dev/null @@ -1,91 +0,0 @@ -import { prisma } from '$lib/prisma'; -import { fail, redirect } from '@sveltejs/kit'; - -export const actions = { - addUser: async ({ request, locals }) => { - if (!locals.org || !locals.org.id) { - return fail(500, { error: 'Organization not found' }); - } - const organizationId = locals.org.id; - - const formData = await request.formData(); - const email = formData.get('email')?.toString(); - const name = formData.get('name')?.toString(); - const role = formData.get('role')?.toString(); - - // Basic validation - if (!email || !name || !role) { - return fail(400, { error: 'Missing required fields', data: { email, name, role } }); - } - - if (role !== 'ADMIN' && role !== 'USER') { - return fail(400, { error: 'Invalid role specified', data: { email, name, role } }); - } - - let userIdToLink; - - try { - // Check if user exists globally - let existingUser = await prisma.user.findUnique({ - where: { email }, - }); - - if (existingUser) { - userIdToLink = existingUser.id; - // Check if user is already in this organization - const existingUserOrganization = await prisma.userOrganization.findFirst({ - where: { - userId: existingUser.id, - organizationId: organizationId, - }, - }); - - if (existingUserOrganization) { - return fail(400, { error: 'User already exists in this organization', data: { email, name, role } }); - } - // If user exists globally but not in this org, we'll link them later - } else { - // Create new user if they don't exist globally - // Assuming user_id should be unique, often email is used or a generated cuid/uuid - // For now, let's assume user_id can be the email if it's meant to be a unique string identifier - // and the actual primary key is 'id' (auto-increment or CUID). - // If 'user_id' is meant to be the Clerk/Auth0 ID, this might need adjustment - // based on how that ID is obtained or if it's set post-creation. - // The schema provided has `user_id String @unique` - const newUser = await prisma.user.create({ - data: { - email, - name, - user_id: email, // Assuming email can serve as the initial unique user_id - }, - }); - userIdToLink = newUser.id; - } - } catch (error) { - console.error('Error finding or creating user:', error); - return fail(500, { error: 'Could not process user information', data: { email, name, role } }); - } - - try { - // Link user to the organization - await prisma.userOrganization.create({ - data: { - userId: userIdToLink, - organizationId: organizationId, - role: role, // 'ADMIN' or 'USER' - }, - }); - - // On success, it's often good to redirect to avoid form resubmission issues, - // or return a success object that the page can use to update its state. - // For this task, returning a success object is specified. - return { success: true, message: 'User added successfully!' }; - - } catch (error) { - console.error('Error linking user to organization:', error); - // This could happen if, for example, a race condition occurred or a DB constraint was violated. - // The earlier check for existingUserOrganization should prevent most common cases. - return fail(500, { error: 'Could not add user to organization', data: { email, name, role } }); - } - }, -}; diff --git a/src/routes/(app)/app/users/new/+page.svelte b/src/routes/(app)/app/users/new/+page.svelte deleted file mode 100644 index cf3cbad..0000000 --- a/src/routes/(app)/app/users/new/+page.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - - - New User - - -
-

Add New User

- - {#if form?.success} -

User added successfully!

- {/if} - - {#if form?.error} -

{form.error}

- {/if} - -
-
- - - {#if form?.errors?.email} -

{form.errors.email}

- {/if} -
- -
- - - {#if form?.errors?.name} -

{form.errors.name}

- {/if} -
- -
- - - {#if form?.errors?.role} -

{form.errors.role}

- {/if} -
- - -
-
- - diff --git a/src/routes/(no-layout)/org/[org_id]/+page.server.js b/src/routes/(no-layout)/org/[org_id]/+page.server.js deleted file mode 100644 index 353baf9..0000000 --- a/src/routes/(no-layout)/org/[org_id]/+page.server.js +++ /dev/null @@ -1,215 +0,0 @@ -import prisma from '$lib/prisma' -import { fail, redirect } from '@sveltejs/kit'; - -/** @type {import('./$types').PageServerLoad} */ -export async function load({ params, locals }) { - const org_id = params.org_id; - const user = locals.user; - - // Check if user is admin of the organization - const userOrg = await prisma.userOrganization.findFirst({ - where: { - userId: user.id, - organizationId: org_id, - role: 'ADMIN' - } - }); - if (!userOrg) { - return { - error: { - name: 'You do not have permission to access this organization' - } - }; - } - // Fetch organization details - const organization = await prisma.organization.findUnique({ - where: { - id: params.org_id - } - }); - - // fetch all users in the organization - const users = await prisma.userOrganization.findMany({ - where: { - organizationId: org_id - }, - include: { - user: true - } - }); - // Pass logged-in user id to page for UI logic - return { organization, users, user: { id: user.id } }; -}; - -/** @type {import('./$types').Actions} */ -export const actions = { - update: async ({ request, params, locals }) => { - const org_id = params.org_id; - const user = locals.user; - if (!user) return fail(401, { error: 'Unauthorized' }); - - // Only ADMIN can update - const userOrg = await prisma.userOrganization.findFirst({ - where: { - userId: user.id, - organizationId: org_id, - role: 'ADMIN' - } - }); - if (!userOrg) return fail(403, { error: 'Forbidden' }); - - const formData = await request.formData(); - const name = formData.get('name')?.toString().trim(); - const domain = formData.get('domain')?.toString().trim(); - const description = formData.get('description')?.toString().trim(); - - if (!name) return fail(400, { error: 'Name is required' }); - - try { - await prisma.organization.update({ - where: { id: org_id }, - data: { - name, - domain, - description - } - }); - return { success: true }; - } catch (err) { - return fail(500, { error: 'Failed to update organization' }); - } - }, - - add_user: async ({ request, params, locals }) => { - const org_id = params.org_id; - const user = locals.user; - if (!user) return fail(401, { error: 'Unauthorized' }); - - // Only ADMIN can add - const userOrg = await prisma.userOrganization.findFirst({ - where: { - userId: user.id, - organizationId: org_id, - role: 'ADMIN' - } - }); - if (!userOrg) return fail(403, { error: 'Forbidden' }); - - const formData = await request.formData(); - const email = formData.get('email')?.toString().trim().toLowerCase(); - const role = formData.get('role')?.toString(); - if (!email || !role) return fail(400, { error: 'Email and role are required' }); - - // Find user by email - const foundUser = await prisma.user.findUnique({ where: { email } }); - if (!foundUser) return fail(404, { error: 'No user found with that email' }); - - // Check if already in org - const already = await prisma.userOrganization.findFirst({ - where: { userId: foundUser.id, organizationId: org_id } - }); - if (already) return fail(400, { error: 'User already in organization' }); - - // Add user to org - await prisma.userOrganization.create({ - data: { - userId: foundUser.id, - organizationId: org_id, - role - } - }); - return { success: true }; - }, - - edit_role: async ({ request, params, locals }) => { - const org_id = params.org_id; - const user = locals.user; - if (!user) return fail(401, { error: 'Unauthorized' }); - - // Only ADMIN can edit - const userOrg = await prisma.userOrganization.findFirst({ - where: { - userId: user.id, - organizationId: org_id, - role: 'ADMIN' - } - }); - if (!userOrg) return fail(403, { error: 'Forbidden' }); - - const formData = await request.formData(); - const user_id = formData.get('user_id')?.toString(); - const role = formData.get('role')?.toString(); - if (!user_id || !role) return fail(400, { error: 'User and role are required' }); - - // Don't allow editing own role (prevent lockout) - if (user_id === user.id) return fail(400, { error: 'You cannot change your own role' }); - - // Don't allow editing role of the only remaining admin - if (role !== 'ADMIN') { - // Count number of admins in org - const adminCount = await prisma.userOrganization.count({ - where: { - organizationId: org_id, - role: 'ADMIN' - } - }); - // If target user is admin and only one admin left, prevent demotion - const target = await prisma.userOrganization.findUnique({ - where: { userId_organizationId: { userId: user_id, organizationId: org_id } } - }); - if (target && target.role === 'ADMIN' && adminCount === 1) { - return fail(400, { error: 'Organization must have at least one admin' }); - } - } - - await prisma.userOrganization.update({ - where: { userId_organizationId: { userId: user_id, organizationId: org_id } }, - data: { role } - }); - return { success: true }; - }, - - remove_user: async ({ request, params, locals }) => { - const org_id = params.org_id; - const user = locals.user; - if (!user) return fail(401, { error: 'Unauthorized' }); - - // Only ADMIN can remove - const userOrg = await prisma.userOrganization.findFirst({ - where: { - userId: user.id, - organizationId: org_id, - role: 'ADMIN' - } - }); - if (!userOrg) return fail(403, { error: 'Forbidden' }); - - const formData = await request.formData(); - const user_id = formData.get('user_id')?.toString(); - if (!user_id) return fail(400, { error: 'User is required' }); - - // Don't allow removing self (prevent lockout) - if (user_id === user.id) return fail(400, { error: 'You cannot remove yourself' }); - - // Don't allow removing the only remaining admin - const target = await prisma.userOrganization.findUnique({ - where: { userId_organizationId: { userId: user_id, organizationId: org_id } } - }); - if (target && target.role === 'ADMIN') { - const adminCount = await prisma.userOrganization.count({ - where: { - organizationId: org_id, - role: 'ADMIN' - } - }); - if (adminCount === 1) { - return fail(400, { error: 'Organization must have at least one admin' }); - } - } - - await prisma.userOrganization.delete({ - where: { userId_organizationId: { userId: user_id, organizationId: org_id } } - }); - return { success: true }; - } -}; diff --git a/src/routes/(no-layout)/org/[org_id]/+page.svelte b/src/routes/(no-layout)/org/[org_id]/+page.svelte deleted file mode 100644 index 3bcd0bc..0000000 --- a/src/routes/(no-layout)/org/[org_id]/+page.svelte +++ /dev/null @@ -1,247 +0,0 @@ - - -
- - - -
- - {#if editing} -
-

- - -

-
- - - - - - - {org.industry} - -
- -
- - -
-
- {:else} -
-

- - {org.name} -

-
- - - {org.domain || '—'} - - - - {org.industry} - -
- {#if org.description} -

{org.description}

- {/if} -
- {/if} -
- - -
-

- Users -

- -
-
- - -
-
- - -
- -
-
- - - - - - - - - - - - {#each users as user, i} - - - - - - - - {/each} - -
NameEmailRoleJoined
-
- {#if user.avatar} - {user.name} - {:else} -
- -
- {/if} - {user.name} -
-
{user.email} - {#if user.isSelf} - - - {user.role} - - {:else} - {#if user.editingRole} -
- - - - -
- {:else} - - - {user.role} - - - {/if} - {/if} -
{user.joined} - {#if user.isSelf} - Remove - {:else} -
confirm('Remove this user from organization?') || event.preventDefault()}> - - -
- {/if} -
-
-
-