forked from atums.world/backend
start of the 50th remake of a file host doomed to never be finished
This commit is contained in:
commit
46c05ca3a9
33 changed files with 2155 additions and 0 deletions
161
src/routes/api/auth/login.ts
Normal file
161
src/routes/api/auth/login.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import {
|
||||
isValidEmail,
|
||||
isValidPassword,
|
||||
isValidUsername,
|
||||
} from "@config/sql/users";
|
||||
import { password as bunPassword, type ReservedSQL, sql } from "bun";
|
||||
|
||||
import { logger } from "@/helpers/logger";
|
||||
import { sessionManager } from "@/helpers/sessions";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
};
|
||||
|
||||
async function handler(
|
||||
request: ExtendedRequest,
|
||||
requestBody: unknown,
|
||||
): Promise<Response> {
|
||||
if (request.session) {
|
||||
if ((request.session as ApiUserSession).is_api) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 403,
|
||||
error: "You cannot log in while using an authorization token",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 403,
|
||||
error: "Already logged in",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const { username, email, password } = requestBody as {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
if (!password || (!username && !email)) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 400,
|
||||
error: "Expected username or email, and password",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const validations: UserValidation[] = [
|
||||
{ check: isValidUsername(username), field: "Username" },
|
||||
{ check: isValidEmail(email), field: "Email" },
|
||||
{ check: isValidPassword(password), field: "Password" },
|
||||
];
|
||||
|
||||
validations.forEach(({ check }: UserValidation): void => {
|
||||
if (!check.valid && check.error) {
|
||||
errors.push(check.error);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 400,
|
||||
errors,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const reservation: ReservedSQL = await sql.reserve();
|
||||
let user: User | null = null;
|
||||
|
||||
try {
|
||||
user = await reservation`
|
||||
SELECT * FROM users
|
||||
WHERE (username = ${username} OR email = ${email})
|
||||
LIMIT 1;
|
||||
`.then((rows: User[]): User | null => rows[0]);
|
||||
|
||||
if (!user) {
|
||||
await bunPassword.verify("fake", await bunPassword.hash("fake"));
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 401,
|
||||
error: "Invalid username, email, or password",
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const passwordMatch: boolean = await bunPassword.verify(
|
||||
password,
|
||||
user.password,
|
||||
);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 401,
|
||||
error: "Invalid username, email, or password",
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(["Error logging in", error as Error]);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 500,
|
||||
error: "An error occurred while logging in",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
if (reservation) reservation.release();
|
||||
}
|
||||
|
||||
const sessionCookie: string = await sessionManager.createSession(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
email_verified: user.email_verified,
|
||||
roles: user.roles,
|
||||
avatar: user.avatar,
|
||||
timezone: user.timezone,
|
||||
authorization_token: user.authorization_token,
|
||||
},
|
||||
request.headers.get("User-Agent") || "",
|
||||
);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: true,
|
||||
code: 200,
|
||||
},
|
||||
{ status: 200, headers: { "Set-Cookie": sessionCookie } },
|
||||
);
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
50
src/routes/api/auth/logout.ts
Normal file
50
src/routes/api/auth/logout.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { sessionManager } from "@/helpers/sessions";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
accepts: "*/*",
|
||||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
if (!request.session) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 403,
|
||||
error: "You are not logged in",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
if ((request.session as ApiUserSession).is_api) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 403,
|
||||
error: "You cannot logout while using an authorization token",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
sessionManager.invalidateSession(request);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: true,
|
||||
code: 200,
|
||||
message: "Successfully logged out",
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Set-Cookie":
|
||||
"session=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
197
src/routes/api/auth/register.ts
Normal file
197
src/routes/api/auth/register.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import { getSetting } from "@config/sql/settings";
|
||||
import {
|
||||
isValidEmail,
|
||||
isValidInvite,
|
||||
isValidPassword,
|
||||
isValidUsername,
|
||||
} from "@config/sql/users";
|
||||
import { password as bunPassword, type ReservedSQL, sql } from "bun";
|
||||
import type { UUID } from "crypto";
|
||||
|
||||
import { logger } from "@/helpers/logger";
|
||||
import { sessionManager } from "@/helpers/sessions";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
accepts: "application/json",
|
||||
returns: "application/json",
|
||||
needsBody: "json",
|
||||
};
|
||||
|
||||
async function handler(
|
||||
request: ExtendedRequest,
|
||||
requestBody: unknown,
|
||||
): Promise<Response> {
|
||||
const { username, email, password, invite } = requestBody as {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
invite?: string;
|
||||
};
|
||||
|
||||
if (!username || !email || !password) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 400,
|
||||
error: "Expected username, email, and password",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
const validations: UserValidation[] = [
|
||||
{ check: isValidUsername(username), field: "Username" },
|
||||
{ check: isValidEmail(email), field: "Email" },
|
||||
{ check: isValidPassword(password), field: "Password" },
|
||||
];
|
||||
|
||||
validations.forEach(({ check }: UserValidation): void => {
|
||||
if (!check.valid && check.error) {
|
||||
errors.push(check.error);
|
||||
}
|
||||
});
|
||||
|
||||
const reservation: ReservedSQL = await sql.reserve();
|
||||
let firstUser: boolean = false;
|
||||
let invitedBy: UUID | null = null;
|
||||
let roles: string[] = [];
|
||||
|
||||
try {
|
||||
const registrationEnabled: boolean =
|
||||
(await getSetting("registrationEnabled", reservation)) === "true";
|
||||
const invitationsEnabled: boolean =
|
||||
(await getSetting("invitationsEnabled", reservation)) === "true";
|
||||
|
||||
firstUser =
|
||||
Number(
|
||||
(await reservation`SELECT COUNT(*) AS count FROM users;`)[0]
|
||||
?.count,
|
||||
) === 0;
|
||||
|
||||
if (!firstUser && invite) {
|
||||
const inviteValidation: { valid: boolean; error?: string } =
|
||||
isValidInvite(invite);
|
||||
if (!inviteValidation.valid && inviteValidation.error) {
|
||||
errors.push(inviteValidation.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(!firstUser && !registrationEnabled && !invite) ||
|
||||
(!firstUser && invite && !invitationsEnabled)
|
||||
) {
|
||||
errors.push("Registration is disabled");
|
||||
}
|
||||
|
||||
roles.push("user");
|
||||
if (firstUser) {
|
||||
roles.push("admin");
|
||||
}
|
||||
|
||||
const { usernameExists, emailExists } = await reservation`
|
||||
SELECT
|
||||
EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER(${username})) AS usernameExists,
|
||||
EXISTS(SELECT 1 FROM users WHERE LOWER(email) = LOWER(${email})) AS emailExists;
|
||||
`;
|
||||
|
||||
if (usernameExists) errors.push("Username already exists");
|
||||
if (emailExists) errors.push("Email already exists");
|
||||
if (invite) {
|
||||
invitedBy = (
|
||||
await reservation`SELECT user_id FROM invites WHERE invite = ${invite};`
|
||||
)[0]?.id;
|
||||
if (!invitedBy) errors.push("Invalid invite code");
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push("An error occurred while checking for existing users");
|
||||
logger.error(["Error checking for existing users:", error as Error]);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 400,
|
||||
errors,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let user: User | null = null;
|
||||
const hashedPassword: string = await bunPassword.hash(password, {
|
||||
algorithm: "argon2id",
|
||||
});
|
||||
const defaultTimezone: string =
|
||||
(await getSetting("default_timezone", reservation)) || "UTC";
|
||||
|
||||
try {
|
||||
user = (
|
||||
await reservation`
|
||||
INSERT INTO users (username, email, password, invited_by, roles, timezone)
|
||||
VALUES (${username}, ${email}, ${hashedPassword}, ${invitedBy}, ARRAY[${roles.join(",")}]::TEXT[], ${defaultTimezone})
|
||||
RETURNING *;
|
||||
`
|
||||
)[0];
|
||||
|
||||
if (!user) {
|
||||
logger.error("User was not created");
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 500,
|
||||
error: "An error occurred with the user registration",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
if (invitedBy) {
|
||||
await reservation`DELETE FROM invites WHERE invite = ${invite};`;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
"Error inserting user into the database:",
|
||||
error as Error,
|
||||
]);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 500,
|
||||
error: "An error occurred while creating the user",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
reservation.release();
|
||||
}
|
||||
|
||||
const sessionCookie: string = await sessionManager.createSession(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
email_verified: user.email_verified,
|
||||
roles: user.roles,
|
||||
avatar: user.avatar,
|
||||
timezone: user.timezone,
|
||||
authorization_token: user.authorization_token,
|
||||
},
|
||||
request.headers.get("User-Agent") || "",
|
||||
);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
success: true,
|
||||
code: 201,
|
||||
message: "User Registered",
|
||||
id: user.id,
|
||||
},
|
||||
{ status: 201, headers: { "Set-Cookie": sessionCookie } },
|
||||
);
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
15
src/routes/api/index.ts
Normal file
15
src/routes/api/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(): Promise<Response> {
|
||||
// TODO: Put something useful here
|
||||
|
||||
return Response.json({
|
||||
message: "Hello, World!",
|
||||
});
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
17
src/routes/index.ts
Normal file
17
src/routes/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { renderEjsTemplate } from "@helpers/ejs";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
returns: "text/html",
|
||||
};
|
||||
|
||||
async function handler(): Promise<Response> {
|
||||
const ejsTemplateData: EjsTemplateData = {
|
||||
title: "Hello, World!",
|
||||
};
|
||||
|
||||
return await renderEjsTemplate("index", ejsTemplateData);
|
||||
}
|
||||
|
||||
export { handler, routeDef };
|
Loading…
Add table
Add a link
Reference in a new issue