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

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