mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 09:56:36 +00:00
Merge dev into fix/log-analytics-adjustments
This commit is contained in:
@@ -6,10 +6,7 @@ import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { User, users } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import {
|
||||
hashPassword,
|
||||
verifyPassword
|
||||
} from "@server/auth/password";
|
||||
import { hashPassword, verifyPassword } from "@server/auth/password";
|
||||
import { verifyTotpCode } from "@server/auth/totp";
|
||||
import logger from "@server/logger";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
@@ -23,10 +20,10 @@ import ConfirmPasswordReset from "@server/emails/templates/NotifyResetPassword";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const changePasswordBody = z.strictObject({
|
||||
oldPassword: z.string(),
|
||||
newPassword: passwordSchema,
|
||||
code: z.string().optional()
|
||||
});
|
||||
oldPassword: z.string(),
|
||||
newPassword: passwordSchema,
|
||||
code: z.string().optional()
|
||||
});
|
||||
|
||||
export type ChangePasswordBody = z.infer<typeof changePasswordBody>;
|
||||
|
||||
@@ -62,12 +59,14 @@ async function invalidateAllSessionsExceptCurrent(
|
||||
}
|
||||
|
||||
// Delete the user sessions (except current)
|
||||
await trx.delete(sessions).where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
await trx
|
||||
.delete(sessions)
|
||||
.where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to invalidate user sessions except current", e);
|
||||
@@ -157,7 +156,10 @@ export async function changePassword(
|
||||
.where(eq(users.userId, user.userId));
|
||||
|
||||
// Invalidate all sessions except the current one
|
||||
await invalidateAllSessionsExceptCurrent(user.userId, req.session.sessionId);
|
||||
await invalidateAllSessionsExceptCurrent(
|
||||
user.userId,
|
||||
req.session.sessionId
|
||||
);
|
||||
|
||||
try {
|
||||
const email = user.email!;
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
||||
|
||||
export const params = z.strictObject({
|
||||
token: z.string(),
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive()),
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
export type CheckResourceSessionParams = z.infer<typeof params>;
|
||||
@@ -21,7 +21,7 @@ export type CheckResourceSessionResponse = {
|
||||
export async function checkResourceSession(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const parsedParams = params.safeParse(req.params);
|
||||
|
||||
@@ -29,8 +29,8 @@ export async function checkResourceSession(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString(),
|
||||
),
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function checkResourceSession(
|
||||
try {
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
token,
|
||||
resourceId,
|
||||
resourceId
|
||||
);
|
||||
|
||||
let valid = false;
|
||||
@@ -52,15 +52,15 @@ export async function checkResourceSession(
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Checked validity",
|
||||
status: HttpCode.OK,
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to reset password",
|
||||
),
|
||||
"Failed to reset password"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export const disable2faBody = z.strictObject({
|
||||
password: z.string(),
|
||||
code: z.string().optional()
|
||||
});
|
||||
password: z.string(),
|
||||
code: z.string().optional()
|
||||
});
|
||||
|
||||
export type Disable2faBody = z.infer<typeof disable2faBody>;
|
||||
|
||||
@@ -56,7 +56,10 @@ export async function disable2fa(
|
||||
}
|
||||
|
||||
try {
|
||||
const validPassword = await verifyPassword(password, user.passwordHash!);
|
||||
const validPassword = await verifyPassword(
|
||||
password,
|
||||
user.passwordHash!
|
||||
);
|
||||
if (!validPassword) {
|
||||
return next(unauthorized());
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ export * from "./checkResourceSession";
|
||||
export * from "./securityKey";
|
||||
export * from "./startDeviceWebAuth";
|
||||
export * from "./verifyDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
|
||||
@@ -7,10 +7,7 @@ import logger from "@server/logger";
|
||||
import { response } from "@server/lib/response";
|
||||
import { db, deviceWebAuthCodes } from "@server/db";
|
||||
import { eq, and, gt } from "drizzle-orm";
|
||||
import {
|
||||
createSession,
|
||||
generateSessionToken
|
||||
} from "@server/auth/sessions/app";
|
||||
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
|
||||
@@ -22,9 +19,7 @@ export type PollDeviceWebAuthParams = z.infer<typeof paramsSchema>;
|
||||
|
||||
// Helper function to hash device code before querying database
|
||||
function hashDeviceCode(code: string): string {
|
||||
return encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(code))
|
||||
);
|
||||
return encodeHexLowerCase(sha256(new TextEncoder().encode(code)));
|
||||
}
|
||||
|
||||
export type PollDeviceWebAuthResponse = {
|
||||
@@ -127,7 +122,9 @@ export async function pollDeviceWebAuth(
|
||||
|
||||
// Check if userId is set (should be set when verified)
|
||||
if (!deviceCode.userId) {
|
||||
logger.error("Device code is verified but userId is missing", { codeId: deviceCode.codeId });
|
||||
logger.error("Device code is verified but userId is missing", {
|
||||
codeId: deviceCode.codeId
|
||||
});
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
@@ -165,4 +162,3 @@ export async function pollDeviceWebAuth(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import { hashPassword } from "@server/auth/password";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export const requestPasswordResetBody = z.strictObject({
|
||||
email: z.email().toLowerCase()
|
||||
});
|
||||
email: z.email().toLowerCase()
|
||||
});
|
||||
|
||||
export type RequestPasswordResetBody = z.infer<typeof requestPasswordResetBody>;
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import { verifySession } from "@server/auth/sessions/verifySession";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
export const requestTotpSecretBody = z.strictObject({
|
||||
password: z.string(),
|
||||
email: z.email().optional()
|
||||
});
|
||||
password: z.string(),
|
||||
email: z.email().optional()
|
||||
});
|
||||
|
||||
export type RequestTotpSecretBody = z.infer<typeof requestTotpSecretBody>;
|
||||
|
||||
@@ -46,7 +46,8 @@ export async function requestTotpSecret(
|
||||
|
||||
const { password, email } = parsedBody.data;
|
||||
|
||||
const { user: sessionUser, session: existingSession } = await verifySession(req);
|
||||
const { user: sessionUser, session: existingSession } =
|
||||
await verifySession(req);
|
||||
|
||||
let user: User | null = sessionUser;
|
||||
if (!existingSession) {
|
||||
@@ -112,11 +113,7 @@ export async function requestTotpSecret(
|
||||
|
||||
const hex = crypto.getRandomValues(new Uint8Array(20));
|
||||
const secret = encodeHex(hex);
|
||||
const uri = createTOTPKeyURI(
|
||||
appName,
|
||||
user.email!,
|
||||
hex
|
||||
);
|
||||
const uri = createTOTPKeyURI(appName, user.email!, hex);
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
|
||||
@@ -18,11 +18,11 @@ import { sendEmail } from "@server/emails";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
|
||||
export const resetPasswordBody = z.strictObject({
|
||||
email: z.email().toLowerCase(),
|
||||
token: z.string(), // reset secret code
|
||||
newPassword: passwordSchema,
|
||||
code: z.string().optional() // 2fa code
|
||||
});
|
||||
email: z.email().toLowerCase(),
|
||||
token: z.string(), // reset secret code
|
||||
newPassword: passwordSchema,
|
||||
code: z.string().optional() // 2fa code
|
||||
});
|
||||
|
||||
export type ResetPasswordBody = z.infer<typeof resetPasswordBody>;
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ import type {
|
||||
GenerateAuthenticationOptionsOpts,
|
||||
AuthenticatorTransportFuture
|
||||
} from "@simplewebauthn/server";
|
||||
import {
|
||||
isoBase64URL
|
||||
} from '@simplewebauthn/server/helpers';
|
||||
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
||||
import config from "@server/lib/config";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
@@ -30,10 +28,12 @@ import { verifyTotpCode } from "@server/auth/totp";
|
||||
|
||||
// The RP ID is the domain name of your application
|
||||
const rpID = (() => {
|
||||
const url = config.getRawConfig().app.dashboard_url ? new URL(config.getRawConfig().app.dashboard_url!) : undefined;
|
||||
const url = config.getRawConfig().app.dashboard_url
|
||||
? new URL(config.getRawConfig().app.dashboard_url!)
|
||||
: undefined;
|
||||
// For localhost, we must use 'localhost' without port
|
||||
if (url?.hostname === 'localhost' || !url) {
|
||||
return 'localhost';
|
||||
if (url?.hostname === "localhost" || !url) {
|
||||
return "localhost";
|
||||
}
|
||||
return url.hostname;
|
||||
})();
|
||||
@@ -46,25 +46,38 @@ const origin = config.getRawConfig().app.dashboard_url || "localhost";
|
||||
// This supports clustered deployments and persists across server restarts
|
||||
|
||||
// Clean up expired challenges every 5 minutes
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(lt(webauthnChallenge.expiresAt, now));
|
||||
// logger.debug("Cleaned up expired security key challenges");
|
||||
} catch (error) {
|
||||
logger.error("Failed to clean up expired security key challenges", error);
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
setInterval(
|
||||
async () => {
|
||||
try {
|
||||
const now = Date.now();
|
||||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(lt(webauthnChallenge.expiresAt, now));
|
||||
// logger.debug("Cleaned up expired security key challenges");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to clean up expired security key challenges",
|
||||
error
|
||||
);
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000
|
||||
);
|
||||
|
||||
// Helper functions for challenge management
|
||||
async function storeChallenge(sessionId: string, challenge: string, securityKeyName?: string, userId?: string) {
|
||||
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes
|
||||
|
||||
async function storeChallenge(
|
||||
sessionId: string,
|
||||
challenge: string,
|
||||
securityKeyName?: string,
|
||||
userId?: string
|
||||
) {
|
||||
const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Delete any existing challenge for this session
|
||||
await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
|
||||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
|
||||
// Insert new challenge
|
||||
await db.insert(webauthnChallenge).values({
|
||||
sessionId,
|
||||
@@ -88,7 +101,9 @@ async function getChallenge(sessionId: string) {
|
||||
|
||||
// Check if expired
|
||||
if (challengeData.expiresAt < Date.now()) {
|
||||
await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -96,7 +111,9 @@ async function getChallenge(sessionId: string) {
|
||||
}
|
||||
|
||||
async function clearChallenge(sessionId: string) {
|
||||
await db.delete(webauthnChallenge).where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
await db
|
||||
.delete(webauthnChallenge)
|
||||
.where(eq(webauthnChallenge.sessionId, sessionId));
|
||||
}
|
||||
|
||||
export const registerSecurityKeyBody = z.strictObject({
|
||||
@@ -153,7 +170,10 @@ export async function startRegistration(
|
||||
|
||||
try {
|
||||
// Verify password
|
||||
const validPassword = await verifyPassword(password, user.passwordHash!);
|
||||
const validPassword = await verifyPassword(
|
||||
password,
|
||||
user.passwordHash!
|
||||
);
|
||||
if (!validPassword) {
|
||||
return next(unauthorized());
|
||||
}
|
||||
@@ -197,9 +217,11 @@ export async function startRegistration(
|
||||
.from(securityKeys)
|
||||
.where(eq(securityKeys.userId, user.userId));
|
||||
|
||||
const excludeCredentials = existingSecurityKeys.map(key => ({
|
||||
const excludeCredentials = existingSecurityKeys.map((key) => ({
|
||||
id: key.credentialId,
|
||||
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined
|
||||
transports: key.transports
|
||||
? (JSON.parse(key.transports) as AuthenticatorTransportFuture[])
|
||||
: undefined
|
||||
}));
|
||||
|
||||
const options: GenerateRegistrationOptionsOpts = {
|
||||
@@ -207,18 +229,23 @@ export async function startRegistration(
|
||||
rpID,
|
||||
userID: isoBase64URL.toBuffer(user.userId),
|
||||
userName: user.email || user.username,
|
||||
attestationType: 'none',
|
||||
attestationType: "none",
|
||||
excludeCredentials,
|
||||
authenticatorSelection: {
|
||||
residentKey: 'preferred',
|
||||
userVerification: 'preferred',
|
||||
residentKey: "preferred",
|
||||
userVerification: "preferred"
|
||||
}
|
||||
};
|
||||
|
||||
const registrationOptions = await generateRegistrationOptions(options);
|
||||
|
||||
// Store challenge in database
|
||||
await storeChallenge(req.session.sessionId, registrationOptions.challenge, name, user.userId);
|
||||
await storeChallenge(
|
||||
req.session.sessionId,
|
||||
registrationOptions.challenge,
|
||||
name,
|
||||
user.userId
|
||||
);
|
||||
|
||||
return response<typeof registrationOptions>(res, {
|
||||
data: registrationOptions,
|
||||
@@ -270,7 +297,7 @@ export async function verifyRegistration(
|
||||
try {
|
||||
// Get challenge from database
|
||||
const challengeData = await getChallenge(req.session.sessionId);
|
||||
|
||||
|
||||
if (!challengeData) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -292,10 +319,7 @@ export async function verifyRegistration(
|
||||
|
||||
if (!verified || !registrationInfo) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Verification failed"
|
||||
)
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Verification failed")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -303,9 +327,13 @@ export async function verifyRegistration(
|
||||
await db.insert(securityKeys).values({
|
||||
credentialId: registrationInfo.credential.id,
|
||||
userId: user.userId,
|
||||
publicKey: isoBase64URL.fromBuffer(registrationInfo.credential.publicKey),
|
||||
publicKey: isoBase64URL.fromBuffer(
|
||||
registrationInfo.credential.publicKey
|
||||
),
|
||||
signCount: registrationInfo.credential.counter || 0,
|
||||
transports: registrationInfo.credential.transports ? JSON.stringify(registrationInfo.credential.transports) : null,
|
||||
transports: registrationInfo.credential.transports
|
||||
? JSON.stringify(registrationInfo.credential.transports)
|
||||
: null,
|
||||
name: challengeData.securityKeyName,
|
||||
lastUsed: new Date().toISOString(),
|
||||
dateCreated: new Date().toISOString()
|
||||
@@ -407,7 +435,10 @@ export async function deleteSecurityKey(
|
||||
|
||||
try {
|
||||
// Verify password
|
||||
const validPassword = await verifyPassword(password, user.passwordHash!);
|
||||
const validPassword = await verifyPassword(
|
||||
password,
|
||||
user.passwordHash!
|
||||
);
|
||||
if (!validPassword) {
|
||||
return next(unauthorized());
|
||||
}
|
||||
@@ -447,10 +478,12 @@ export async function deleteSecurityKey(
|
||||
|
||||
await db
|
||||
.delete(securityKeys)
|
||||
.where(and(
|
||||
eq(securityKeys.credentialId, credentialId),
|
||||
eq(securityKeys.userId, user.userId)
|
||||
));
|
||||
.where(
|
||||
and(
|
||||
eq(securityKeys.credentialId, credentialId),
|
||||
eq(securityKeys.userId, user.userId)
|
||||
)
|
||||
);
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
@@ -502,10 +535,7 @@ export async function startAuthentication(
|
||||
|
||||
if (!user || user.type !== UserType.Internal) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid credentials"
|
||||
)
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid credentials")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -525,25 +555,37 @@ export async function startAuthentication(
|
||||
);
|
||||
}
|
||||
|
||||
allowCredentials = userSecurityKeys.map(key => ({
|
||||
allowCredentials = userSecurityKeys.map((key) => ({
|
||||
id: key.credentialId,
|
||||
transports: key.transports ? JSON.parse(key.transports) as AuthenticatorTransportFuture[] : undefined
|
||||
transports: key.transports
|
||||
? (JSON.parse(
|
||||
key.transports
|
||||
) as AuthenticatorTransportFuture[])
|
||||
: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
const options: GenerateAuthenticationOptionsOpts = {
|
||||
rpID,
|
||||
allowCredentials,
|
||||
userVerification: 'preferred',
|
||||
userVerification: "preferred"
|
||||
};
|
||||
|
||||
const authenticationOptions = await generateAuthenticationOptions(options);
|
||||
const authenticationOptions =
|
||||
await generateAuthenticationOptions(options);
|
||||
|
||||
// Generate a temporary session ID for unauthenticated users
|
||||
const tempSessionId = email ? `temp_${email}_${Date.now()}` : `temp_${Date.now()}`;
|
||||
const tempSessionId = email
|
||||
? `temp_${email}_${Date.now()}`
|
||||
: `temp_${Date.now()}`;
|
||||
|
||||
// Store challenge in database
|
||||
await storeChallenge(tempSessionId, authenticationOptions.challenge, undefined, userId);
|
||||
await storeChallenge(
|
||||
tempSessionId,
|
||||
authenticationOptions.challenge,
|
||||
undefined,
|
||||
userId
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: { ...authenticationOptions, tempSessionId },
|
||||
@@ -580,7 +622,7 @@ export async function verifyAuthentication(
|
||||
}
|
||||
|
||||
const { credential } = parsedBody.data;
|
||||
const tempSessionId = req.headers['x-temp-session-id'] as string;
|
||||
const tempSessionId = req.headers["x-temp-session-id"] as string;
|
||||
|
||||
if (!tempSessionId) {
|
||||
return next(
|
||||
@@ -594,7 +636,7 @@ export async function verifyAuthentication(
|
||||
try {
|
||||
// Get challenge from database
|
||||
const challengeData = await getChallenge(tempSessionId);
|
||||
|
||||
|
||||
if (!challengeData) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -646,7 +688,11 @@ export async function verifyAuthentication(
|
||||
id: securityKey.credentialId,
|
||||
publicKey: isoBase64URL.toBuffer(securityKey.publicKey),
|
||||
counter: securityKey.signCount,
|
||||
transports: securityKey.transports ? JSON.parse(securityKey.transports) as AuthenticatorTransportFuture[] : undefined
|
||||
transports: securityKey.transports
|
||||
? (JSON.parse(
|
||||
securityKey.transports
|
||||
) as AuthenticatorTransportFuture[])
|
||||
: undefined
|
||||
},
|
||||
requireUserVerification: false
|
||||
});
|
||||
@@ -672,7 +718,8 @@ export async function verifyAuthentication(
|
||||
.where(eq(securityKeys.credentialId, credentialId));
|
||||
|
||||
// Create session for the user
|
||||
const { createSession, generateSessionToken, serializeSessionCookie } = await import("@server/auth/sessions/app");
|
||||
const { createSession, generateSessionToken, serializeSessionCookie } =
|
||||
await import("@server/auth/sessions/app");
|
||||
const token = generateSessionToken();
|
||||
const session = await createSession(token, user.userId);
|
||||
const isSecure = req.protocol === "https";
|
||||
@@ -703,4 +750,4 @@ export async function verifyAuthentication(
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,14 @@ export async function signup(
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password, inviteToken, inviteId, termsAcceptedTimestamp, marketingEmailConsent } =
|
||||
parsedBody.data;
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
inviteToken,
|
||||
inviteId,
|
||||
termsAcceptedTimestamp,
|
||||
marketingEmailConsent
|
||||
} = parsedBody.data;
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const userId = generateId(15);
|
||||
@@ -222,7 +228,9 @@ export async function signup(
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
if (build == "saas" && marketingEmailConsent) {
|
||||
logger.debug(`User ${email} opted in to marketing emails during signup.`);
|
||||
logger.debug(
|
||||
`User ${email} opted in to marketing emails during signup.`
|
||||
);
|
||||
moveEmailToAudience(email, AudienceIds.SignUps);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import { maxmindLookup } from "@server/db/maxmind";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
|
||||
const bodySchema = z.object({
|
||||
deviceName: z.string().optional(),
|
||||
applicationName: z.string().min(1, "Application name is required")
|
||||
}).strict();
|
||||
const bodySchema = z
|
||||
.object({
|
||||
deviceName: z.string().optional(),
|
||||
applicationName: z.string().min(1, "Application name is required")
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type StartDeviceWebAuthBody = z.infer<typeof bodySchema>;
|
||||
|
||||
@@ -34,14 +36,12 @@ function generateDeviceCode(): string {
|
||||
|
||||
// Helper function to hash device code before storing in database
|
||||
function hashDeviceCode(code: string): string {
|
||||
return encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(code))
|
||||
);
|
||||
return encodeHexLowerCase(sha256(new TextEncoder().encode(code)));
|
||||
}
|
||||
|
||||
// Helper function to extract IP from request
|
||||
function extractIpFromRequest(req: Request): string | undefined {
|
||||
const ip = req.ip || req.socket.remoteAddress;
|
||||
const ip = req.ip;
|
||||
if (!ip) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -75,10 +75,10 @@ async function getCityFromIp(ip: string): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// MaxMind CountryResponse doesn't include city by default
|
||||
// If city data is available, it would be in result.city?.names?.en
|
||||
// But since we're using CountryResponse type, we'll just return undefined
|
||||
// The user said "don't do this if not easy", so we'll skip city for now
|
||||
if (result.country) {
|
||||
return result.country.names?.en || result.country.iso_code;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to get city from IP", error);
|
||||
|
||||
@@ -5,4 +5,4 @@ export type TransferSessionResponse = {
|
||||
|
||||
export type GetSessionTransferTokenRenponse = {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const validateSetupTokenSchema = z.strictObject({
|
||||
token: z.string().min(1, "Token is required")
|
||||
});
|
||||
token: z.string().min(1, "Token is required")
|
||||
});
|
||||
|
||||
export type ValidateSetupTokenResponse = {
|
||||
valid: boolean;
|
||||
@@ -41,10 +41,7 @@ export async function validateSetupToken(
|
||||
.select()
|
||||
.from(setupTokens)
|
||||
.where(
|
||||
and(
|
||||
eq(setupTokens.token, token),
|
||||
eq(setupTokens.used, false)
|
||||
)
|
||||
and(eq(setupTokens.token, token), eq(setupTokens.used, false))
|
||||
);
|
||||
|
||||
if (!setupToken) {
|
||||
@@ -79,4 +76,4 @@ export async function validateSetupToken(
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import { freeLimitSet, limitsService } from "@server/lib/billing";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export const verifyEmailBody = z.strictObject({
|
||||
code: z.string()
|
||||
});
|
||||
code: z.string()
|
||||
});
|
||||
|
||||
export type VerifyEmailBody = z.infer<typeof verifyEmailBody>;
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ import { verifySession } from "@server/auth/sessions/verifySession";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
|
||||
export const verifyTotpBody = z.strictObject({
|
||||
email: z.email().optional(),
|
||||
password: z.string().optional(),
|
||||
code: z.string()
|
||||
});
|
||||
email: z.email().optional(),
|
||||
password: z.string().optional(),
|
||||
code: z.string()
|
||||
});
|
||||
|
||||
export type VerifyTotpBody = z.infer<typeof verifyTotpBody>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user