diff --git a/src/app/api/admin/2fa/global/route.ts b/src/app/api/admin/2fa/global/route.ts new file mode 100644 index 000000000..ee9640154 --- /dev/null +++ b/src/app/api/admin/2fa/global/route.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import prisma from '@/lib/prisma'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canEnforceTwoFactorAuthForEveryone } from '@/permissions'; + +export async function POST(request: Request) { + const schema = z.object({ required: z.boolean() }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canEnforceTwoFactorAuthForEveryone(auth))) { + return unauthorized(); + } + + const { required } = body; + + await prisma.client.appSetting.upsert({ + where: { key: 'twoFactorRequiredGlobal' }, + update: { value: String(required) }, + create: { key: 'twoFactorRequiredGlobal', value: String(required) }, + }); + + return json({ ok: true, required }); +} diff --git a/src/app/api/admin/teams/[teamId]/2fa/route.ts b/src/app/api/admin/teams/[teamId]/2fa/route.ts new file mode 100644 index 000000000..bc3a66806 --- /dev/null +++ b/src/app/api/admin/teams/[teamId]/2fa/route.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import prisma from '@/lib/prisma'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { canEnforceTwoFactorAuthForTeam } from '@/permissions'; + +export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ required: z.boolean() }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canEnforceTwoFactorAuthForTeam(auth, teamId))) { + return unauthorized(); + } + const { required } = body; + + await prisma.client.team.update({ + where: { id: teamId }, + data: { twoFactorRequired: required }, + }); + + return json({ ok: true, teamId, twoFactorRequired: required }); +} diff --git a/src/app/api/admin/users/[userId]/2fa/route.ts b/src/app/api/admin/users/[userId]/2fa/route.ts new file mode 100644 index 000000000..7cb09cd64 --- /dev/null +++ b/src/app/api/admin/users/[userId]/2fa/route.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import prisma from '@/lib/prisma'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { updateUser } from '@/queries/prisma/user'; +import { canEnforceTwoFactorAuthForUser } from '@/permissions'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + if (!(await canEnforceTwoFactorAuthForUser(auth))) { + return unauthorized(); + } + + const { userId } = await params; + + const twoFactor = await prisma.client.twoFactorAuth.findUnique({ where: { userId } }); + + return json({ isEnabled: twoFactor?.isEnabled ?? false }); +} + +export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ required: z.boolean() }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canEnforceTwoFactorAuthForUser(auth))) { + return unauthorized(); + } + + const { userId } = await params; + const { required } = body; + + const user = await updateUser(userId, { twoFactorRequired: required }); + + return json({ ok: true, userId: user.id, twoFactorRequired: required }); +} diff --git a/src/permissions/link.ts b/src/permissions/link.ts index f274c7b25..be6eef71a 100644 --- a/src/permissions/link.ts +++ b/src/permissions/link.ts @@ -8,7 +8,11 @@ export async function canViewLink({ user, shareToken }: Auth, linkId: string) { return true; } - if (shareToken?.linkId === linkId || shareToken?.websiteId === linkId || shareToken?.linkIds?.includes(linkId)) { + if ( + shareToken?.linkId === linkId || + shareToken?.websiteId === linkId || + shareToken?.linkIds?.includes(linkId) + ) { return true; } diff --git a/src/permissions/pixel.ts b/src/permissions/pixel.ts index 79b1b5976..061363063 100644 --- a/src/permissions/pixel.ts +++ b/src/permissions/pixel.ts @@ -8,7 +8,11 @@ export async function canViewPixel({ user, shareToken }: Auth, pixelId: string) return true; } - if (shareToken?.pixelId === pixelId || shareToken?.websiteId === pixelId || shareToken?.pixelIds?.includes(pixelId)) { + if ( + shareToken?.pixelId === pixelId || + shareToken?.websiteId === pixelId || + shareToken?.pixelIds?.includes(pixelId) + ) { return true; } diff --git a/src/permissions/team.ts b/src/permissions/team.ts index 130290af2..1925f59c6 100644 --- a/src/permissions/team.ts +++ b/src/permissions/team.ts @@ -90,3 +90,11 @@ export async function canCreateTeamWebsite({ user }: Auth, teamId: string) { export async function canViewAllTeams({ user }: Auth) { return user?.isAdmin ?? false; } + +export async function canEnforceTwoFactorAuthForTeam({ user }: Auth, teamId: string) { + if (!user) { + return false; + } + + return user.isAdmin; +} diff --git a/src/permissions/user.ts b/src/permissions/user.ts index 8aa453aec..bdb59c418 100644 --- a/src/permissions/user.ts +++ b/src/permissions/user.ts @@ -35,3 +35,11 @@ export async function canUpdateUser({ user }: Auth, viewedUserId: string) { export async function canDeleteUser({ user }: Auth) { return user?.isAdmin ?? false; } + +export async function canEnforceTwoFactorAuthForEveryone({ user }: Auth) { + return user?.isAdmin ?? false; +} + +export async function canEnforceTwoFactorAuthForUser({ user }: Auth) { + return user?.isAdmin ?? false; +}