Implement auth as source of truth on server and protect routes. Integrate with portal
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*",
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
"@blade-and-brawn/domain": "workspace:*",
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
|
"@elysia/jwt": "^1.4.2",
|
||||||
"@elysia/server-timing": "^1.4.1",
|
"@elysia/server-timing": "^1.4.1",
|
||||||
"@elysiajs/cors": "^1.4.2",
|
"@elysiajs/cors": "^1.4.2",
|
||||||
"elysia": "^1.4.29",
|
"elysia": "^1.4.29",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const env = {
|
|||||||
WEBFLOW_COLLECTION_ID: requireEnv("WEBFLOW_COLLECTION_ID"),
|
WEBFLOW_COLLECTION_ID: requireEnv("WEBFLOW_COLLECTION_ID"),
|
||||||
WEBFLOW_AUTH: requireEnv("WEBFLOW_AUTH"),
|
WEBFLOW_AUTH: requireEnv("WEBFLOW_AUTH"),
|
||||||
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
|
WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"),
|
||||||
|
AUTH_SECRET: requireEnv("AUTH_SECRET"),
|
||||||
|
ADMIN_PASSWORD: requireEnv("ADMIN_PASSWORD"),
|
||||||
NODE_ENV: optionEnv("NODE_ENV", "development"),
|
NODE_ENV: optionEnv("NODE_ENV", "development"),
|
||||||
LOG_LEVEL: optionEnv("LOG_LEVEL", "info"),
|
LOG_LEVEL: optionEnv("LOG_LEVEL", "info"),
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-5
@@ -24,6 +24,7 @@ import z from "zod";
|
|||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
import serverTiming from "@elysia/server-timing";
|
import serverTiming from "@elysia/server-timing";
|
||||||
|
import jwt from "@elysia/jwt";
|
||||||
|
|
||||||
const log = pino({
|
const log = pino({
|
||||||
level: env.LOG_LEVEL,
|
level: env.LOG_LEVEL,
|
||||||
@@ -75,7 +76,7 @@ export const app = new Elysia()
|
|||||||
"http://dev.portal.bladeandbrawn.com/"
|
"http://dev.portal.bladeandbrawn.com/"
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
)
|
).use(jwt({ name: "jwt", secret: env.AUTH_SECRET }))
|
||||||
|
|
||||||
.error({
|
.error({
|
||||||
PrintfulError,
|
PrintfulError,
|
||||||
@@ -116,8 +117,29 @@ export const app = new Elysia()
|
|||||||
.get("/", () => ({ status: "ok" }))
|
.get("/", () => ({ status: "ok" }))
|
||||||
.get("/health", () => ({ status: "ok" }))
|
.get("/health", () => ({ status: "ok" }))
|
||||||
|
|
||||||
// CALCULATOR
|
// AUTHENTICATION
|
||||||
|
.post("/auth/login", async ({ jwt, body, cookie: { auth } }) => {
|
||||||
|
if (body.password !== env.ADMIN_PASSWORD) throw status(401, "Invalid credentials");
|
||||||
|
|
||||||
|
auth.set({
|
||||||
|
value: await jwt.sign({ id: "admin", exp: "7d" }),
|
||||||
|
path: "/",
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
sameSite: "lax",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: env.NODE_ENV === "production",
|
||||||
|
domain: env.NODE_ENV === "production" ?
|
||||||
|
".bladeandbrawn.com" :
|
||||||
|
undefined
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
body: z.object({
|
||||||
|
password: z.coerce.string()
|
||||||
|
}),
|
||||||
|
cookie: t.Cookie({ auth: t.Optional(t.String()) })
|
||||||
|
})
|
||||||
|
|
||||||
|
// CALCULATOR
|
||||||
.post("/calculate", async ({ body }) => {
|
.post("/calculate", async ({ body }) => {
|
||||||
return {
|
return {
|
||||||
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
||||||
@@ -130,11 +152,12 @@ export const app = new Elysia()
|
|||||||
})
|
})
|
||||||
|
|
||||||
// COMMERCE
|
// COMMERCE
|
||||||
// TODO: protect with api key/token
|
|
||||||
.group("/products", (app) =>
|
.group("/products", (app) =>
|
||||||
app
|
app
|
||||||
.onBeforeHandle(({ }) => {
|
.guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) })
|
||||||
// authenticate cookie
|
.onBeforeHandle(async ({ jwt, cookie: { auth } }) => {
|
||||||
|
const token = auth.value && await jwt.verify(auth.value);
|
||||||
|
if (!token) throw status(401, "Unauthorized");
|
||||||
})
|
})
|
||||||
.group("/sync", (app) =>
|
.group("/sync", (app) =>
|
||||||
app
|
app
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@elysia/eden": "^1.4.10",
|
"@elysia/eden": "^1.4.10",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"daisyui": "^5.6.6",
|
"daisyui": "^5.6.6",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"tailwindcss": "^4.3.2"
|
"tailwindcss": "^4.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { redirect, type Handle } from '@sveltejs/kit';
|
import { redirect, type Handle } from '@sveltejs/kit';
|
||||||
import { createHmac } from 'node:crypto';
|
import { jwtVerify } from 'jose';
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const cookieHash = event.cookies.get("session");
|
if (event.url.pathname !== "/login") {
|
||||||
const computedHash = createHmac("sha256", env.PORTAL_SECRET)
|
const token = event.cookies.get("auth");
|
||||||
.update("authenticated")
|
const secret = new TextEncoder().encode(env.AUTH_SECRET);
|
||||||
.digest("hex");
|
const authenticated = token != null && await jwtVerify(token, secret)
|
||||||
if (event.url.pathname !== "/login" && cookieHash !== computedHash)
|
.then(() => true)
|
||||||
redirect(307, "/login");
|
.catch(() => false);
|
||||||
|
if (!authenticated) redirect(307, "/login");
|
||||||
|
}
|
||||||
|
|
||||||
const response = await resolve(event);
|
return await resolve(event);
|
||||||
return response;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,4 +2,8 @@ import { treaty } from "@elysia/eden"
|
|||||||
import type { API } from "@blade-and-brawn/api"
|
import type { API } from "@blade-and-brawn/api"
|
||||||
import { PUBLIC_API_URL } from "$env/static/public"
|
import { PUBLIC_API_URL } from "$env/static/public"
|
||||||
|
|
||||||
export const api = treaty<API>(PUBLIC_API_URL)
|
export const api = treaty<API>(PUBLIC_API_URL, {
|
||||||
|
fetch: {
|
||||||
|
credentials: "include",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { fail, redirect } from "@sveltejs/kit";
|
|
||||||
import type { Actions } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
import { createHmac } from "node:crypto";
|
|
||||||
|
|
||||||
export const actions: Actions = {
|
|
||||||
default: async ({ request, cookies }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
const password = data.get("password");
|
|
||||||
|
|
||||||
if (password !== env.PORTAL_PASSWORD)
|
|
||||||
return fail(401, { error: "Invalid password" });
|
|
||||||
|
|
||||||
const hash = createHmac("sha256", env.PORTAL_SECRET)
|
|
||||||
.update("authenticated")
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
cookies.set("session", hash, { path: "/", maxAge: 60 * 60 * 24 * 7 });
|
|
||||||
redirect(303, "/");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import type { PageProps } from "./$types";
|
import { goto } from "$app/navigation";
|
||||||
|
import { api } from "$lib/api";
|
||||||
|
|
||||||
let { form }: PageProps = $props();
|
let error = $state("");
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
async function handleSubmit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = event.currentTarget as HTMLFormElement;
|
||||||
|
const password = new FormData(form).get("password")?.toString() ?? "";
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
const { status } = await api.auth.login.post({ password });
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
error = "Invalid password";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await goto("/");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen flex items-center justify-center bg-base-200">
|
<div class="min-h-screen flex items-center justify-center bg-base-200">
|
||||||
<div class="card bg-base-100 w-full max-w-sm shadow-xl">
|
<div class="card bg-base-100 w-full max-w-sm shadow-xl">
|
||||||
<div class="card-body gap-4">
|
<div class="card-body gap-4">
|
||||||
{#if form?.error}
|
{#if error}
|
||||||
<div role="alert" class="alert alert-error">
|
<div role="alert" class="alert alert-error">
|
||||||
<span>{form.error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form method="POST" class="flex flex-col gap-4">
|
<form onsubmit={handleSubmit} class="flex flex-col gap-4">
|
||||||
<label class="floating-label">
|
<label class="floating-label">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -26,7 +46,11 @@
|
|||||||
<span>Password</span>
|
<span>Password</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary w-full">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*",
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
"@blade-and-brawn/domain": "workspace:*",
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
|
"@elysia/jwt": "^1.4.2",
|
||||||
"@elysia/server-timing": "^1.4.1",
|
"@elysia/server-timing": "^1.4.1",
|
||||||
"@elysiajs/cors": "^1.4.2",
|
"@elysiajs/cors": "^1.4.2",
|
||||||
"elysia": "^1.4.29",
|
"elysia": "^1.4.29",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"@elysia/eden": "^1.4.10",
|
"@elysia/eden": "^1.4.10",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"daisyui": "^5.6.6",
|
"daisyui": "^5.6.6",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -89,6 +91,8 @@
|
|||||||
|
|
||||||
"@elysia/eden": ["@elysia/eden@1.4.10", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-vcZXQcW6wZj6rhTxaiTkuCbVuS/yAJQ9jqCM6b83a5hu99F5Aj3HOyWXL17iod2Rz+pJpYsQGX0XaeoCsMpw2g=="],
|
"@elysia/eden": ["@elysia/eden@1.4.10", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-vcZXQcW6wZj6rhTxaiTkuCbVuS/yAJQ9jqCM6b83a5hu99F5Aj3HOyWXL17iod2Rz+pJpYsQGX0XaeoCsMpw2g=="],
|
||||||
|
|
||||||
|
"@elysia/jwt": ["@elysia/jwt@1.4.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.27" } }, "sha512-KhLe6XxI+NHoYZiHqPhWg8SU0v+pegsxXsZSa/CXic/BrjlFbO26uTERtOQNlnByp/IGUB4qpLF6kQhvfYvGyg=="],
|
||||||
|
|
||||||
"@elysia/server-timing": ["@elysia/server-timing@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-+NDGhslxN0HZuyT8o4dt+hgib9bzsMuvxTE5bPrDhFFRl1duCNosseW3wE76pmwle8+m9+1aOeZ28mZAv51ggQ=="],
|
"@elysia/server-timing": ["@elysia/server-timing@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-+NDGhslxN0HZuyT8o4dt+hgib9bzsMuvxTE5bPrDhFFRl1duCNosseW3wE76pmwle8+m9+1aOeZ28mZAv51ggQ=="],
|
||||||
|
|
||||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="],
|
"@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="],
|
||||||
@@ -339,6 +343,8 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
||||||
|
|
||||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user