kinda start index page, add email verify and request route, add session updating

This commit is contained in:
creations 2025-03-14 22:27:05 -04:00
parent b24b1484cb
commit cc4ebfbdd0
Signed by: creations
GPG key ID: 8F553AA4320FC711
12 changed files with 287 additions and 0 deletions

View file

@ -14,6 +14,7 @@ const defaultSettings: Setting[] = [
{ key: "date_format", value: "yyyy-MM-dd_HH-mm-ss" }, { key: "date_format", value: "yyyy-MM-dd_HH-mm-ss" },
{ key: "random_name_length", value: "8" }, { key: "random_name_length", value: "8" },
{ key: "enable_thumbnails", value: "true" }, { key: "enable_thumbnails", value: "true" },
{ key: "index_page_stats", value: "true" },
]; ];
export async function createTable(reservation?: ReservedSQL): Promise<void> { export async function createTable(reservation?: ReservedSQL): Promise<void> {

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
public/css/global.css Normal file
View file

@ -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;
}

8
public/js/global.js Normal file
View file

@ -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);

View file

@ -64,6 +64,38 @@ class SessionManager {
return payload; return payload;
} }
public async updateSession(
request: Request,
payload: UserSession,
userAgent: string,
): Promise<string> {
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<UserSession> { public async verifySession(token: string): Promise<UserSession> {
const userSessions: string[] = await redis const userSessions: string[] = await redis
.getInstance() .getInstance()

View file

@ -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<Response> {
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 };

View file

@ -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<Response> {
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 };

31
src/views/global.ejs Normal file
View file

@ -0,0 +1,31 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<% if (title) { %>
<title><%= title %></title>
<% } %>
<link rel="stylesheet" href="/public/css/global.css">
<% if (typeof styles !== "undefined") { %>
<% styles.forEach(style => { %>
<link rel="stylesheet" href="/public/css/<%= style %>.css">
<% }) %>
<% } %>
<% if (typeof scripts !== "undefined") { %>
<% scripts.forEach(script => { %>
<% if (typeof script === "string") { %>
<script src="/public/js/<%= script %>.js" defer></script>
<% } else if (Array.isArray(script)) { %>
<% if (script[1]) { %>
<script src="/public/js/<%= script[0] %>.js" defer></script>
<% } else { %>
<script src="/public/js/<%= script[0] %>.js"></script>
<% } %>
<% } %>
<% }) %>
<% } %>
<script src="/public/js/global.js"></script>

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<%- include("global", { styles: [], scripts: [] }) %>
</head> </head>
<body> <body>

View file