Added basic Auth-Service features

This commit is contained in:
Giuseppe Raffa
2026-05-21 10:56:25 +02:00
commit 318ea3555f
17 changed files with 2144 additions and 0 deletions

25
auth/src/core/jwt.js Normal file
View File

@@ -0,0 +1,25 @@
import jwt from 'jsonwebtoken';
/* The secret used for signing and verifying tokens */
const secret = process.env.AUTH_TOKEN;
const cookieName = process.env.COOKIE_NAME;
//expires in 1 year
const ttl_seconds = 60 * 60 * 24 * 365;
export function sign(payload) {
return jwt.sign(payload, secret, { expiresIn: ttl_seconds });
}
export async function verify(token) {
return jwt.verify(token, secret);
}
export const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV,
sameSite: 'lax',
path: '/',
maxAge: ttl_seconds * 1000,
};

View File

@@ -0,0 +1,9 @@
import bcrypt from "bcrypt";
export async function hash(password) {
return bcrypt.hash(password, 12);
}
export async function verify(password, hash) {
return bcrypt.compare(password, hash);
}

13
auth/src/data/db.js Normal file
View File

@@ -0,0 +1,13 @@
import pg from 'pg';
export const pool = new pg.Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: 'data', //default database for user and sessions. Not other database needed,
max: 10,
idleTimeoutMillis: 30_000
});
export const query = (text, params) => pool.query(text, params);

9
auth/src/data/redis.js Normal file
View File

@@ -0,0 +1,9 @@
import Redis from 'ioredis';
const client = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
});
export { client as redis };

35
auth/src/index.js Normal file
View File

@@ -0,0 +1,35 @@
import express from 'express';
import cookieParser from 'cookie-parser';
import { authRouter } from './routes/auth.js';
const app = express();
app.use(express.json());
app.use(cookieParser());
app.get('/health', (req, res) => {
res.send({
status: 'ok',
service: 'auth',
version: {
'major': process.env.V_MAJOR,
'minor': process.env.V_MINOR,
'patch': process.env.V_PATCH,
},
timestamp: new Date().toISOString(),
});
});
// Public web pages
// app.use('/login', authRouter);
// app.use('/profile', profileRouter);
// app.use('/profile/sessions', sessionRouter);
app.use('/api', authRouter);
// app.use('/api/users', usersRouter);
// app.use('/api/sessions', sessionRouter);
//
app.listen('3000', '0.0.0.0', () => {
console.log('Auth started');
})

View File

@@ -0,0 +1,35 @@
import { verify } from "../core/jwt";
import { query } from "../data/db";
import { redis } from '../data/redis';
const cookieName = process.env.COOKIE_NAME;
export async function requireUserAuth(req, res, next) {
const token = req.cookies?.[cookieName];
if (!token) return res.status(401).json({ message: 'No token' });
let payload;
try {
payload = await verify(token);
} catch (error) {
return res.status(401).json({ message: 'Invalid token' });
}
// Session
const { rows } = await query(
'select id, user_id, expires_at from sessions where id = $1',
[payload.sessionId]
);
if (!rows[0]) return res.status(401).json({ message: 'Invalid session' });
await query('update sessions set last_activity = now() where id = $1', [payload.sessionId]).catch(() => { });
redis.set(`onlineuser:${payload.sub}`, '1', 'EX', 60).catch(() => { });
req.user = {
id: payload.sub,
name: payload.sessionId,
}
next();
}

12
auth/src/pages/login.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Ciao</title>
</head>
<body>
<h1>Ciao</h1>
<form>
</form>
</body>
</html>

70
auth/src/routes/auth.js Normal file
View File

@@ -0,0 +1,70 @@
import { Router } from "express";
import { query } from "../data/db.js";
import { hash, verify } from "../core/securitycore.js";
import { sign, cookieOptions } from "../core/jwt.js";
import crypto from "crypto";
import {redis} from "../data/redis.js";
const router = Router();
const cookieName = process.env.COOKIE_NAME
router.post('/signup', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required' });
}
const exist = await query('select 1 from users where username = $1', [username]);
if (exist.rows.length > 0) {
return res.status(400).json({ message: 'Username already exists' });
}
const passwordHash = await hash(password);
//TODO: Remove username sending from resposne in prod
const { rows } = await query('insert into users (username, password_hash) values ($1, $2) returning id, username', [username, passwordHash]);
res.status(201).json(rows[0]);
});
router.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required' });
}
const { rows } = await query('select id, username, password_hash from users where username = $1', [username]);
const user = rows[0];
const ok = user ? await verify(password, user.password_hash) : false;
if (!ok) {
return res.status(400).json({ message: 'Invalid username or password' });
}
const ua = req.headers['user-agent'];
const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ?? req.socket.remoteAddress;
const sessionToken = crypto.randomUUID();
const ttlDays = 360;
const { rows: srow } = await query('insert into sessions (user_id, session_token, device_name, device_os, ip_address, expires_at) values ($1, $2, $3, $4, $5, $6) returning id', [user.id, sessionToken, ua.slice(0, 100), '', ip?.slice(0, 45), ttlDays]);
const session_id = srow[0].id;
const jtoken = sign({ sub: user.id, session_id });
await redis.set(`usersession:${session_id}`, user.id, 'EX', ttlDays * 24 * 3600);
await redis.set(`online:${user.id}`, '1', 'EX', 60);
res.cookie(cookieName, jtoken, cookieOptions);
res.json({
ok: true,
user: user.id,
session: session_id
});
})
router.post('/logout', async (req, res) => {
await query('delete from sessions where id = $1', [req.user.sessionID]);
await redis.del(`online:${req.user.id}`);
res.clearCookie(cookieName);
res.json({ loggedOut: true });
})
export { router as authRouter };