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 (
+
+ );
+};
+
+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
+ }
+}