kahvi.juustodiilerit.fi/backend/app.ts

230 lines
6.7 KiB
TypeScript
Raw Permalink Normal View History

2026-01-05 23:39:34 +02:00
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<typeof CoffeePostBody>
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<typeof GrinderPostBody>
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<typeof BrewerPostBody>
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<typeof MethodPostBody>
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();