229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
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();
|