refactor(lst): refactor to monolithic completed

This commit is contained in:
2025-02-19 14:07:51 -06:00
parent b15f1d8ae8
commit dae00716ec
71 changed files with 225 additions and 624 deletions

View File

@@ -1,20 +0,0 @@
import {create} from "zustand";
type User = {
id: number;
username: string;
};
type SessionState = {
user: User | null;
token: string | null;
setSession: (user: SessionState["user"], token: string) => void;
clearSession: () => void;
};
export const useSessionStore = create<SessionState>((set) => ({
user: null,
token: null,
setSession: (user, token) => set({user, token}),
clearSession: () => set({user: null}),
}));

View File

@@ -1,14 +0,0 @@
import app from "./src/app";
const port = process.env.OCME_PORT || 5001; //bun will automatically grab this as well.
Bun.serve({
port,
fetch: app.fetch,
hostname: "0.0.0.0",
});
await Bun.build({
entrypoints: ["./index.js"],
outdir: "../../dist/ocme",
});
console.log(`ocme is running on port ${port}`);

View File

@@ -1,13 +0,0 @@
{
"name": "lstv2-ocme",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"dev": "bun --watch index.ts",
"build": "bun build ./index.ts"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

View File

@@ -1,27 +0,0 @@
import { funnyFunction } from "@shared/lib";
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { logger } from "hono/logger";
//import { expensesRoute } from "./routes/expenses";
const app = new Hono();
app.use("*", logger());
// running the hi function
funnyFunction();
app.get("/", (c) => {
return c.json({ success: true, message: "hello from bun on the ocmeserver" });
});
app.get("/api", (c) => {
return c.json({ success: true, message: "first api for ocme" });
});
//const apiRoute = app.basePath("/api").route("/expenses", expensesRoute);
app.get("*", serveStatic({ root: "./frontend/dist" }));
app.get("*", serveStatic({ path: "./frontend/dist/index.html" }));
export default app;
//export type ApiRoute = typeof apiRoute;

View File

@@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "index.ts"]
}

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,9 +1,12 @@
import LoginForm from "./components/LoginForm"; import LoginForm from "./components/LoginForm";
import {Button} from "./components/ui/button";
import {useSession} from "./lib/hooks/useSession"; import {useSession} from "./lib/hooks/useSession";
import {useLogout} from "./lib/hooks/useLogout";
import "./styles.css"; import "./styles.css";
function App() { function App() {
const {session, status} = useSession(); const {session, status} = useSession();
const logout = useLogout();
if (!session || status === "error") { if (!session || status === "error") {
return ( return (
@@ -15,8 +18,12 @@ function App() {
return ( return (
<> <>
<p>Logged in user: {session.user.username}</p> {/* <p>Logged in user: {session.user.username}</p> */}
<>Session: {JSON.stringify(session)}</>
<p>Status: {JSON.stringify(status)}</p> <p>Status: {JSON.stringify(status)}</p>
<p>
<Button onClick={() => logout()}>Logout</Button>
</p>
</> </>
); );
} }

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -11,7 +11,7 @@ const LoginForm = () => {
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
console.log("Form data", {username, password}); // console.log("Form data", {username, password});
try { try {
const response = await fetch("/api/auth/login", { const response = await fetch("/api/auth/login", {
method: "POST", method: "POST",
@@ -21,15 +21,19 @@ const LoginForm = () => {
body: JSON.stringify({username, password}), body: JSON.stringify({username, password}),
}); });
console.log("Response", response);
// if (!response.ok) { // if (!response.ok) {
// throw new Error("Invalid credentials"); // throw new Error("Invalid credentials");
// } // }
const data = await response.json(); const data = await response.json();
console.log("Response", data); console.log("Response", data.data);
setSession(data.user, data.token); // Store token in localStorage
localStorage.setItem("auth_token", data.data.token);
// Optionally store user info
// localStorage.setItem("user", JSON.stringify(user));
setSession(data.data.token);
// Refetch the session data to reflect the logged-in state // Refetch the session data to reflect the logged-in state
queryClient.invalidateQueries(["session"]); queryClient.invalidateQueries(["session"]);

View File

@@ -0,0 +1,12 @@
import {useSessionStore} from "../store/sessionStore";
export const useLogout = () => {
const clearSession = useSessionStore((state) => state.clearSession);
const logout = () => {
clearSession(); // Clears Zustand state
return "Logged out";
};
return logout;
};

View File

@@ -3,8 +3,19 @@ import {useSessionStore} from "../store/sessionStore";
import {useEffect} from "react"; import {useEffect} from "react";
const fetchSession = async () => { const fetchSession = async () => {
const res = await fetch("/api/auth/session", {credentials: "include"}); const token = localStorage.getItem("auth_token");
if (!token) {
throw new Error("No token found");
}
const res = await fetch("/api/auth/session", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
console.log(res);
if (!res.ok) { if (!res.ok) {
throw new Error("Session not found"); throw new Error("Session not found");
} }
@@ -13,10 +24,13 @@ const fetchSession = async () => {
}; };
export const useSession = () => { export const useSession = () => {
const {setSession, clearSession} = useSessionStore(); const {setSession, clearSession, token} = useSessionStore();
// Fetch session only if token is available
const {data, status, error} = useQuery({ const {data, status, error} = useQuery({
queryKey: ["session"], queryKey: ["session"],
queryFn: fetchSession, queryFn: fetchSession,
enabled: !!token, // Prevents query if token is null
staleTime: 5 * 60 * 1000, // 5 mins staleTime: 5 * 60 * 1000, // 5 mins
gcTime: 10 * 60 * 1000, // 10 mins gcTime: 10 * 60 * 1000, // 10 mins
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
@@ -24,12 +38,12 @@ export const useSession = () => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
setSession(data.user, data.token); setSession(data.token);
} }
if (error) { if (error) {
clearSession(); clearSession();
} }
}, [data, error]); }, [data, error]);
return {session: data, status, error}; return {session: data && token ? {user: data.user, token: data.token} : null, status, error};
}; };

View File

@@ -0,0 +1,36 @@
import {create} from "zustand";
// type User = {
// id: number;
// username: string;
// };
type SessionState = {
//user: User | null;
token: string | null;
setSession: (token: string) => void;
clearSession: () => void;
};
export const useSessionStore = create<SessionState>((set) => {
// Initialize from localStorage
//const storedUser = localStorage.getItem("user");
const storedToken = localStorage.getItem("auth_token");
return {
//user: storedUser ? JSON.parse(storedUser) : null,
token: storedToken || null,
setSession: (token) => {
localStorage.setItem("auth_token", token);
//localStorage.setItem("user", JSON.stringify(user));
set({token});
},
clearSession: () => {
localStorage.removeItem("auth_token");
//localStorage.removeItem("user");
set({token: null});
},
};
});

View File

@@ -17,7 +17,7 @@ export default defineConfig({
}, },
server: { server: {
proxy: { proxy: {
"/api": {target: "http://localhost:4000", changeOrigin: true}, "/api": {target: "http://localhost:3000", changeOrigin: true},
}, },
}, },
}); });

View File

@@ -1,9 +0,0 @@
{
"packages": ["apps/*", "packages/*"],
"version": "independent",
"command": {
"publish": {
"conventionalCommits": true
}
}
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -5,76 +5,46 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"dev": "dotenvx run -- bun run -r --parallel --aggregate-output dev", "dev": "dotenvx run -- bun run -r --parallel --aggregate-output dev",
"dev:all": "concurrently -n 'server,ocme,frontend' -c '#007755,#2f6da3,#c61cb8' 'cd apps/server && bun run dev' 'cd apps/ocme && bun run dev' 'cd apps/frontend && bun run dev'", "dev:all": "concurrently -n 'server,frontend' -c '#007755,#2f6da3' 'cd server && bun run dev' 'cd frontend && bun run dev'",
"dev:server": "bun --watch apps/server/index.ts", "dev:server": "bun --env-file .env --watch server/index.ts",
"dev:ocme": "bun --watch apps/ocme/index.ts", "dev:ocme": "bun --env-file .env --watch ocme/index.ts",
"dev:frontend": "cd apps/frontend && bunx --bun vite", "dev:frontend": "cd frontend && bunx --bun vite",
"build:server": "nx exec -- rimraf dist/server && cd apps/server && bun build index.js --outdir ../../dist/server", "build:server": "cd apps/server && bun build index.js --outdir ../../dist/server",
"build:ocme": "nx exec -- rimraf dist/ocme && cd apps/ocme && bun build index.js --outdir ../../dist/ocme", "build:ocme": "rimraf dist/ocme && cd apps/ocme && bun build index.js --outdir ../../dist/ocme",
"build:front": "nx exec -- rimraf dist/frontend && cd apps/frontend && bun run build", "build:front": "cd frontend && rimraf frontend/dist && bun run build",
"start": "cd dist/server && bun run ./index.js", "start": "bun --env-file .env run ./server/index.js",
"commit": "cz", "commit": "cz",
"clean": "rimraf dist/server" "clean": "rimraf dist/server",
"deploy": "standard-version --conventional-commits"
}, },
"workspaces": [
"apps/*",
"packages/*"
],
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@auth/core": "^0.37.4",
"@dotenvx/dotenvx": "^1.35.0", "@dotenvx/dotenvx": "^1.35.0",
"@hono/zod-openapi": "^0.18.4", "@hono/zod-openapi": "^0.18.4",
"@shared/lib": "*", "@scalar/hono-api-reference": "^0.5.174",
"@types/bun": "^1.2.2", "@types/bun": "^1.2.2",
"axios": "^1.7.9", "axios": "^1.7.9",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"compression": "^1.8.0", "compression": "^1.8.0",
"concurrently": "^9.1.2",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"dotenv-expand": "^12.0.1",
"hono": "^4.7.1", "hono": "^4.7.1",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^3.0.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lst-auth": "*",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"peerDependencies": {
"typescript": "^5.0.0"
},
"devDependencies": { "devDependencies": {
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"nx": "^20.4.4",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"typescript": "~5.7.3" "typescript": "~5.7.3",
"concurrently": "^9.1.2"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "./node_modules/cz-conventional-changelog" "path": "./node_modules/cz-conventional-changelog"
} }
},
"nx": {
"targets": {
"build": {
"cache": true,
"inputs": [
"{projectRoot}/**/*.ts",
"{projectRoot}/tsconfig.json",
{
"externalDependencies": [
"typescript"
]
}
],
"outputs": [
"{projectRoot}/dist"
]
}
}
} }
} }

