kinda start index page, add email verify and request route, add session updating
This commit is contained in:
parent
b24b1484cb
commit
cc4ebfbdd0
12 changed files with 287 additions and 0 deletions
|
@ -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> {
|
||||||
|
|
BIN
public/assets/fonts/Fira_code/FiraCode-Regular.ttf
Normal file
BIN
public/assets/fonts/Fira_code/FiraCode-Regular.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf
Normal file
BIN
public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf
Normal file
BIN
public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf
Normal file
Binary file not shown.
36
public/css/global.css
Normal file
36
public/css/global.css
Normal 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
8
public/js/global.js
Normal 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);
|
|
@ -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()
|
||||||
|
|
94
src/routes/api/auth/email/verify/[code].ts
Normal file
94
src/routes/api/auth/email/verify/[code].ts
Normal 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 };
|
84
src/routes/api/auth/email/verify/request.ts
Normal file
84
src/routes/api/auth/email/verify/request.ts
Normal 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
31
src/views/global.ejs
Normal 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>
|
|
@ -1,6 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<%- include("global", { styles: [], scripts: [] }) %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
0
src/views/partials/header.ejs
Normal file
0
src/views/partials/header.ejs
Normal file
Loading…
Add table
Reference in a new issue