Files
techtracker/packages/backend/convex/auth.ts

174 lines
5.1 KiB
TypeScript

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<User> => {
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 };
},
});