import path from 'path' import express, { NextFunction, Request, Response } from 'express' import Knex from 'knex' import jose from 'jose' import * as z from 'zod' // this `data` folder should be mounted in the docker-compose in production, or mkdired in development const dbPath = path.join(process.env.DB_PREFIX ?? '../data/', '/database.db') console.log(dbPath) const knex = Knex({ client: 'better-sqlite3', connection: { filename: dbPath, }, }); const app = express(); app.use(express.json()); // app.use(express.urlencoded()); const authenticatedRouter = express.Router() const JWKS = jose.createRemoteJWKSet(new URL('https://rauthy.juustodiilerit.fi/auth/v1/oidc/certs')) authenticatedRouter.use(async (req, res, next) => { const authHeader = req.headers['authorization'] const jwt = authHeader && authHeader.split(' ')[1] if (!jwt) { return res.status(401).send('Unauthorized') } try { const { payload } = await jose.jwtVerify(jwt, JWKS, { issuer: 'https://rauthy.juustodiilerit.fi/auth/v1', audience: process.env.VITE_RAUTHY_CLIENT_ID, }) if (!payload.sub) { return res.status(401).send('Unauthorized') } req.userId = payload.sub; return next(); } catch (error) { console.error(error) } return res.status(401).send('Unauthorized') }); const validateBody = (schema: z.Schema) => async (req: Request, res: Response, next: NextFunction) => { try { const validatedData = await schema.parseAsync(req.body); req.body = validatedData; next(); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).send('Bad request'); } else { return res.status(500).send('Internal server error'); } } }; // ============================================== // COFFEES // ============================================== const CoffeePostBody = z.object({ name: z.string(), roastLevel: z.int(), notes: z.union([z.string(), z.undefined()]) }) type CoffeePostBody = z.infer authenticatedRouter.post('/coffee', validateBody(CoffeePostBody), async (req: Request, res: Response) => { const coffee: CoffeePostBody = req.body try { await knex('coffee').insert({ id: knex.fn.uuid(), user_id: req.userId, name: coffee.name, roast_level: coffee.roastLevel, notes: coffee.notes, }) } catch (error) { console.error(error) return res.status(500).send('Internal server error'); } return res.status(200).send('Great success') }); authenticatedRouter.get('/coffee', async (req: Request, res: Response) => { const coffees = await knex('coffee').select('*') .where({user_id: req.userId}) return res.status(200).json(coffees) }); // ============================================== // GRINDERS // ============================================== const GrinderPostBody = z.object({ name: z.string(), notes: z.union([z.string(), z.undefined()]) }) type GrinderPostBody = z.infer authenticatedRouter.post('/grinder', validateBody(GrinderPostBody), async (req: Request, res: Response) => { const grinder: GrinderPostBody = req.body try { await knex('grinder').insert({ id: knex.fn.uuid(), user_id: req.userId, name: grinder.name, notes: grinder.notes, }) } catch (error) { console.error(error) return res.status(500).send('Internal server error'); } return res.status(200).send('Great success') }); authenticatedRouter.get('/grinder', async (req: Request, res: Response) => { const grinders = await knex('grinder').select('*') .where({user_id: req.userId}) return res.status(200).json(grinders) }); // ============================================== // BREWERS // ============================================== const BrewerPostBody = z.object({ name: z.string(), notes: z.union([z.string(), z.undefined()]) }) type BrewerPostBody = z.infer authenticatedRouter.post('/brewer', validateBody(BrewerPostBody), async (req: Request, res: Response) => { const brewer: BrewerPostBody = req.body try { await knex('brewer').insert({ id: knex.fn.uuid(), user_id: req.userId, name: brewer.name, notes: brewer.notes, }) } catch (error) { console.error(error) return res.status(500).send('Internal server error'); } return res.status(200).send('Great success') }); authenticatedRouter.get('/brewer', async (req: Request, res: Response) => { const brewers = await knex('brewer').select('*') .where({user_id: req.userId}) return res.status(200).json(brewers) }); // ============================================== // METHODS // ============================================== const MethodPostBody = z.object({ coffeeId: z.uuid(), coffeeAmountG: z.int(), grinderId: z.uuid(), grindSetting: z.string(), brewerId: z.uuid(), waterAmountMl: z.int(), waterTemperatureC: z.int(), bloomingTimeSec: z.int(), brewingTimeSec: z.int(), notes: z.union([z.string(), z.undefined()]) }) type MethodPostBody = z.infer authenticatedRouter.post('/method', validateBody(MethodPostBody), async (req: Request, res: Response) => { const method: MethodPostBody = req.body try { await knex('method').insert({ id: knex.fn.uuid(), user_id: req.userId, coffee_id: method.coffeeId, coffee_amount_g: method.coffeeAmountG, grinder_id: method.grinderId, grind_setting: method.grindSetting, brewer_id: method.brewerId, water_amount_ml: method.waterAmountMl, water_temperature_c: method.waterTemperatureC, blooming_time_sec: method.bloomingTimeSec, brewing_time_sec: method.brewingTimeSec, notes: method.notes, }) } catch (error) { console.error(error) return res.status(500).send('Internal server error'); } return res.status(200).send('Great success') }); authenticatedRouter.get('/method', async (req: Request, res: Response) => { const methods = await knex('method').select('*') .where({user_id: req.userId}) return res.status(200).json(methods) }); app.use('/api', authenticatedRouter) const fallbackRouter = express.Router() // These 2 relative paths require the server to be run from the 'backend' folder fallbackRouter.use(express.static('../frontend/dist')); fallbackRouter.get('/{*fallback}', (_req: Request, res: Response) => { res.sendFile('./frontend/dist/index.html', { root: '..'}); }); app.use(fallbackRouter) const PORT = process.env.PORT || 3000; async function startServer() { try { app.listen(PORT, () => { console.log( `Server is running on port ${PORT} at http://localhost:${PORT}` ); }); } catch (error) { console.error('Error starting server:', error); } } startServer();