Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/api/domain-invites/[domainId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';

export async function DELETE(_request: NextRequest, { params: _params }: { params: { domainId: string } }) {
// Return error since domain invites are not implemented yet
return NextResponse.json({ success: false, error: 'Domain invites are coming soon' }, { status: 501 });
}
11 changes: 11 additions & 0 deletions app/api/domain-invites/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';

export async function GET(_request: NextRequest) {
// Return empty array for now since domain invites are "Coming Soon"
return NextResponse.json({ success: true, domainInvites: [] });
Comment thread
ATechAdventurer marked this conversation as resolved.
}

export async function POST(_request: NextRequest) {
// Return error since domain invites are not implemented yet
return NextResponse.json({ success: false, error: 'Domain invites are coming soon' }, { status: 501 });
}
31 changes: 31 additions & 0 deletions app/api/invites/[inviteId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireUserAbility } from '~/auth/session';
import { PermissionAction, PermissionResource } from '@prisma/client';
import { inviteService } from '~/lib/services/inviteService';

export async function DELETE(request: NextRequest, { params }: { params: { inviteId: string } }) {
try {
const { userAbility } = await requireUserAbility(request);

if (!userAbility.can(PermissionAction.delete, PermissionResource.AdminApp)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const { inviteId } = params;

await inviteService.deleteInvite(inviteId);

return NextResponse.json({
success: true,
message: 'Invite removed successfully',
});
} catch (error) {
console.error('Error deleting invite:', error);
Comment thread
bears4barrett marked this conversation as resolved.
Outdated

if (error instanceof Error && error.message === 'Invite not found') {
return NextResponse.json({ success: false, error: 'Invite not found' }, { status: 404 });
}

return NextResponse.json({ success: false, error: 'Failed to remove invite' }, { status: 500 });
}
}
45 changes: 45 additions & 0 deletions app/api/invites/accept/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { requireUserAbility } from '~/auth/session';
import { inviteService } from '~/lib/services/inviteService';

const acceptInviteSchema = z.object({
inviteId: z.string().min(1, 'Invite ID is required'),
});

export async function POST(request: NextRequest) {
try {
const { userId } = await requireUserAbility(request);

const body = await request.json();
const parsedBody = acceptInviteSchema.safeParse(body);

if (!parsedBody.success) {
return NextResponse.json({ success: false, error: parsedBody.error.flatten().fieldErrors }, { status: 400 });
}

const { inviteId } = parsedBody.data;

await inviteService.acceptInvite(inviteId, userId);

return NextResponse.json({
success: true,
message: 'Invite accepted successfully',
});
} catch (error) {
console.error('Error accepting invite:', error);

if (error instanceof Error) {
if (
error.message.includes('not found') ||
error.message.includes('no longer valid') ||
error.message.includes('expired') ||
error.message.includes('does not match')
) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
Comment thread
ATechAdventurer marked this conversation as resolved.

return NextResponse.json({ success: false, error: 'Failed to accept invite' }, { status: 500 });
}
}
74 changes: 74 additions & 0 deletions app/api/invites/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { requireUserAbility } from '~/auth/session';
import { PermissionAction, PermissionResource } from '@prisma/client';
import { inviteService } from '~/lib/services/inviteService';
import { InviteStatus } from '@prisma/client';

const createInviteSchema = z.object({
email: z.string().email('Invalid email address'),
roleId: z.string().min(1, 'Role ID is required'),
});

export async function GET(request: NextRequest) {
try {
const { userAbility } = await requireUserAbility(request);

if (!userAbility.can(PermissionAction.read, PermissionResource.AdminApp)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const { searchParams } = new URL(request.url);
const status = searchParams.get('status') as InviteStatus;

if (status && !Object.values(InviteStatus).includes(status)) {
return NextResponse.json({ success: false, error: 'Invalid status' }, { status: 400 });
}

const invites = status
? await inviteService.getInvitesByStatus(status)
: await inviteService.getInvitesByStatus(InviteStatus.PENDING);

return NextResponse.json({ success: true, invites });
} catch (error) {
console.error('Error fetching invites:', error);
return NextResponse.json({ success: false, error: 'Failed to fetch invites' }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const { userAbility, userId } = await requireUserAbility(request);

if (!userAbility.can(PermissionAction.create, PermissionResource.AdminApp)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const body = await request.json();
const parsedBody = createInviteSchema.safeParse(body);

if (!parsedBody.success) {
return NextResponse.json({ success: false, error: parsedBody.error.flatten().fieldErrors }, { status: 400 });
}

const { email, roleId } = parsedBody.data;

const invite = await inviteService.createInvite(email, roleId, userId);

return NextResponse.json({
success: true,
invite,
message: 'Invitation sent successfully',
});
} catch (error) {
console.error('Error creating invite:', error);

if (error instanceof Error) {
if (error.message.includes('already exists') || error.message.includes('already a pending invite')) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
}
}
Comment thread
ATechAdventurer marked this conversation as resolved.

return NextResponse.json({ success: false, error: 'Failed to create invite' }, { status: 500 });
}
}
34 changes: 26 additions & 8 deletions app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { PermissionAction, PermissionResource } from '@prisma/client';
import { NextRequest, NextResponse } from 'next/server';
import { requireUserAbility } from '~/auth/session';
import { userService } from '~/lib/services/userService';
import { z } from 'zod';
import { createScopedLogger } from '~/utils/logger';

const logger = createScopedLogger('user-api');

type UpdateRoleBody = {
userId: string;
role: 'ADMIN' | 'MEMBER';
};
const updateRoleSchema = z.object({
userId: z.string().min(1, 'User ID is required'),
roleId: z.string().min(1, 'Role ID is required'),
});

export async function GET(request: NextRequest) {
try {
Expand All @@ -19,9 +20,19 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const users = await userService.getAllUsers();
const [usersWithRoles, usersWithoutRoles] = await Promise.all([
userService.getAllUsersWithRoles(),
userService.getAllUsersWithoutRoles(),
]);

return NextResponse.json({ success: true, users }, { status: 200 });
return NextResponse.json(
{
success: true,
users: usersWithRoles,
usersWithoutRoles,
},
{ status: 200 },
);
} catch (error) {
logger.error('Failed to fetch users:', error);
return NextResponse.json({ success: false, error: 'Failed to fetch users' }, { status: 500 });
Expand All @@ -31,13 +42,20 @@ export async function GET(request: NextRequest) {
export async function PATCH(request: NextRequest) {
try {
const { userAbility } = await requireUserAbility(request);
const body = (await request.json()) as UpdateRoleBody;
const body = await request.json();
const parsedBody = updateRoleSchema.safeParse(body);

if (!parsedBody.success) {
return NextResponse.json({ success: false, error: parsedBody.error.flatten().fieldErrors }, { status: 400 });
}

if (!userAbility.can(PermissionAction.manage, PermissionResource.AdminApp)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const targetUser = await userService.updateUserRole(body.userId, body.role);
const { userId, roleId } = parsedBody.data;

const targetUser = await userService.updateUserRoleToNewSystem(userId, roleId);

return NextResponse.json({ success: true, user: targetUser });
} catch (error) {
Expand Down
14 changes: 14 additions & 0 deletions app/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { prisma } from '~/lib/prisma';
import { env } from '~/env';
import { createAuthMiddleware } from 'better-auth/plugins';
import { userService } from '~/lib/services/userService';
import { inviteService } from '~/lib/services/inviteService';

const { BASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, LICENSE_KEY } = env.server;

Expand Down Expand Up @@ -121,6 +122,19 @@ export const auth = betterAuth({
await userService.grantSystemAdminAccess(createdUser.id);
}
});

// Check for pending invites and auto-accept them
try {
const pendingInvite = await inviteService.getInviteByEmail(email);

if (pendingInvite) {
await inviteService.acceptInvite(pendingInvite.id, createdUser.id);
console.log(`✅ Auto-accepted invite for user ${email}`);
}
} catch (error) {
console.error(`❌ Failed to auto-accept invite for user ${email}:`, error);
// Don't fail the auth flow if invite acceptance fails
}
}),
},
});
Loading