Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 });
}
}
36 changes: 27 additions & 9 deletions app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ 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';

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 @@ -16,9 +17,19 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const users = await userService.getAllUsers();

return NextResponse.json({ success: true, users }, { status: 200 });
const [usersWithRoles, usersWithoutRoles] = await Promise.all([
userService.getAllUsersWithRoles(),
userService.getAllUsersWithoutRoles(),
]);

return NextResponse.json(
{
success: true,
users: usersWithRoles,
usersWithoutRoles,
},
{ status: 200 },
);
} catch (error) {
console.error('Failed to fetch users:', error);
return NextResponse.json({ success: false, error: 'Failed to fetch users' }, { status: 500 });
Expand All @@ -28,13 +39,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