import { ConvexError, v } from 'convex/values'; import { convexAuth, getAuthUserId, retrieveAccount, modifyAccountCredentials, } from '@convex-dev/auth/server'; import { api } from './_generated/api'; import { type Id } from './_generated/dataModel'; import { action, mutation, query, type MutationCtx, type QueryCtx, } from './_generated/server'; import Authentik from '@auth/core/providers/authentik'; import { Entra, Password, validatePassword } from './custom/auth'; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Authentik({ allowDangerousEmailAccountLinking: true }), Entra, Password, ], }); export const PASSWORD_MIN = 8; export const PASSWORD_MAX = 100; export const PASSWORD_REGEX = /^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u; type RWCtx = MutationCtx | QueryCtx; type User = { id: Id<'users'>; email: string | null; name: string | null; image: Id<'_storage'> | null; lunchTime: string | null; automaticLunch: boolean; provider: string | null; }; const getUserData = async (ctx: RWCtx, userId: Id<'users'>): Promise => { const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); const image: Id<'_storage'> | null = typeof user.image === 'string' && user.image.length > 0 ? (user.image as Id<'_storage'>) : null; const authAccount = await getUserAuthAccountData(ctx, userId); return { id: user._id, email: user.email ?? null, name: user.name ?? null, image, lunchTime: user.lunchTime ?? null, automaticLunch: user.automaticLunch ?? false, provider: authAccount?.provider ?? null, }; }; const getUserAuthAccountData = async (ctx: RWCtx, userId: Id<'users'>) => { const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); const authAccountData = await ctx.db .query('authAccounts') .withIndex('userIdAndProvider', (q) => q.eq('userId', userId)) .first(); return authAccountData; }; export const getUser = query({ args: { userId: v.optional(v.id('users')) }, handler: async (ctx, args) => { const userId = args.userId ?? await getAuthUserId(ctx); if (!userId) return null; return getUserData(ctx, userId); }, }); export const getUserAuthAccount = query(async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return null; return getUserAuthAccountData(ctx, userId); }); export const getAllUsers = query(async (ctx) => { const users = await ctx.db.query('users').collect(); return Promise.all(users.map((u) => getUserData(ctx, u._id))); }); export const getAllUserIds = query(async (ctx) => { const users = await ctx.db.query('users').collect(); return users.map((u) => u._id); }); export const updateUser = mutation({ args: { name: v.optional(v.string()), email: v.optional(v.string()), image: v.optional(v.id('_storage')), lunchTime: v.optional(v.string()), automaticLunch: v.optional(v.boolean()), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new ConvexError('Not authenticated.'); const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); if (args.lunchTime !== undefined && !args.lunchTime.includes(':')) { throw new ConvexError('Lunch time is invalid.'); } const patch: Partial<{ name: string; email: string; image: Id<'_storage'>; lunchTime: string; automaticLunch: boolean; }> = {}; if (args.name !== undefined) patch.name = args.name; if (args.email !== undefined) patch.email = args.email; if (args.lunchTime !== undefined) patch.lunchTime = args.lunchTime; if (args.automaticLunch !== undefined) patch.automaticLunch = args.automaticLunch; if (args.image !== undefined) { const oldImage = user.image as Id<'_storage'> | undefined; patch.image = args.image; if (oldImage && oldImage !== args.image) { await ctx.storage.delete(oldImage); } } if (Object.keys(patch).length > 0) { await ctx.db.patch(userId, patch); } return { success: true }; }, }); export const updateUserPassword = action({ args: { currentPassword: v.string(), newPassword: v.string(), }, handler: async (ctx, { currentPassword, newPassword }) => { const userId = await getAuthUserId(ctx); if (!userId) throw new ConvexError('Not authenticated.'); const user = await ctx.runQuery(api.auth.getUser, { userId }); if (!user?.email) throw new ConvexError('User not found.'); const verified = await retrieveAccount(ctx, { provider: 'password', account: { id: user.email, secret: currentPassword }, }); if (!verified) throw new ConvexError('Current password is incorrect.'); if (!validatePassword(newPassword)) throw new ConvexError('Invalid password.'); await modifyAccountCredentials(ctx, { provider: 'password', account: { id: user.email, secret: newPassword }, }); return { success: true }; }, });