From 5f7a3dd182f87f964813faaa169206842c3b6772 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Mon, 17 Feb 2025 20:01:04 -0600 Subject: [PATCH] feat(lst): added in basic authentication --- apiDocs/lstV2/Auth/Login.bru | 18 ++ apiDocs/lstV2/Auth/Test Protected.bru | 15 ++ apiDocs/lstV2/Auth/session.bru | 19 ++ apiDocs/lstV2/bruno.json | 9 + apps/frontend/package.json | 4 +- apps/frontend/src/App.tsx | 25 ++- apps/frontend/src/components/LoginForm.tsx | 72 +++++++ .../src/components/providers/Providers.tsx | 7 + apps/frontend/src/lib/hooks/useSession.ts | 35 ++++ apps/frontend/src/lib/store/sessionStore.ts | 20 ++ apps/frontend/src/main.tsx | 13 +- apps/frontend/vite.config.ts | 28 +-- apps/server/index.ts | 14 +- apps/server/package.json | 22 +-- apps/server/src/app.ts | 65 +++---- apps/server/src/route/auth/login.ts | 28 +++ apps/server/src/route/auth/session.ts | 38 ++++ package.json | 143 +++++++------- packages/lst-auth/.gitignore | 175 ++++++++++++++++++ packages/lst-auth/README.md | 15 ++ packages/lst-auth/index.ts | 99 ++++++++++ packages/lst-auth/package.json | 18 ++ packages/lst-auth/routes/login.ts | 45 +++++ packages/lst-auth/test/test.ts | 10 + packages/lst-auth/tsconfig.json | 27 +++ 25 files changed, 810 insertions(+), 154 deletions(-) create mode 100644 apiDocs/lstV2/Auth/Login.bru create mode 100644 apiDocs/lstV2/Auth/Test Protected.bru create mode 100644 apiDocs/lstV2/Auth/session.bru create mode 100644 apiDocs/lstV2/bruno.json create mode 100644 apps/frontend/src/components/LoginForm.tsx create mode 100644 apps/frontend/src/components/providers/Providers.tsx create mode 100644 apps/frontend/src/lib/hooks/useSession.ts create mode 100644 apps/frontend/src/lib/store/sessionStore.ts create mode 100644 apps/server/src/route/auth/login.ts create mode 100644 apps/server/src/route/auth/session.ts create mode 100644 packages/lst-auth/.gitignore create mode 100644 packages/lst-auth/README.md create mode 100644 packages/lst-auth/index.ts create mode 100644 packages/lst-auth/package.json create mode 100644 packages/lst-auth/routes/login.ts create mode 100644 packages/lst-auth/test/test.ts create mode 100644 packages/lst-auth/tsconfig.json diff --git a/apiDocs/lstV2/Auth/Login.bru b/apiDocs/lstV2/Auth/Login.bru new file mode 100644 index 0000000..361529d --- /dev/null +++ b/apiDocs/lstV2/Auth/Login.bru @@ -0,0 +1,18 @@ +meta { + name: Login + type: http + seq: 2 +} + +post { + url: http://localhost:4000/api/auth/login + body: json + auth: none +} + +body:json { + { + "username":"admin", + "password": "password123" + } +} diff --git a/apiDocs/lstV2/Auth/Test Protected.bru b/apiDocs/lstV2/Auth/Test Protected.bru new file mode 100644 index 0000000..ae5127a --- /dev/null +++ b/apiDocs/lstV2/Auth/Test Protected.bru @@ -0,0 +1,15 @@ +meta { + name: Test Protected + type: http + seq: 1 +} + +get { + url: http://localhost:4000/api/protected + body: none + auth: bearer +} + +auth:bearer { + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTczOTgyMTgyNiwiZXhwIjoxNzM5ODI1NDI2fQ.N5pn4PaPDhM_AXAOTGwd-_TOP9UOU1wK0vmICVE7vEc +} diff --git a/apiDocs/lstV2/Auth/session.bru b/apiDocs/lstV2/Auth/session.bru new file mode 100644 index 0000000..4c4fedc --- /dev/null +++ b/apiDocs/lstV2/Auth/session.bru @@ -0,0 +1,19 @@ +meta { + name: session + type: http + seq: 3 +} + +get { + url: http://localhost:4000/api/auth/session + body: none + auth: bearer +} + +headers { + : +} + +auth:bearer { + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTczOTgyNzE1NiwiZXhwIjoxNzM5ODI3MjE2fQ.VE9URMrRI_5_wc8CEmj-VEVeP01LL412vKhNwWRRHRM +} diff --git a/apiDocs/lstV2/bruno.json b/apiDocs/lstV2/bruno.json new file mode 100644 index 0000000..9f4e474 --- /dev/null +++ b/apiDocs/lstV2/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "lstV2", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 217bc2c..41fc443 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -13,6 +13,7 @@ "@antfu/ni": "^23.3.1", "@radix-ui/react-slot": "^1.1.2", "@tailwindcss/vite": "^4.0.6", + "@tanstack/react-query": "^5.66.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", @@ -21,7 +22,8 @@ "shadcn": "^2.4.0-canary.6", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.6", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index bf6ea61..cac92da 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,13 +1,24 @@ +import LoginForm from "./components/LoginForm"; +import {useSession} from "./lib/hooks/useSession"; import "./styles.css"; -import { funnyFunction } from "@shared/lib"; function App() { - funnyFunction(); - return ( - <> -

lstv2

- - ); + const {session, status} = useSession(); + + if (!session || status === "error") { + return ( +

+ no session please login +

+ ); + } + + return ( + <> +

Logged in user: {session.user.username}

+

Status: {JSON.stringify(status)}

+ + ); } export default App; diff --git a/apps/frontend/src/components/LoginForm.tsx b/apps/frontend/src/components/LoginForm.tsx new file mode 100644 index 0000000..bb37bb1 --- /dev/null +++ b/apps/frontend/src/components/LoginForm.tsx @@ -0,0 +1,72 @@ +import {useState} from "react"; +import {useSessionStore} from "../lib/store/sessionStore"; +import {useQueryClient} from "@tanstack/react-query"; + +const LoginForm = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const {setSession} = useSessionStore(); + const queryClient = useQueryClient(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + console.log("Form data", {username, password}); + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({username, password}), + }); + + console.log("Response", response); + + // if (!response.ok) { + // throw new Error("Invalid credentials"); + // } + + const data = await response.json(); + console.log("Response", data); + setSession(data.user, data.token); + + // Refetch the session data to reflect the logged-in state + queryClient.invalidateQueries(["session"]); + + setUsername(""); + setPassword(""); + } catch (err) { + setError("Invalid credentials"); + } + }; + + return ( +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {error &&

