diff --git a/config/sql/settings.ts b/config/sql/settings.ts index cd067d3..9e68174 100644 --- a/config/sql/settings.ts +++ b/config/sql/settings.ts @@ -14,6 +14,7 @@ const defaultSettings: Setting[] = [ { key: "date_format", value: "yyyy-MM-dd_HH-mm-ss" }, { key: "random_name_length", value: "8" }, { key: "enable_thumbnails", value: "true" }, + { key: "index_page_stats", value: "true" }, ]; export async function createTable(reservation?: ReservedSQL): Promise { diff --git a/public/assets/fonts/Fira_code/FiraCode-Regular.ttf b/public/assets/fonts/Fira_code/FiraCode-Regular.ttf new file mode 100644 index 0000000..bd73685 Binary files /dev/null and b/public/assets/fonts/Fira_code/FiraCode-Regular.ttf differ diff --git a/public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf b/public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf new file mode 100644 index 0000000..c2293d5 Binary files /dev/null and b/public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf differ diff --git a/public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf b/public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf new file mode 100644 index 0000000..f98a2da Binary files /dev/null and b/public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf differ diff --git a/public/css/global.css b/public/css/global.css new file mode 100644 index 0000000..55545de --- /dev/null +++ b/public/css/global.css @@ -0,0 +1,36 @@ +[data-theme="dark"] { + --background: rgb(31, 30, 30); +} + +body { + font-family: "Ubuntu", sans-serif; + + margin: 0; + padding: 0; + box-sizing: border-box; + font-size: 16px; + + background-color: var(--background); +} + +/* Fonts */ +@font-face { + font-family: "Ubuntu"; + src: url("/public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Ubuntu Bold"; + src: url("/public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf") format("truetype"); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: "Fira Code"; + src: url("/public/assets/fonts/Fira_code/FiraCode-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} diff --git a/public/js/global.js b/public/js/global.js new file mode 100644 index 0000000..86ce0e3 --- /dev/null +++ b/public/js/global.js @@ -0,0 +1,8 @@ +const htmlElement = document.documentElement; +const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)"); + +const currentTheme = + localStorage.getItem("theme") || + (prefersDarkScheme.matches ? "dark" : "light"); + +htmlElement.setAttribute("data-theme", currentTheme); diff --git a/src/helpers/sessions.ts b/src/helpers/sessions.ts index de686a2..05b5376 100644 --- a/src/helpers/sessions.ts +++ b/src/helpers/sessions.ts @@ -64,6 +64,38 @@ class SessionManager { return payload; } + public async updateSession( + request: Request, + payload: UserSession, + userAgent: string, + ): Promise { + const cookie: string | null = request.headers.get("Cookie"); + if (!cookie) throw new Error("No session found in request"); + + const token: string | null = + cookie.match(/session=([^;]+)/)?.[1] || null; + if (!token) throw new Error("Session token not found"); + + const userSessions: string[] = await redis + .getInstance() + .keys("session:*:" + token); + if (!userSessions.length) + throw new Error("Session not found or expired"); + + const sessionKey: string = userSessions[0]; + + await redis + .getInstance() + .set( + "JSON", + sessionKey, + { ...payload, userAgent }, + this.getExpirationInSeconds(), + ); + + return this.generateCookie(token); + } + public async verifySession(token: string): Promise { const userSessions: string[] = await redis .getInstance() diff --git a/src/routes/api/auth/email/verify/[code].ts b/src/routes/api/auth/email/verify/[code].ts new file mode 100644 index 0000000..0c9f627 --- /dev/null +++ b/src/routes/api/auth/email/verify/[code].ts @@ -0,0 +1,94 @@ +import { sql } from "bun"; + +import { isUUID } from "@/helpers/char"; +import { logger } from "@/helpers/logger"; +import { redis } from "@/helpers/redis"; +import { sessionManager } from "@/helpers/sessions"; + +const routeDef: RouteDef = { + method: "POST", + accepts: "*/*", + returns: "application/json", +}; + +async function handler(request: ExtendedRequest): Promise { + const { code } = request.params as { code: string }; + + if (!code) { + return Response.json( + { + success: false, + code: 400, + error: "Missing verification code", + }, + { status: 400 }, + ); + } + + if (!isUUID(code)) { + return Response.json( + { + success: false, + code: 400, + error: "Invalid verification code", + }, + { status: 400 }, + ); + } + + try { + const verificationData: unknown = await redis + .getInstance() + .get("JSON", `email:verify:${code}`); + + if (!verificationData) { + return Response.json( + { + success: false, + code: 400, + error: "Invalid verification code", + }, + { status: 400 }, + ); + } + + const { user_id: userId } = verificationData as { + user_id: string; + }; + + await redis.getInstance().delete("JSON", `email:verify:${code}`); + await sql` + UPDATE users + SET email_verified = true + WHERE id = ${userId};`; + } catch (error) { + logger.error(["Could not verify email:", error as Error]); + return Response.json( + { + success: false, + code: 500, + error: "Could not verify email", + }, + { status: 500 }, + ); + } + + if (request.session) { + await sessionManager.updateSession( + request, + { ...request.session, email_verified: true }, + request.headers.get("User-Agent") || "", + ); + } + + return Response.json( + { + success: true, + code: 200, + message: "Email has been verified", + }, + { status: 200 }, + ); +} + +export { handler, routeDef }; diff --git a/src/routes/api/auth/email/verify/request.ts b/src/routes/api/auth/email/verify/request.ts new file mode 100644 index 0000000..089301e --- /dev/null +++ b/src/routes/api/auth/email/verify/request.ts @@ -0,0 +1,84 @@ +import { randomUUIDv7, sql } from "bun"; + +import { logger } from "@/helpers/logger"; +import { redis } from "@/helpers/redis"; + +const routeDef: RouteDef = { + method: "GET", + accepts: "*/*", + returns: "application/json", +}; + +async function handler(request: ExtendedRequest): Promise { + if (!request.session) { + return Response.json( + { + success: false, + code: 403, + error: "Unauthorized", + }, + { status: 403 }, + ); + } + + try { + const [user] = await sql` + SELECT email_verified + FROM users + WHERE id = ${request.session.id} + LIMIT 1;`; + + if (!user) { + return Response.json( + { + success: false, + code: 404, + error: "Unknown user", + }, + { status: 404 }, + ); + } + + if (user.email_verified) { + return Response.json( + { + success: true, + code: 200, + message: "Email already verified", + }, + { status: 200 }, + ); + } + + const code: string = randomUUIDv7(); + await redis.getInstance().set( + "JSON", + `email:verify:${code}`, + { user_id: request.session.id }, + 60 * 60 * 2, // 2 hours + ); + + // TODO: Send email when email service is implemented + + return Response.json( + { + success: true, + code: 200, + message: "Verification email sent", + }, + { status: 200 }, + ); + } catch (error) { + logger.error(["Could not send email verification:", error as Error]); + return Response.json( + { + success: false, + code: 500, + error: "Could not send email verification", + }, + { status: 500 }, + ); + } +} + +export { handler, routeDef }; diff --git a/src/views/global.ejs b/src/views/global.ejs new file mode 100644 index 0000000..8815e98 --- /dev/null +++ b/src/views/global.ejs @@ -0,0 +1,31 @@ + + + + +<% if (title) { %> + <%= title %> +<% } %> + + + +<% if (typeof styles !== "undefined") { %> + <% styles.forEach(style => { %> + + <% }) %> +<% } %> + +<% if (typeof scripts !== "undefined") { %> + <% scripts.forEach(script => { %> + <% if (typeof script === "string") { %> + + <% } else if (Array.isArray(script)) { %> + <% if (script[1]) { %> + + <% } else { %> + + <% } %> + <% } %> + <% }) %> +<% } %> + + diff --git a/src/views/index.ejs b/src/views/index.ejs index 541eee1..dfc2c62 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -1,6 +1,7 @@ + <%- include("global", { styles: [], scripts: [] }) %> diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs new file mode 100644 index 0000000..e69de29