View File

@@ -1,175 +0,0 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -1,15 +0,0 @@
# lst-auth
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -1,99 +0,0 @@
import {Hono, type MiddlewareHandler} from "hono";
import {basicAuth} from "hono/basic-auth";
import {sign, verify} from "jsonwebtoken";
const JWT_SECRET = "your-secret-key";
const EXPIRATION_TIME = "1h"; // Token expires in 1 minute
const REFRESH_THRESHOLD = 30; // Refresh token if it has less than 30 seconds left
const ACCESS_EXPIRATION = "1h"; // 1 minute for access tokens
const REFRESH_EXPIRATION = "7d"; // 7 days for refresh tokens
const fakeUsers = [
{id: 1, username: "admin", password: "password123"},
{id: 2, username: "user", password: "password123"},
{id: 3, username: "user2", password: "password123"},
];
// Hardcoded user credentials (for demonstration purposes)
const users = [{username: "admin", password: "password123"}];
// Middleware to check authentication
export const lstVerifyAuth = basicAuth({
verifyUser: (username, password) => {
const user = users.find((u) => u.username === username && u.password === password);
return !!user; // Return true if user exists, otherwise false
},
});
/**
* Authenticate a user and return a JWT.
*/
export function login(username: string, password: string): {token: string; user: {id: number; username: string}} {
const user = fakeUsers.find((u) => u.username === username && u.password === password);
if (!user) {
throw new Error("Invalid credentials");
}
// Create a JWT
const token = sign({userId: user?.id, username: user?.username}, JWT_SECRET, {expiresIn: EXPIRATION_TIME});
return {token, user: {id: user?.id, username: user.username}};
}
/**
* Verify a JWT and return the decoded payload.
*/
export function verifyToken(token: string): {userId: number} {
try {
const payload = verify(token, JWT_SECRET) as {userId: number};
return payload;
} catch (err) {
throw new Error("Invalid token");
}
}
export const authMiddleware: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json({error: "Unauthorized"}, 401);
}
const token = authHeader.split(" ")[1];
try {
const decoded = verify(token, JWT_SECRET, {ignoreExpiration: false}) as {userId: number; exp: number};
const currentTime = Math.floor(Date.now() / 1000); // Get current timestamp
const timeLeft = decoded.exp - currentTime;
// If the token has less than REFRESH_THRESHOLD seconds left, refresh it
let newToken = null;
if (timeLeft < REFRESH_THRESHOLD) {
newToken = sign({userId: decoded.userId}, JWT_SECRET, {expiresIn: EXPIRATION_TIME});
c.res.headers.set("Authorization", `Bearer ${newToken}`);
}
c.set("user", {id: decoded.userId});
await next();
// If a new token was generated, send it in response headers
if (newToken) {
console.log("token was refreshed");
c.res.headers.set("X-Refreshed-Token", newToken);
}
} catch (err) {
return c.json({error: "Invalid token"}, 401);
}
};
/**
* Logout (clear the token).
* This is a placeholder function since JWTs are stateless.
* In a real app, you might want to implement token blacklisting.
*/
export function logout(): {message: string} {
return {message: "Logout successful"};
}

