From 043305c04c7259ef8ea52abf2936164931efb734 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 07:07:12 +0200 Subject: [PATCH] u --- .env.example | 1 + src/api/routes/auth/login.js | 24 +++-- src/api/routes/auth/selectedCompany.js | 40 ++++++++ src/api/routes/company.js | 130 +++++++++++++++++++++++++ src/config/env.js | 1 + src/db/company.js | 21 ++++ src/db/googleAuth.js | 12 ++- 7 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 src/api/routes/auth/selectedCompany.js create mode 100644 src/api/routes/company.js create mode 100644 src/db/company.js diff --git a/.env.example b/.env.example index 8988463..2449151 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ CORS_ORIGINS=http://localhost:5173 # Auth — Google OAuth client ID and JWT signing secret GOOGLE_CLIENT_ID= JWT_SECRET=fallback_secret_please_change_in_production +IMPLICIT_CREATE=false # Database — MSSQL (auth allowlist and JTL-Wawi article data) MSSQL_SERVER= diff --git a/src/api/routes/auth/login.js b/src/api/routes/auth/login.js index 51d0bd0..b13ef38 100644 --- a/src/api/routes/auth/login.js +++ b/src/api/routes/auth/login.js @@ -13,7 +13,7 @@ const googleClient = new OAuth2Client(env.googleClientId); export function register(app, basePath = '') { /** * @route POST /auth/login - * @description Verifies a Google ID token, checks allowlist, and sets an HTTP-only `token` cookie. + * @description Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), and sets an HTTP-only `token` cookie. * @body {string} idToken - Google ID token from Google Identity Services * @returns {Object} 200 - `{ message, user }` with `{ id, email, selectedCompanyId, picture? }` in `user` * @returns {Object} 400 - Missing token or Google client not configured @@ -32,10 +32,15 @@ export function register(app, basePath = '') { return res.status(400).json({ error: 'GOOGLE_CLIENT_ID is not configured' }); } - const ticket = await googleClient.verifyIdToken({ - idToken, - audience: env.googleClientId, - }); + let ticket; + try { + ticket = await googleClient.verifyIdToken({ + idToken, + audience: env.googleClientId, + }); + } catch { + return res.status(401).json({ error: 'Invalid Google token' }); + } const payload = ticket.getPayload(); const email = payload?.email?.toLowerCase(); @@ -43,7 +48,11 @@ export function register(app, basePath = '') { return res.status(401).json({ error: 'Invalid Google account' }); } - const allowed = await db.getAllowedEmail(email); + let allowed = await db.getAllowedEmail(email); + if (!allowed && env.implicitCreate) { + await db.addAllowedEmail(email); + allowed = await db.getAllowedEmail(email); + } if (!allowed) { console.warn(`Login rejected: email not authorized (${email})`); return res.status(403).json({ error: 'Email is not authorized' }); @@ -66,7 +75,8 @@ export function register(app, basePath = '') { res.status(200).json({ message: 'Login successful', user }); } catch (error) { - res.status(401).json({ error: 'Invalid Google token' }); + console.error('Login failed:', error); + res.status(500).json({ error: 'Login failed' }); } }); } diff --git a/src/api/routes/auth/selectedCompany.js b/src/api/routes/auth/selectedCompany.js new file mode 100644 index 0000000..5823018 --- /dev/null +++ b/src/api/routes/auth/selectedCompany.js @@ -0,0 +1,40 @@ +import * as db from '../../../db/googleAuth.js'; +import { clearCachedUser } from '../../middleware/auth.js'; + +/** + * Registers the selected-company route (requires auth middleware). + * @param {import('express').Application} app - Express app + * @param {string} [basePath=''] - Optional URL prefix for all routes in this module + */ +export function register(app, basePath = '') { + /** + * @route PUT /auth/selected-company + * @description Sets the authenticated user's selected company. + * @body {number} companyId + * @returns {Object} 200 - `{ user: { id, email, selectedCompanyId, picture? } }` + */ + app.put(`${basePath}/auth/selected-company`, async (req, res) => { + const companyId = Number(req.body?.companyId); + if (!Number.isInteger(companyId) || companyId <= 0) { + return res.status(400).json({ error: 'companyId is required' }); + } + + try { + await db.setSelectedCompany(req.user.id, companyId); + clearCachedUser(req.user.email); + + const updated = await db.getAllowedEmail(req.user.email); + const user = { + id: updated.id, + email: updated.email, + selectedCompanyId: updated.selectedCompanyId, + ...(req.user.picture && { picture: req.user.picture }), + }; + + res.status(200).json({ user }); + } catch (error) { + console.error('Set selected company failed:', error); + res.status(500).json({ error: 'Failed to set selected company' }); + } + }); +} diff --git a/src/api/routes/company.js b/src/api/routes/company.js new file mode 100644 index 0000000..a0a9fc0 --- /dev/null +++ b/src/api/routes/company.js @@ -0,0 +1,130 @@ +import * as db from '../../db/company.js'; +import * as googleAuth from '../../db/googleAuth.js'; +import { clearCachedUser } from '../middleware/auth.js'; + +function userPayload(req, authRow) { + return { + id: authRow.id, + email: authRow.email, + selectedCompanyId: authRow.selectedCompanyId, + ...(req.user.picture && { picture: req.user.picture }), + }; +} + +function parseCompanyBody(body) { + const { iso639, name, taxrate, reducedtaxrate } = body ?? {}; + if (!iso639 || !name || taxrate == null || reducedtaxrate == null) { + return { error: 'iso639, name, taxrate, and reducedtaxrate are required' }; + } + return { iso639, name, taxrate, reducedtaxrate }; +} + +function parseCompanyId(id) { + const companyId = Number(id); + if (!Number.isInteger(companyId) || companyId <= 0) { + return { error: 'Invalid company id' }; + } + return { companyId }; +} + +function companyErrorStatus(error) { + if (error.message === 'Company not found') return 404; + if (error.message === 'Company name not available') return 409; + return 500; +} + +/** + * Registers company CRUD routes (requires auth middleware). + * @param {import('express').Application} app - Express app + * @param {string} [basePath=''] - Optional URL prefix for all routes in this module + */ +export function register(app, basePath = '') { + /** + * @route GET /company + * @description Lists companies for the authenticated user. + * @returns {Object} 200 - `{ companies }` + */ + app.get(`${basePath}/company`, async (req, res) => { + try { + const companies = await db.listCompanies(req.user.id); + res.status(200).json({ companies }); + } catch (error) { + console.error('List companies failed:', error); + res.status(500).json({ error: 'Failed to list companies' }); + } + }); + + /** + * @route POST /company + * @description Creates a company. + * @body {string} iso639 + * @body {string} name + * @body {number} taxrate + * @body {number} reducedtaxrate + * @returns {Object} 201 - `{ company }` + */ + app.post(`${basePath}/company`, async (req, res) => { + const body = parseCompanyBody(req.body); + if (body.error) { + return res.status(400).json({ error: body.error }); + } + + try { + const company = await db.createCompany(req.user.id, body); + await googleAuth.setSelectedCompany(req.user.id, company.id); + clearCachedUser(req.user.email); + const authRow = await googleAuth.getAllowedEmail(req.user.email); + res.status(201).json({ company, user: userPayload(req, authRow) }); + } catch (error) { + const status = companyErrorStatus(error); + if (status === 500) console.error('Create company failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @route PUT /company/:id + * @description Updates a company. + * @returns {Object} 200 - `{ company }` + */ + app.put(`${basePath}/company/:id`, async (req, res) => { + const parsedId = parseCompanyId(req.params.id); + if (parsedId.error) { + return res.status(400).json({ error: parsedId.error }); + } + + const body = parseCompanyBody(req.body); + if (body.error) { + return res.status(400).json({ error: body.error }); + } + + try { + const company = await db.updateCompany(parsedId.companyId, body); + res.status(200).json({ company }); + } catch (error) { + const status = companyErrorStatus(error); + if (status === 500) console.error('Update company failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @route DELETE /company/:id + * @description Deletes a company. + * @returns {Object} 204 - No content + */ + app.delete(`${basePath}/company/:id`, async (req, res) => { + const parsedId = parseCompanyId(req.params.id); + if (parsedId.error) { + return res.status(400).json({ error: parsedId.error }); + } + + try { + await db.deleteCompany(parsedId.companyId); + res.sendStatus(204); + } catch (error) { + console.error('Delete company failed:', error); + res.status(500).json({ error: 'Failed to delete company' }); + } + }); +} diff --git a/src/config/env.js b/src/config/env.js index 60646db..ba922b9 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -12,6 +12,7 @@ export default { .map((s) => s.trim()) .filter(Boolean), googleClientId: process.env.GOOGLE_CLIENT_ID, + implicitCreate: process.env.IMPLICIT_CREATE === 'true', jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production', mssql: { server: process.env.MSSQL_SERVER, diff --git a/src/db/company.js b/src/db/company.js new file mode 100644 index 0000000..4b1b350 --- /dev/null +++ b/src/db/company.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function listCompanies(authId) { + const store = await getStore(); + return store.company.list(authId); +} + +export async function createCompany(authId, data) { + const store = await getStore(); + return store.company.create({ ...data, authId }); +} + +export async function updateCompany(id, data) { + const store = await getStore(); + return store.company.update(id, data); +} + +export async function deleteCompany(companyId) { + const store = await getStore(); + await store.company.delete(companyId); +} diff --git a/src/db/googleAuth.js b/src/db/googleAuth.js index 0137f01..37d6f98 100644 --- a/src/db/googleAuth.js +++ b/src/db/googleAuth.js @@ -2,7 +2,7 @@ import { getStore } from './store.js'; export async function listAllowedEmails() { const store = await getStore(); - return store.google_auth.list(); + return store.google_auth.listAllowedEmails(); } export async function getAllowedEmail(email) { @@ -19,9 +19,15 @@ export async function addAllowedEmail(email) { } export async function removeAllowedEmail(email) { - const row = await getAllowedEmail(email); + const normalized = email.trim().toLowerCase(); + const row = await getAllowedEmail(normalized); if (!row) return false; const store = await getStore(); - await store.google_auth.del(row.id); + await store.google_auth.removeAllowedEmail(normalized); return true; } + +export async function setSelectedCompany(authId, companyId) { + const store = await getStore(); + await store.google_auth.setSelectedCompany(authId, companyId); +}