First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import issueSSOLoginCode from "@/src/functions/backend/auth/issue-sso-login-code";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import rateLimit from "@/src/utils/rate-limit";
|
||||
import findUserByIdentifier from "@/src/functions/backend/auth/find-user-by-identifier";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import { LoginIdentifierRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (grabLoginMethod() != "code") {
|
||||
throw new AppError(`Please log in with your password.`);
|
||||
}
|
||||
|
||||
const { login } = parseWithSchema({
|
||||
schema: LoginIdentifierRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const window_ms = AppData["RateLimitWindowMinutes"] * 60 * 1000;
|
||||
|
||||
const ip_limit = rateLimit({
|
||||
key: `sso-code-request:ip:${grabRequestIP(req) || "unknown"}`,
|
||||
limit: AppData["SSOCodeRequestLimit"] * 4,
|
||||
window_ms,
|
||||
});
|
||||
|
||||
const identifier_limit = rateLimit({
|
||||
key: `sso-code-request:id:${String(login.email_or_username).toLowerCase()}`,
|
||||
limit: AppData["SSOCodeRequestLimit"],
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!ip_limit.allowed || !identifier_limit.allowed) {
|
||||
throw new AppError(`Too many requests. Please try again later.`);
|
||||
}
|
||||
|
||||
const target_user = await findUserByIdentifier({
|
||||
identifier: login.email_or_username,
|
||||
});
|
||||
|
||||
/**
|
||||
* Same response whether or not the account exists, so this
|
||||
* endpoint can't be used to enumerate users.
|
||||
*/
|
||||
if (!target_user?.id || !target_user.email) {
|
||||
await logEvent({
|
||||
title: "SSO login code requested for unknown user",
|
||||
category: "auth",
|
||||
plain_text: `SSO login code requested for unknown identifier.`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
await logEvent({
|
||||
title: "SSO login code requested",
|
||||
category: "auth",
|
||||
user_id: target_user.id,
|
||||
plain_text: `User #${target_user.id} [${target_user.first_name} ${target_user.last_name}] requested an SSO login code.`,
|
||||
metadata: {
|
||||
user_id: target_user.id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: target_user.id,
|
||||
email: target_user.email,
|
||||
method: "code",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await logEvent({
|
||||
title: "SSO login code request failed",
|
||||
category: "auth",
|
||||
plain_text: `SSO login code request failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import verifyGoogleIdToken from "@/src/functions/backend/auth/verify-google-id-token";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import createSignupUser from "@/src/functions/backend/auth/create-signup-user";
|
||||
import generateUniqueUsername from "@/src/functions/backend/auth/generate-unique-username";
|
||||
import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { GoogleLoginRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { google_token, google_signup } = parseWithSchema({
|
||||
schema: GoogleLoginRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
|
||||
if (!googleClientId) {
|
||||
throw new Error(`Google login is not configured on this server`);
|
||||
}
|
||||
|
||||
const googleUser = await verifyGoogleIdToken({
|
||||
clientId: googleClientId,
|
||||
idToken: google_token,
|
||||
});
|
||||
|
||||
const target_user_res = await grabUsers({
|
||||
query: {
|
||||
search_term_email: googleUser.email,
|
||||
},
|
||||
exact_match: true,
|
||||
});
|
||||
|
||||
let target_user = target_user_res.singleRes;
|
||||
|
||||
if (!target_user?.id && google_signup) {
|
||||
const new_user_id = await createSignupUser({
|
||||
first_name:
|
||||
googleUser.given_name ||
|
||||
googleUser.name?.split(" ")[0] ||
|
||||
googleUser.email.split("@")[0] ||
|
||||
"",
|
||||
last_name: googleUser.family_name,
|
||||
email: googleUser.email.toLowerCase(),
|
||||
username: await generateUniqueUsername({
|
||||
email: googleUser.email,
|
||||
}),
|
||||
});
|
||||
|
||||
await logEvent({
|
||||
title: "Google signup",
|
||||
category: "auth",
|
||||
user_id: new_user_id,
|
||||
plain_text: `User #${new_user_id} signed up with Google.`,
|
||||
metadata: {
|
||||
user_id: new_user_id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
target_user = (
|
||||
await grabUsers({
|
||||
query: { user_id: new_user_id },
|
||||
})
|
||||
).singleRes;
|
||||
}
|
||||
|
||||
if (!target_user?.id) {
|
||||
throw new AppError(
|
||||
`No account exists for ${googleUser.email}. Please sign up first.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (googleUser.picture && !target_user.profile_media_id) {
|
||||
const fullResUrl = googleUser.picture.replace(/=s\d+-c$/, "=s0");
|
||||
|
||||
const res = await fetch(fullResUrl);
|
||||
|
||||
if (res.ok) {
|
||||
const bytes = new Uint8Array(await res.arrayBuffer());
|
||||
|
||||
await uploadAndRecordMedia({
|
||||
data: bytes,
|
||||
user: target_user,
|
||||
media_paradigms: ["user-profile-image"],
|
||||
media_type: "image",
|
||||
is_primary: true,
|
||||
media_name: `user-${target_user.id}-profile`,
|
||||
media_mime_type: "jpeg",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await loginUser({
|
||||
user_id: target_user.id,
|
||||
req,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await logEvent({
|
||||
title: "Google login failed",
|
||||
category: "auth",
|
||||
plain_text: `Google login failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import verifySSOCode from "@/src/functions/backend/auth/verify-sso-code";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { LoginCodeRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { sso_code } = parseWithSchema({
|
||||
schema: LoginCodeRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
/**
|
||||
* Only codes issued by the site-wide login method complete a login.
|
||||
* In password mode that means the password step has passed.
|
||||
*/
|
||||
const user_id = await verifySSOCode({
|
||||
req,
|
||||
sso_code,
|
||||
method: grabLoginMethod(),
|
||||
});
|
||||
|
||||
return await loginUser({
|
||||
user_id,
|
||||
req,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await logEvent({
|
||||
title: "SSO login failed",
|
||||
category: "auth",
|
||||
plain_text: `SSO login failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import issueSSOLoginCode from "@/src/functions/backend/auth/issue-sso-login-code";
|
||||
import findUserByIdentifier from "@/src/functions/backend/auth/find-user-by-identifier";
|
||||
import verifyUserPassword from "@/src/functions/backend/auth/verify-user-password";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import rateLimit from "@/src/utils/rate-limit";
|
||||
import { PasswordLoginRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* First factor of password login. A correct password issues an emailed
|
||||
* login code (second factor), completed through `api/auth/login`.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (grabLoginMethod() != "password") {
|
||||
throw new AppError(`Password login is turned off.`);
|
||||
}
|
||||
|
||||
const { login } = parseWithSchema({
|
||||
schema: PasswordLoginRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const identifier = login.email_or_username;
|
||||
const password = login.password;
|
||||
|
||||
const window_ms = AppData["RateLimitWindowMinutes"] * 60 * 1000;
|
||||
|
||||
const ip_limit = rateLimit({
|
||||
key: `password-login:ip:${grabRequestIP(req) || "unknown"}`,
|
||||
limit: AppData["PasswordAttemptLimit"] * 4,
|
||||
window_ms,
|
||||
});
|
||||
|
||||
const identifier_limit = rateLimit({
|
||||
key: `password-login:id:${identifier.toLowerCase()}`,
|
||||
limit: AppData["PasswordAttemptLimit"],
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!ip_limit.allowed || !identifier_limit.allowed) {
|
||||
throw new AppError(`Too many attempts. Please try again later.`);
|
||||
}
|
||||
|
||||
const target_user = await findUserByIdentifier({ identifier });
|
||||
|
||||
const is_valid = await verifyUserPassword({
|
||||
user_id: target_user?.id,
|
||||
password,
|
||||
});
|
||||
|
||||
if (!is_valid || !target_user?.id || !target_user.email) {
|
||||
await logEvent({
|
||||
title: "Password login failed",
|
||||
category: "auth",
|
||||
user_id: target_user?.id || undefined,
|
||||
plain_text: target_user?.id
|
||||
? `Wrong password for user #${target_user.id}.`
|
||||
: `Password login attempted for unknown identifier.`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: `Incorrect email, username or password.`,
|
||||
};
|
||||
}
|
||||
|
||||
await logEvent({
|
||||
title: "Password verified",
|
||||
category: "auth",
|
||||
user_id: target_user.id,
|
||||
plain_text: `User #${target_user.id} [${target_user.first_name} ${target_user.last_name}] entered a correct password. Login code sent.`,
|
||||
metadata: {
|
||||
user_id: target_user.id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: target_user.id,
|
||||
email: target_user.email,
|
||||
method: "password",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import issueSSOLoginCode from "@/src/functions/backend/auth/issue-sso-login-code";
|
||||
import findUserByIdentifier from "@/src/functions/backend/auth/find-user-by-identifier";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import rateLimit from "@/src/utils/rate-limit";
|
||||
import { LoginIdentifierRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Emails a password reset code (password login mode only). Also how
|
||||
* accounts without a password (e.g. created before password mode, or via
|
||||
* Google) set their first one.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (grabLoginMethod() != "password") {
|
||||
throw new AppError(`Password login is turned off.`);
|
||||
}
|
||||
|
||||
const { login } = parseWithSchema({
|
||||
schema: LoginIdentifierRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const identifier = login.email_or_username;
|
||||
|
||||
const window_ms = AppData["RateLimitWindowMinutes"] * 60 * 1000;
|
||||
|
||||
const ip_limit = rateLimit({
|
||||
key: `password-reset:ip:${grabRequestIP(req) || "unknown"}`,
|
||||
limit: AppData["SSOCodeRequestLimit"] * 4,
|
||||
window_ms,
|
||||
});
|
||||
|
||||
const identifier_limit = rateLimit({
|
||||
key: `password-reset:id:${identifier.toLowerCase()}`,
|
||||
limit: AppData["SSOCodeRequestLimit"],
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!ip_limit.allowed || !identifier_limit.allowed) {
|
||||
throw new AppError(`Too many requests. Please try again later.`);
|
||||
}
|
||||
|
||||
const target_user = await findUserByIdentifier({ identifier });
|
||||
|
||||
/**
|
||||
* Same response whether or not the account exists.
|
||||
*/
|
||||
if (!target_user?.id || !target_user.email) {
|
||||
await logEvent({
|
||||
title: "Password reset requested for unknown user",
|
||||
category: "auth",
|
||||
plain_text: `Password reset requested for unknown identifier.`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
await logEvent({
|
||||
title: "Password reset requested",
|
||||
category: "auth",
|
||||
user_id: target_user.id,
|
||||
plain_text: `User #${target_user.id} requested a password reset code.`,
|
||||
metadata: {
|
||||
user_id: target_user.id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: target_user.id,
|
||||
email: target_user.email,
|
||||
method: "reset",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
import issueSSOLoginCode from "@/src/functions/backend/auth/issue-sso-login-code";
|
||||
import { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { SSOAuth } from "@/src/types";
|
||||
import { getCookie } from "@/src/utils/cookies";
|
||||
import { decrypt } from "@/src/utils/crypt";
|
||||
import EJSON from "@/src/utils/ejson";
|
||||
import rateLimit from "@/src/utils/rate-limit";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Re-sends the login code for the pending login held in the SSO cookie.
|
||||
* The cookie is only issued once the first step has passed, and only
|
||||
* codes for the current login method (or a password reset) are resent.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
}) => {
|
||||
try {
|
||||
const window_ms = AppData["RateLimitWindowMinutes"] * 60 * 1000;
|
||||
|
||||
const ip_limit = rateLimit({
|
||||
key: `sso-resend:ip:${grabRequestIP(req) || "unknown"}`,
|
||||
limit: AppData["SSOCodeRequestLimit"] * 4,
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!ip_limit.allowed) {
|
||||
throw new AppError(`Too many requests. Please try again later.`);
|
||||
}
|
||||
|
||||
const sso_cookie = getCookie(req, AppData["SSOAuthCookieName"]);
|
||||
|
||||
/**
|
||||
* No pending login (e.g. the identifier didn't match an account).
|
||||
* Answer like a real resend so this can't be used to probe.
|
||||
*/
|
||||
if (!sso_cookie) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
const sso_auth = EJSON.parse(await decrypt(sso_cookie)) as SSOAuth;
|
||||
|
||||
const max_age_ms = AppData["SSOPendingLoginMaxMinutes"] * 60 * 1000;
|
||||
const login_method = grabLoginMethod();
|
||||
|
||||
const is_current_method =
|
||||
sso_auth?.method == login_method ||
|
||||
(login_method == "password" && sso_auth?.method == "reset");
|
||||
|
||||
if (
|
||||
!sso_auth?.user_id ||
|
||||
!sso_auth.method ||
|
||||
!is_current_method ||
|
||||
!sso_auth.first_step_at ||
|
||||
Date.now() - sso_auth.first_step_at > max_age_ms
|
||||
) {
|
||||
throw new AppError(`Your login session expired. Please start again.`);
|
||||
}
|
||||
|
||||
const user_limit = rateLimit({
|
||||
key: `sso-resend:user:${sso_auth.user_id}`,
|
||||
limit: AppData["SSOCodeRequestLimit"],
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!user_limit.allowed) {
|
||||
throw new AppError(`Too many requests. Please try again later.`);
|
||||
}
|
||||
|
||||
const target_user = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
targetId: sso_auth.user_id,
|
||||
})
|
||||
).singleRes;
|
||||
|
||||
if (!target_user?.id || !target_user.email) {
|
||||
throw new AppError(`Your login session expired. Please start again.`);
|
||||
}
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: target_user.id,
|
||||
email: target_user.email,
|
||||
method: sso_auth.method,
|
||||
first_step_at: sso_auth.first_step_at,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import verifySSOCode from "@/src/functions/backend/auth/verify-sso-code";
|
||||
import setUserPassword from "@/src/functions/backend/auth/set-user-password";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import validatePassword from "@/src/utils/validate-password";
|
||||
import { ResetPasswordRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Completes a password reset: checks the emailed reset code, saves the new
|
||||
* password, then logs the user in. The emailed code is the proof of
|
||||
* identity here, as with any email-based reset.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (grabLoginMethod() != "password") {
|
||||
throw new AppError(`Password login is turned off.`);
|
||||
}
|
||||
|
||||
const { sso_code, new_password } = parseWithSchema({
|
||||
schema: ResetPasswordRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const password_error = validatePassword({ password: new_password });
|
||||
|
||||
if (password_error) {
|
||||
throw new AppError(password_error);
|
||||
}
|
||||
|
||||
const user_id = await verifySSOCode({
|
||||
req,
|
||||
sso_code,
|
||||
method: "reset",
|
||||
});
|
||||
|
||||
await setUserPassword({ user_id, password: new_password });
|
||||
|
||||
await logEvent({
|
||||
title: "Password reset",
|
||||
category: "auth",
|
||||
user_id,
|
||||
plain_text: `User #${user_id} reset their password.`,
|
||||
metadata: {
|
||||
user_id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return await loginUser({
|
||||
user_id,
|
||||
req,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await logEvent({
|
||||
title: "Password reset failed",
|
||||
category: "auth",
|
||||
plain_text: `Password reset failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { BUN_SQLITE_BSQLITE_USERS, BunSQLiteTables } from "@/db/types/db";
|
||||
import issueSSOLoginCode from "@/src/functions/backend/auth/issue-sso-login-code";
|
||||
import createSignupUser from "@/src/functions/backend/auth/create-signup-user";
|
||||
import setUserPassword from "@/src/functions/backend/auth/set-user-password";
|
||||
import grabLoginMethod from "@/src/functions/backend/auth/grab-login-method";
|
||||
import validatePassword from "@/src/utils/validate-password";
|
||||
import { grabRequestIP, logError } from "@/src/functions/backend/logger";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import rateLimit from "@/src/utils/rate-limit";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { SignupRequestSchema } from "@/src/schemas/auth-schema";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { signup } = parseWithSchema({
|
||||
schema: SignupRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const email = signup.email.trim().toLowerCase();
|
||||
const username = signup.username.trim().toLowerCase();
|
||||
|
||||
const window_ms = AppData["RateLimitWindowMinutes"] * 60 * 1000;
|
||||
|
||||
const ip_limit = rateLimit({
|
||||
key: `signup:ip:${grabRequestIP(req) || "unknown"}`,
|
||||
limit: AppData["SSOCodeRequestLimit"] * 4,
|
||||
window_ms,
|
||||
});
|
||||
|
||||
const email_limit = rateLimit({
|
||||
key: `signup:email:${email}`,
|
||||
limit: AppData["SSOCodeRequestLimit"],
|
||||
window_ms,
|
||||
});
|
||||
|
||||
if (!ip_limit.allowed || !email_limit.allowed) {
|
||||
throw new AppError(`Too many requests. Please try again later.`);
|
||||
}
|
||||
|
||||
if (!AppData["UsernameRegex"].test(username)) {
|
||||
throw new AppError(
|
||||
`Usernames must be 3-32 characters: lowercase letters, numbers, dots, dashes or underscores`,
|
||||
);
|
||||
}
|
||||
|
||||
const login_method = grabLoginMethod();
|
||||
|
||||
const password_error =
|
||||
login_method == "password"
|
||||
? validatePassword({ password: signup.password })
|
||||
: undefined;
|
||||
|
||||
if (password_error) {
|
||||
throw new AppError(password_error);
|
||||
}
|
||||
|
||||
const existing_email_user = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
email: {
|
||||
value: email,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)?.singleRes;
|
||||
|
||||
/**
|
||||
* Respond exactly as a fresh signup would, so this endpoint can't
|
||||
* be used to discover which emails have accounts. In code mode the
|
||||
* owner gets a login code. In password mode nothing is sent, since
|
||||
* a code alone mustn't skip their password.
|
||||
*/
|
||||
if (existing_email_user?.id) {
|
||||
if (login_method == "password") {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: existing_email_user.id,
|
||||
email,
|
||||
method: "code",
|
||||
});
|
||||
}
|
||||
|
||||
const existing_username_user = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_BSQLITE_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
username: {
|
||||
value: username,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)?.singleRes;
|
||||
|
||||
if (existing_username_user?.id) {
|
||||
throw new AppError(
|
||||
`That username is taken. Please choose another.`,
|
||||
);
|
||||
}
|
||||
|
||||
const new_user_id = await createSignupUser({
|
||||
first_name: signup.first_name.trim(),
|
||||
last_name: signup.last_name?.trim(),
|
||||
email,
|
||||
username,
|
||||
});
|
||||
|
||||
if (login_method == "password" && signup.password) {
|
||||
await setUserPassword({
|
||||
user_id: new_user_id,
|
||||
password: signup.password,
|
||||
});
|
||||
}
|
||||
|
||||
return issueSSOLoginCode({
|
||||
user_id: new_user_id,
|
||||
email,
|
||||
method: login_method,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await logError({
|
||||
title: "Signup failed",
|
||||
error,
|
||||
ip_address: grabRequestIP(req),
|
||||
}).catch(() => {});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: safeErrorMessage({ error }),
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user