View File

@@ -1,18 +0,0 @@
{
"name": "lst-auth",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@types/jsonwebtoken": "^9.0.8"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"cookie": "^1.0.2",
"hono": "^4.7.1",
"jsonwebtoken": "^9.0.2"
}
}

View File

@@ -1,45 +0,0 @@
import {Hono} from "hono";
import {setCookie, getCookie, deleteCookie} from "hono/cookie";
import {sign, verify} from "jsonwebtoken";
const JWT_SECRET = "your-secret-key";
const fakeUsers = [
{id: 1, username: "admin", password: "password123"},
{id: 2, username: "user", password: "password123"},
{id: 3, username: "user2", password: "password123"},
];
export const authLogin = new Hono().get("/", async (c) => {
// lets get the username and password to check everything
const {username, password} = await c.req.json();
let user = null;
// make sure we go a username and password
if (!username || !password) {
return c.json({error: "Username and password required"}, 400);
}
// check the user exist in our db
if (!fakeUsers.includes(username && password)) {
return c.json({error: "Invalid username or password"}, 400);
}
user = fakeUsers.find((u) => u.username === username && u.password === password);
// create the token
const token = sign({userId: user?.id}, JWT_SECRET, {expiresIn: "1h"});
setCookie(c, "auth_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 3600, //parseInt(process.env.JWT_EXPIRES_IN) * 60 * 1000 || 3600, // expires in 1 hour is not set in env
path: "/",
sameSite: "strict",
});
return c.json({
success: true,
message: "Login successful",
user: {id: user?.id, username: user?.username, token: token},
});
});