{error}

} + +
+ ); +}; + +export default LoginForm; diff --git a/apps/frontend/src/components/providers/Providers.tsx b/apps/frontend/src/components/providers/Providers.tsx new file mode 100644 index 0000000..a5856f0 --- /dev/null +++ b/apps/frontend/src/components/providers/Providers.tsx @@ -0,0 +1,7 @@ +import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +export const SessionProvider = ({children}: {children: React.ReactNode}) => { + return {children}; +}; diff --git a/apps/frontend/src/lib/hooks/useSession.ts b/apps/frontend/src/lib/hooks/useSession.ts new file mode 100644 index 0000000..cc16ce5 --- /dev/null +++ b/apps/frontend/src/lib/hooks/useSession.ts @@ -0,0 +1,35 @@ +import {useQuery} from "@tanstack/react-query"; +import {useSessionStore} from "../store/sessionStore"; +import {useEffect} from "react"; + +const fetchSession = async () => { + const res = await fetch("/api/auth/session", {credentials: "include"}); + + if (!res.ok) { + throw new Error("Session not found"); + } + + return res.json(); +}; + +export const useSession = () => { + const {setSession, clearSession} = useSessionStore(); + const {data, status, error} = useQuery({ + queryKey: ["session"], + queryFn: fetchSession, + staleTime: 5 * 60 * 1000, // 5 mins + gcTime: 10 * 60 * 1000, // 10 mins + refetchOnWindowFocus: true, + }); + + useEffect(() => { + if (data) { + setSession(data.user, data.token); + } + if (error) { + clearSession(); + } + }, [data, error]); + + return {session: data, status, error}; +}; diff --git a/apps/frontend/src/lib/store/sessionStore.ts b/apps/frontend/src/lib/store/sessionStore.ts new file mode 100644 index 0000000..b4fe2ed --- /dev/null +++ b/apps/frontend/src/lib/store/sessionStore.ts @@ -0,0 +1,20 @@ +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((set) => ({ + user: null, + token: null, + setSession: (user, token) => set({user, token}), + clearSession: () => set({user: null}), +})); diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 275f045..2bf9b3f 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -1,10 +1,13 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; +import {StrictMode} from "react"; +import {createRoot} from "react-dom/client"; import "./styles.css"; import App from "./App.tsx"; +import {SessionProvider} from "./components/providers/Providers.tsx"; createRoot(document.getElementById("root")!).render( - - - + + + + + ); diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 2cfedfc..8ac556f 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,23 +1,23 @@ -import { defineConfig } from "vite"; +import {defineConfig} from "vite"; import react from "@vitejs/plugin-react-swc"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss()], - build: { - outDir: path.resolve(__dirname, "../../dist/frontend/dist"), - emptyOutDir: true, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), + plugins: [react(), tailwindcss()], + // build: { + // outDir: path.resolve(__dirname, "../../dist/frontend/dist"), + // emptyOutDir: true, + // }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, }, - }, - server: { - proxy: { - "/api": { target: "http://localhost:4000", changeOrigin: true }, + server: { + proxy: { + "/api": {target: "http://localhost:4000", changeOrigin: true}, + }, }, - }, }); diff --git a/apps/server/index.ts b/apps/server/index.ts index c854ac3..0161532 100644 --- a/apps/server/index.ts +++ b/apps/server/index.ts @@ -1,14 +1,14 @@ import app from "./src/app"; const port = process.env.SERVER_PORT || 4000; Bun.serve({ - port, - fetch: app.fetch, - hostname: "0.0.0.0", + port, + fetch: app.fetch, + hostname: "0.0.0.0", }); -// await Bun.build({ -// entrypoints: ["./index.js"], -// outdir: "../../dist/server", -// }); +await Bun.build({ + entrypoints: ["./index.js"], + outdir: "../../dist/server", +}); console.log(`server is running on port ${port}`); diff --git a/apps/server/package.json b/apps/server/package.json index e57ff76..fdc0bc4 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,13 +1,13 @@ { - "name": "lstv2-server", - "version": "1.0.0", - "description": "", - "private": true, - "scripts": { - "dev": "bun --watch index.ts", - "build": "bun build ./index.ts" - }, - "devDependencies": { - "typescript": "^5.7.3" - } + "name": "lstv2-server", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "dev": "bun --watch ./index.ts", + "build": "bun build ./index.ts" + }, + "devDependencies": { + "typescript": "^5.7.3" + } } diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 292f489..c536934 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,60 +1,49 @@ -import { Hono } from "hono"; -import { serveStatic } from "hono/bun"; -import { logger } from "hono/logger"; -import { ocmeService } from "./services/ocmeServer"; -import { AuthConfig } from "@auth/core/types"; -import { authHandler, initAuthConfig, verifyAuth } from "@hono/auth-js"; -import Credentials from "@auth/core/providers/credentials"; -import { authConfig } from "./auth/auth"; +import {Hono} from "hono"; +import {serveStatic} from "hono/bun"; +import {logger} from "hono/logger"; +import {ocmeService} from "./services/ocmeServer"; +import {authMiddleware} from "lst-auth"; +import {cors} from "hono/cors"; + //import { expensesRoute } from "./routes/expenses"; +import login from "./route/auth/login"; +import session from "./route/auth/session"; const app = new Hono(); app.use("*", logger()); +app.use( + "*", + cors({ + origin: "http://localhost:5173", + allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length", "X-Kuma-Revision"], + maxAge: 600, + credentials: true, + }) +); // as we dont want to change ocme again well use a proxy to this app.all("/ocme/*", async (c) => { - return ocmeService(c); + return ocmeService(c); }); +app.basePath("/api/auth").route("/login", login).route("/session", session); //auth stuff -app.use("*", initAuthConfig(authConfig)); - -app.use("/api/auth/*", async (c, next) => { - const response = await authHandler()(c, next); - - if (c.req.path === "/api/auth/callback/credentials") { - const setCookieHeader = response.headers.get("Set-Cookie"); - - if (setCookieHeader) { - const tokenMatch = setCookieHeader.match(/authjs\.session-token=([^;]+)/); - const jwt = tokenMatch ? tokenMatch[1] : null; - - if (jwt) { - console.log("Extracted JWT:", jwt); - return c.json({ token: jwt }); - } - } - } - - return response; -}); - -app.get("/api/protected", verifyAuth(), (c) => { - const auth = c.get("authUser"); - return c.json(auth); +app.get("/api/protected", authMiddleware, (c) => { + return c.json({success: true, message: "is authenticated"}); }); app.get("/api/test", (c) => { - const auth = c.get("authUser"); - return c.json({ success: true, message: "hello from bun" }); + return c.json({success: true, message: "hello from bun"}); }); // const authRoute = app.basePath("/api/auth").route("*", ) //const apiRoute = app.basePath("/api").route("/expenses", expensesRoute); -app.get("*", serveStatic({ root: "../frontend/dist" })); -app.get("*", serveStatic({ path: "../frontend/dist/index.html" })); +app.get("*", serveStatic({root: "../frontend/dist"})); +app.get("*", serveStatic({path: "../frontend/dist/index.html"})); export default app; diff --git a/apps/server/src/route/auth/login.ts b/apps/server/src/route/auth/login.ts new file mode 100644 index 0000000..d7a2e51 --- /dev/null +++ b/apps/server/src/route/auth/login.ts @@ -0,0 +1,28 @@ +import {Hono} from "hono"; +import {login} from "lst-auth"; + +const router = new Hono().post("/", async (c) => { + let body = {username: "", password: "", error: ""}; + try { + body = await c.req.json(); + } catch (error) { + return c.json({success: false, message: "Username and password required"}, 400); + } + + if (!body?.username || !body?.password) { + return c.json({message: "Username and password required"}, 400); + } + try { + const {token, user} = login(body?.username, body?.password); + + // Set the JWT as an HTTP-only cookie + c.header("Set-Cookie", `auth_token=${token}; HttpOnly; Secure; Path=/; SameSite=None; Max-Age=3600`); + + return c.json({message: "Login successful", user}); + } catch (err) { + // console.log(err); + return c.json({message: err}, 401); + } +}); + +export default router; diff --git a/apps/server/src/route/auth/session.ts b/apps/server/src/route/auth/session.ts new file mode 100644 index 0000000..e51b673 --- /dev/null +++ b/apps/server/src/route/auth/session.ts @@ -0,0 +1,38 @@ +import {Hono} from "hono"; +import {verify} from "hono/jwt"; + +const app = new Hono(); + +const JWT_SECRET = "your-secret-key"; + +app.get("/", async (c) => { + const authHeader = c.req.header("Authorization"); + const cookies = c.req.header("cookie"); + + if (authHeader?.includes("Basic")) { + // + return c.json({message: "You are a Basic user! Please login to get a token"}, 401); + } + + if (!authHeader && !cookies) { + return c.json({error: "Unauthorized"}, 401); + } + // if (!cookies || !cookies.startsWith("Bearer ")) { + // return c.json({error: "Unauthorized"}, 401); + // } + + // if (!authHeader || !authHeader.startsWith("Bearer ")) { + // return c.json({error: "Unauthorized"}, 401); + // } + + const token = cookies?.split("auth_token=")[1].split(";")[0] || authHeader?.split("Bearer ")[1] || ""; + + try { + const payload = await verify(token, JWT_SECRET); + return c.json({user: {id: payload.userId, username: payload.username}, token}); + } catch (err) { + return c.json({error: "Invalid or expired token"}, 401); + } +}); + +export default app; diff --git a/package.json b/package.json index d6eea2a..66e24b0 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,75 @@ { - "name": "lstv2", - "version": "1.0.0", - "description": "", - "main": "index.ts", - "scripts": { - "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:server": "bun --watch apps/server/index.ts", - "dev:ocme": "bun --watch apps/ocme/index.ts", - "dev:frontend": "cd apps/frontend && bunx --bun vite", - "build:server": "nx exec -- rimraf dist/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:front": "nx exec -- rimraf dist/frontend && cd apps/frontend && bun run build", - "start": "cd dist/server && bun run ./index.js", - "commit": "cz", - "clean": "rimraf dist/server" - }, - "workspaces": [ - "apps/*", - "packages/*" - ], - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@auth/core": "^0.37.4", - "@dotenvx/dotenvx": "^1.35.0", - "@hono/auth-js": "^1.0.15", - "@hono/zod-openapi": "^0.18.4", - "@shared/lib": "*", - "@types/bun": "^1.2.2", - "concurrently": "^9.1.2", - "cors": "^2.8.5", - "dotenv": "^16.4.7", - "hono": "^4.7.1", - "http-proxy-middleware": "^3.0.3", - "zod": "^3.24.2" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "devDependencies": { - "cz-conventional-changelog": "^3.3.0", - "nx": "^20.4.4", - "rimraf": "^6.0.1", - "standard-version": "^9.5.0", - "typescript": "~5.7.3" - }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" + "name": "lstv2", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "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:server": "bun --watch apps/server/index.ts", + "dev:ocme": "bun --watch apps/ocme/index.ts", + "dev:frontend": "cd apps/frontend && bunx --bun vite", + "build:server": "nx exec -- rimraf dist/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:front": "nx exec -- rimraf dist/frontend && cd apps/frontend && bun run build", + "start": "cd dist/server && bun run ./index.js", + "commit": "cz", + "clean": "rimraf dist/server" + }, + "workspaces": [ + "apps/*", + "packages/*" + ], + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@auth/core": "^0.37.4", + "@dotenvx/dotenvx": "^1.35.0", + "@hono/zod-openapi": "^0.18.4", + "@shared/lib": "*", + "@types/bun": "^1.2.2", + "concurrently": "^9.1.2", + "cookie": "^1.0.2", + "dotenv": "^16.4.7", + "hono": "^4.7.1", + "http-proxy-middleware": "^3.0.3", + "jsonwebtoken": "^9.0.2", + "lst-auth": "*", + "zod": "^3.24.2" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "devDependencies": { + "cz-conventional-changelog": "^3.3.0", + "nx": "^20.4.4", + "rimraf": "^6.0.1", + "standard-version": "^9.5.0", + "typescript": "~5.7.3" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "nx": { + "targets": { + "build": { + "cache": true, + "inputs": [ + "{projectRoot}/**/*.ts", + "{projectRoot}/tsconfig.json", + { + "externalDependencies": [ + "typescript" + ] + } + ], + "outputs": [ + "{projectRoot}/dist" + ] + } + } } - }, - "nx": { - "targets": { - "build": { - "cache": true, - "inputs": [ - "{projectRoot}/**/*.ts", - "{projectRoot}/tsconfig.json", - { - "externalDependencies": [ - "typescript" - ] - } - ], - "outputs": [ - "{projectRoot}/dist" - ] - } - } - } } diff --git a/packages/lst-auth/.gitignore b/packages/lst-auth/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/packages/lst-auth/.gitignore @@ -0,0 +1,175 @@ +# 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 diff --git a/packages/lst-auth/README.md b/packages/lst-auth/README.md new file mode 100644 index 0000000..6b584ea --- /dev/null +++ b/packages/lst-auth/README.md @@ -0,0 +1,15 @@ +# 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. diff --git a/packages/lst-auth/index.ts b/packages/lst-auth/index.ts new file mode 100644 index 0000000..e45a35e --- /dev/null +++ b/packages/lst-auth/index.ts @@ -0,0 +1,99 @@ +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"}; +} diff --git a/packages/lst-auth/package.json b/packages/lst-auth/package.json new file mode 100644 index 0000000..856146d --- /dev/null +++ b/packages/lst-auth/package.json @@ -0,0 +1,18 @@ +{ + "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" + } +} diff --git a/packages/lst-auth/routes/login.ts b/packages/lst-auth/routes/login.ts new file mode 100644 index 0000000..fb1edb4 --- /dev/null +++ b/packages/lst-auth/routes/login.ts @@ -0,0 +1,45 @@ +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}, + }); +}); diff --git a/packages/lst-auth/test/test.ts b/packages/lst-auth/test/test.ts new file mode 100644 index 0000000..4766932 --- /dev/null +++ b/packages/lst-auth/test/test.ts @@ -0,0 +1,10 @@ +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!" diff --git a/packages/lst-auth/tsconfig.json b/packages/lst-auth/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/packages/lst-auth/tsconfig.json @@ -0,0 +1,27 @@ +{ + "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 + } +}