feat(lst): added in basic authentication

This commit is contained in:
2025-02-17 20:01:04 -06:00
parent ca27264bb0
commit 5f7a3dd182
25 changed files with 810 additions and 154 deletions

View File

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

View File

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

View File

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

9
apiDocs/lstV2/bruno.json Normal file
View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "lstV2",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -13,6 +13,7 @@
"@antfu/ni": "^23.3.1", "@antfu/ni": "^23.3.1",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.66.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
@@ -21,7 +22,8 @@
"shadcn": "^2.4.0-canary.6", "shadcn": "^2.4.0-canary.6",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6", "tailwindcss": "^4.0.6",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",

View File

@@ -1,13 +1,24 @@
import LoginForm from "./components/LoginForm";
import {useSession} from "./lib/hooks/useSession";
import "./styles.css"; import "./styles.css";
import { funnyFunction } from "@shared/lib";
function App() { function App() {
funnyFunction(); const {session, status} = useSession();
return (
<> if (!session || status === "error") {
<p>lstv2</p> return (
</> <p>
); no session please login <LoginForm />
</p>
);
}
return (
<>
<p>Logged in user: {session.user.username}</p>
<p>Status: {JSON.stringify(status)}</p>
</>
);
} }
export default App; export default App;

View File

@@ -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 (
<form onSubmit={handleLogin}>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <p>{error}</p>}
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;

View File

@@ -0,0 +1,7 @@
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
const queryClient = new QueryClient();
export const SessionProvider = ({children}: {children: React.ReactNode}) => {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};

View File

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

View File

@@ -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<SessionState>((set) => ({
user: null,
token: null,
setSession: (user, token) => set({user, token}),
clearSession: () => set({user: null}),
}));

View File

@@ -1,10 +1,13 @@
import { StrictMode } from "react"; import {StrictMode} from "react";
import { createRoot } from "react-dom/client"; import {createRoot} from "react-dom/client";
import "./styles.css"; import "./styles.css";
import App from "./App.tsx"; import App from "./App.tsx";
import {SessionProvider} from "./components/providers/Providers.tsx";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <SessionProvider>
</StrictMode> <App />
</SessionProvider>
</StrictMode>
); );

View File

@@ -1,23 +1,23 @@
import { defineConfig } from "vite"; import {defineConfig} from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import path from "path"; import path from "path";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
build: { // build: {
outDir: path.resolve(__dirname, "../../dist/frontend/dist"), // outDir: path.resolve(__dirname, "../../dist/frontend/dist"),
emptyOutDir: true, // emptyOutDir: true,
}, // },
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
},
}, },
}, server: {
server: { proxy: {
proxy: { "/api": {target: "http://localhost:4000", changeOrigin: true},
"/api": { target: "http://localhost:4000", changeOrigin: true }, },
}, },
},
}); });

View File

@@ -1,14 +1,14 @@
import app from "./src/app"; import app from "./src/app";
const port = process.env.SERVER_PORT || 4000; const port = process.env.SERVER_PORT || 4000;
Bun.serve({ Bun.serve({
port, port,
fetch: app.fetch, fetch: app.fetch,
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

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

View File

@@ -1,60 +1,49 @@
import { Hono } from "hono"; import {Hono} from "hono";
import { serveStatic } from "hono/bun"; import {serveStatic} from "hono/bun";
import { logger } from "hono/logger"; import {logger} from "hono/logger";
import { ocmeService } from "./services/ocmeServer"; import {ocmeService} from "./services/ocmeServer";
import { AuthConfig } from "@auth/core/types"; import {authMiddleware} from "lst-auth";
import { authHandler, initAuthConfig, verifyAuth } from "@hono/auth-js"; import {cors} from "hono/cors";
import Credentials from "@auth/core/providers/credentials";
import { authConfig } from "./auth/auth";
//import { expensesRoute } from "./routes/expenses"; //import { expensesRoute } from "./routes/expenses";
import login from "./route/auth/login";
import session from "./route/auth/session";
const app = new Hono(); const app = new Hono();
app.use("*", logger()); 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 // as we dont want to change ocme again well use a proxy to this
app.all("/ocme/*", async (c) => { app.all("/ocme/*", async (c) => {
return ocmeService(c); return ocmeService(c);
}); });
app.basePath("/api/auth").route("/login", login).route("/session", session);
//auth stuff //auth stuff
app.use("*", initAuthConfig(authConfig)); app.get("/api/protected", authMiddleware, (c) => {
return c.json({success: true, message: "is authenticated"});
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/test", (c) => { 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 authRoute = app.basePath("/api/auth").route("*", )
//const apiRoute = app.basePath("/api").route("/expenses", expensesRoute); //const apiRoute = app.basePath("/api").route("/expenses", expensesRoute);
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,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;

View File

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

View File

@@ -1,74 +1,75 @@
{ {
"name": "lstv2", "name": "lstv2",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"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,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:server": "bun --watch apps/server/index.ts",
"dev:ocme": "bun --watch apps/ocme/index.ts", "dev:ocme": "bun --watch apps/ocme/index.ts",
"dev:frontend": "cd apps/frontend && bunx --bun vite", "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: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: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", "build:front": "nx exec -- rimraf dist/frontend && cd apps/frontend && bun run build",
"start": "cd dist/server && bun run ./index.js", "start": "cd dist/server && bun run ./index.js",
"commit": "cz", "commit": "cz",
"clean": "rimraf dist/server" "clean": "rimraf dist/server"
}, },
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@auth/core": "^0.37.4", "@auth/core": "^0.37.4",
"@dotenvx/dotenvx": "^1.35.0", "@dotenvx/dotenvx": "^1.35.0",
"@hono/auth-js": "^1.0.15", "@hono/zod-openapi": "^0.18.4",
"@hono/zod-openapi": "^0.18.4", "@shared/lib": "*",
"@shared/lib": "*", "@types/bun": "^1.2.2",
"@types/bun": "^1.2.2", "concurrently": "^9.1.2",
"concurrently": "^9.1.2", "cookie": "^1.0.2",
"cors": "^2.8.5", "dotenv": "^16.4.7",
"dotenv": "^16.4.7", "hono": "^4.7.1",
"hono": "^4.7.1", "http-proxy-middleware": "^3.0.3",
"http-proxy-middleware": "^3.0.3", "jsonwebtoken": "^9.0.2",
"zod": "^3.24.2" "lst-auth": "*",
}, "zod": "^3.24.2"
"peerDependencies": { },
"typescript": "^5.0.0" "peerDependencies": {
}, "typescript": "^5.0.0"
"devDependencies": { },
"cz-conventional-changelog": "^3.3.0", "devDependencies": {
"nx": "^20.4.4", "cz-conventional-changelog": "^3.3.0",
"rimraf": "^6.0.1", "nx": "^20.4.4",
"standard-version": "^9.5.0", "rimraf": "^6.0.1",
"typescript": "~5.7.3" "standard-version": "^9.5.0",
}, "typescript": "~5.7.3"
"config": { },
"commitizen": { "config": {
"path": "./node_modules/cz-conventional-changelog" "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"
]
}
}
}
} }

175
packages/lst-auth/.gitignore vendored Normal file
View File

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

View File

@@ -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.

View File

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

View File

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

View File

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

View File

@@ -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!"

View File

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