View File

@@ -1,10 +0,0 @@
import app from "../index";
const request = new Request("http://localhost/protected", {
headers: {
Authorization: "Basic " + Buffer.from("admin:password12").toString("base64"),
},
});
const response = await app.fetch(request);
console.log(await response.text()); // Should print "You are authenticated!"

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -1,18 +0,0 @@
{
"name": "@shared/lib",
"version": "0.0.1",
"main": "src/index.js",
"types": "src/index.d.ts",
"module": "src/index.js",
"type": "module",
"scripts": {
"build": "bun run tsc",
"clean": "rm -rf dist"
},
"dependencies": {
"bun-types": "latest"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

View File

@@ -1,5 +0,0 @@
export function funnyFunction() {
setInterval(() => {
console.log("hi");
}, 1000);
}

View File

@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true
},
"include": ["src"]
}

View File

@@ -1,18 +0,0 @@
{
"name": "@shared/ui",
"version": "0.0.1",
"main": "src/index.js",
"types": "src/index.d.ts",
"module": "src/index.js",
"type": "module",
"scripts": {
"build": "bun run tsc",
"clean": "rm -rf dist"
},
"dependencies": {
"bun-types": "latest"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

View File

@@ -6,9 +6,9 @@ Bun.serve({
hostname: "0.0.0.0", hostname: "0.0.0.0",
}); });
await Bun.build({ // await Bun.build({
entrypoints: ["./index.js"], // entrypoints: ["./index.js"],
outdir: "../../dist/server", // outdir: "../../dist/server",
}); // });
console.log(`server is running on port ${port}`); console.log(`server is running on port ${port}`);

View File

@@ -4,7 +4,7 @@
"description": "", "description": "",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "bun --watch ./index.ts", "dev": "bun --env-file ../.env --watch ./index.ts",
"build": "bun build ./index.ts" "build": "bun build ./index.ts"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,6 +1,6 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import {serveStatic} from "hono/bun"; import {serveStatic} from "hono/bun";
import {logger} from "hono/logger"; import {logger} from "hono/logger";
import {authMiddleware} from "lst-auth";
import {cors} from "hono/cors"; import {cors} from "hono/cors";
import {OpenAPIHono} from "@hono/zod-openapi"; import {OpenAPIHono} from "@hono/zod-openapi";
@@ -10,13 +10,14 @@ import scalar from "./route/scalar";
// services // services
import {ocmeService} from "./services/ocme/ocmeServer"; import {ocmeService} from "./services/ocme/ocmeServer";
console.log(process.env.JWT_SECRET);
const app = new OpenAPIHono(); const app = new OpenAPIHono();
app.use("*", logger()); app.use("*", logger());
app.use( app.use(
"*", "*",
cors({ cors({
origin: "http://localhost:5173", origin: `http://localhost:5173`,
allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"], allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
allowMethods: ["POST", "GET", "OPTIONS"], allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"], exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
@@ -47,12 +48,12 @@ routes.forEach((route) => {
//app.basePath("/api/auth").route("/login", login).route("/session", session).route("/register", register); //app.basePath("/api/auth").route("/login", login).route("/session", session).route("/register", register);
//auth stuff //auth stuff
app.get("/api/protected", authMiddleware, (c) => { // app.get("/api/protected", authMiddleware, (c) => {
return c.json({success: true, message: "is authenticated"}); // return c.json({success: true, message: "is authenticated"});
}); // });
app.get("*", serveStatic({root: "../frontend/dist"})); app.get("*", serveStatic({root: "./frontend/dist"}));
app.get("*", serveStatic({path: "../frontend/dist/index.html"})); app.get("*", serveStatic({path: "./frontend/dist/index.html"}));
export default app; export default app;

View File

@@ -0,0 +1,25 @@
import {sign, verify} from "jsonwebtoken";
/**
* Authenticate a user and return a JWT.
*/
const fakeUsers = [
{id: 1, username: "admin", password: "password123"},
{id: 2, username: "user", password: "password123"},
{id: 3, username: "user2", password: "password123"},
];
export function login(username: string, password: string): {token: string; user: {id: number; username: string}} {
const user = fakeUsers.find((u) => u.username === username && u.password === password);
if (!user) {
throw new Error("Invalid credentials");
}
// Create a JWT
const token = sign({userId: user?.id, username: user?.username}, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES,
});
return {token, user: {id: user?.id, username: user.username}};
}

View File

@@ -0,0 +1,8 @@
/**
* Logout (clear the token).
* This is a placeholder function since JWTs are stateless.
* In a real app, you might want to implement token blacklisting.
*/
export function logout(): {message: string} {
return {message: "Logout successful"};
}

View File

@@ -0,0 +1,13 @@
import {sign, verify} from "jsonwebtoken";
/**
* Verify a JWT and return the decoded payload.
*/
export function verifyToken(token: string): {userId: number} {
try {
const payload = verify(token, process.env.JWT_SECRET) as {userId: number};
return payload;
} catch (err) {
throw new Error("Invalid token");
}
}

View File

@@ -0,0 +1,41 @@
import {type MiddlewareHandler} from "hono";
import {sign, verify} from "jsonwebtoken";
export const authMiddleware: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json({error: "Unauthorized"}, 401);
}
const token = authHeader.split(" ")[1];
try {
const decoded = verify(token, process.env.JWT_SECRET, {ignoreExpiration: false}) as {
userId: number;
exp: number;
};
const currentTime = Math.floor(Date.now() / 1000); // Get current timestamp
const timeLeft = decoded.exp - currentTime;
// If the token has less than REFRESH_THRESHOLD seconds left, refresh it
let newToken = null;
if (timeLeft < parseInt(process.env.REFRESH_THRESHOLD)) {
newToken = sign({userId: decoded.userId}, process.env.JWT_SECRET, {expiresIn: process.env.EXPIRATION_TIME});
c.res.headers.set("Authorization", `Bearer ${newToken}`);
}
c.set("user", {id: decoded.userId});
await next();
// If a new token was generated, send it in response headers
if (newToken) {
console.log("token was refreshed");
c.res.headers.set("X-Refreshed-Token", newToken);
}
} catch (err) {
return c.json({error: "Invalid token"}, 401);
}
};

View File

@@ -1,5 +1,5 @@
import {z, createRoute, OpenAPIHono} from "@hono/zod-openapi"; import {z, createRoute, OpenAPIHono} from "@hono/zod-openapi";
import {login} from "lst-auth"; import {login} from "../controllers/login";
const app = new OpenAPIHono(); const app = new OpenAPIHono();
@@ -76,9 +76,9 @@ app.openapi(route, async (c) => {
const {token, user} = login(body.username, body.password); const {token, user} = login(body.username, body.password);
// Set the JWT as an HTTP-only cookie // Set the JWT as an HTTP-only cookie
c.header("Set-Cookie", `auth_token=${token}; HttpOnly; Path=/; SameSite=None; Max-Age=3600`); // c.header("Set-Cookie", `auth_token=${token}; HttpOnly; Path=/; SameSite=None; Max-Age=3600`);
return c.json({message: "Login successful", user}); return c.json({message: "Login successful", data: {token, user}});
} catch (err) { } catch (err) {
return c.json({message: err instanceof Error ? err.message : "Invalid credentials"}, 401); return c.json({message: err instanceof Error ? err.message : "Invalid credentials"}, 401);
} }

View File

@@ -25,22 +25,22 @@ const route = createRoute({
}); });
session.openapi(route, async (c) => { session.openapi(route, async (c) => {
const authHeader = c.req.header("Authorization"); const authHeader = c.req.header("Authorization");
const cookies = c.req.header("cookie");
if (authHeader?.includes("Basic")) { if (authHeader?.includes("Basic")) {
// //
return c.json({message: "You are a Basic user! Please login to get a token"}, 401); return c.json({message: "You are a Basic user! Please login to get a token"}, 401);
} }
if (!authHeader && !cookies) { if (!authHeader) {
return c.json({error: "Unauthorized"}, 401); return c.json({error: "Unauthorized"}, 401);
} }
const token = cookies?.split("auth_token=")[1].split(";")[0] || authHeader?.split("Bearer ")[1] || ""; const token = authHeader?.split("Bearer ")[1] || "";
try { try {
const payload = await verify(token, JWT_SECRET); const payload = await verify(token, JWT_SECRET);
return c.json({user: {id: payload.userId, username: payload.username}, token}); console.log(payload);
return c.json({token});
} catch (err) { } catch (err) {
return c.json({error: "Invalid or expired token"}, 401); return c.json({error: "Invalid or expired token"}, 401);
} }

View File

@@ -1,23 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "esnext", "module": "esnext",
"target": "esnext", "target": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"moduleDetection": "force", "moduleDetection": "force",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"composite": true, "composite": true,
"strict": true, "strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"removeComments": true, "removeComments": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
"types": [ "types": [
"bun-types" // add Bun global "bun-types" // add Bun global
] ]
} }
} }