From e097d154de8b8d4d6c8c30d534b8f8b36ebf9870 Mon Sep 17 00:00:00 2001 From: michivonah Date: Sat, 4 Oct 2025 18:37:16 +0200 Subject: [PATCH 1/2] add user creation in db & validation --- README.md | 5 +++ api/src/db/schema.ts | 14 +++++--- api/src/index.ts | 7 ++-- api/src/lib/user-auth.ts | 70 ++++++++++++++++++++++++++++++++++++++++ api/src/types/error.ts | 13 ++++++++ 5 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 api/src/lib/user-auth.ts create mode 100644 api/src/types/error.ts diff --git a/README.md b/README.md index 5d0e7b9..ded9be1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ apply changes npx drizzle-kit push --config=drizzle-dev.config.ts ``` +export sql statements instead of running migration +```bash +npx drizzle-kit export --config=drizzle-dev.config.ts +``` + ## SQLite / D1 Delete view ```sql diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index bc56e07..cdb7942 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, text, sqliteTable, sqliteView } from "drizzle-orm/sqlite-core"; +import { check, integer, text, sqliteTable, sqliteView } from "drizzle-orm/sqlite-core"; import { eq, sql } from "drizzle-orm"; // Tables @@ -48,9 +48,15 @@ export const themepark = sqliteTable('themepark', { export const user = sqliteTable('user', { id: integer().primaryKey({ autoIncrement: true }), - username: text().notNull(), - isActive: integer({ mode: 'boolean' }).default(false) -}) + mail: text().notNull().unique(), + isActive: integer({ mode: 'boolean' }).notNull().default(false), + createdAt: integer().notNull(), + lastActive: integer().notNull() +}, +(table) => [ + check("mail_validation", sql`${table.mail} LIKE '%@%'`) +] +) // Views export const subscribedThemeparks = sqliteView('subscribed_themeparks').as((qb) => diff --git a/api/src/index.ts b/api/src/index.ts index 767b467..6547f1b 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' import { authHandler, initAuthConfig, verifyAuth } from '@hono/auth-js' +import { getUser } from './lib/user-auth' import GitHub from '@auth/core/providers/github' import notification from './routes/notification' import logbook from './routes/logbook' @@ -24,9 +25,9 @@ app.use('/auth/*', authHandler()) app.use('/*', verifyAuth()) // example endpoint -app.get('/protected', (c) => { - const auth = c.get('authUser') - return c.json(auth) +app.get('/protected', async (c) => { + const user = await getUser(c); + return c.json(user); }) // define routes & export app diff --git a/api/src/lib/user-auth.ts b/api/src/lib/user-auth.ts new file mode 100644 index 0000000..b5ebec8 --- /dev/null +++ b/api/src/lib/user-auth.ts @@ -0,0 +1,70 @@ +import { getDbContext } from "../db/client"; +import { Context } from "hono"; +import { UserSelect } from "../types/user"; +import { user } from "../db/schema"; +import { like } from "drizzle-orm"; +import { DrizzleD1Database } from "drizzle-orm/d1"; +import { MissingMailError, UserInactiveError } from "../types/error"; + +/** + * Returns the details of a user from the given context + * @param c Request context + * @returns Object of the user details as type UserSelect + */ +export async function getUser(c: Context): Promise{ + const db = getDbContext(c); + const auth = c.get('authUser'); + if(!auth.session.user || !auth.session.user.email) throw new MissingMailError(); + + const currentUser: UserSelect = c.get('currentUser'); + if(currentUser) return currentUser; + + const mail = auth.session.user.email; + + let userData: UserSelect[]; + try{ + userData = await db.selectDistinct().from(user).limit(1).where(like(user.mail, mail)); + + } + catch(e){ + throw new Error(`Database error: ${e}`); + } + + const dbResult = userData[0] ?? await createUser(db, mail); + if(!dbResult.isActive) throw new UserInactiveError(); + + c.set('currentUser', dbResult); + return dbResult; +} + +/** + * Creates a new user in the DB from the given context + * @param c Request context + * @returns The created user as Object of type UserSelect + */ +async function createUser(db: DrizzleD1Database, userMail: string): Promise{ + let userData: UserSelect[]; + + try{ + userData = await db.insert(user).values( + { + mail: userMail, + isActive: true, + createdAt: now(), + lastActive: now() + } + ).returning(); + + } + catch(e){ + throw new Error(`Database error: ${e}`); + } + + return userData[0]; +} + +/** + * Getting the current time + * @returns Current unix timestamp + */ +const now = () => Math.floor(Date.now() / 1000); diff --git a/api/src/types/error.ts b/api/src/types/error.ts new file mode 100644 index 0000000..fa072b6 --- /dev/null +++ b/api/src/types/error.ts @@ -0,0 +1,13 @@ +import { HTTPException } from "hono/http-exception"; + +export class UserInactiveError extends HTTPException{ + constructor(){ + super(403, { message: 'User is currently disabled.' }) + } +} + +export class MissingMailError extends HTTPException{ + constructor(){ + super(400, { message: 'Mail address is missing in authorizaton header.' }) + } +} \ No newline at end of file From 3574559eadf8eb2f525ef327a39e66ac3e762eff Mon Sep 17 00:00:00 2001 From: michivonah Date: Sat, 4 Oct 2025 20:16:20 +0200 Subject: [PATCH 2/2] implement themepark endpoints & specifiy new error type --- api/src/index.ts | 6 +++-- api/src/lib/cache.ts | 9 +++++++ api/src/lib/user-auth.ts | 6 ++--- api/src/routes/themepark.ts | 51 +++++++++++++++++++++++++++++++++++++ api/src/types/error.ts | 6 +++++ 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 api/src/lib/cache.ts create mode 100644 api/src/routes/themepark.ts diff --git a/api/src/index.ts b/api/src/index.ts index 6547f1b..a375971 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -4,6 +4,7 @@ import { getUser } from './lib/user-auth' import GitHub from '@auth/core/providers/github' import notification from './routes/notification' import logbook from './routes/logbook' +import themepark from './routes/themepark' import cronRouter from './jobs/cron' // create app @@ -26,13 +27,14 @@ app.use('/*', verifyAuth()) // example endpoint app.get('/protected', async (c) => { - const user = await getUser(c); - return c.json(user); + const user = await getUser(c); + return c.json(user); }) // define routes & export app app.route('/notification', notification) app.route('/logbook', logbook) +app.route('/themepark', themepark) export default { fetch: app.fetch, scheduled: cronRouter, diff --git a/api/src/lib/cache.ts b/api/src/lib/cache.ts new file mode 100644 index 0000000..33124ce --- /dev/null +++ b/api/src/lib/cache.ts @@ -0,0 +1,9 @@ +import { cache } from 'hono/cache' + +/** + * Cache unit to use for multiple endpoints as needed + */ +export const responseCache = cache({ + cacheName: 'themepark-assistant', + cacheControl: 'max-age=86400' +}); \ No newline at end of file diff --git a/api/src/lib/user-auth.ts b/api/src/lib/user-auth.ts index b5ebec8..fbc0ae9 100644 --- a/api/src/lib/user-auth.ts +++ b/api/src/lib/user-auth.ts @@ -4,7 +4,7 @@ import { UserSelect } from "../types/user"; import { user } from "../db/schema"; import { like } from "drizzle-orm"; import { DrizzleD1Database } from "drizzle-orm/d1"; -import { MissingMailError, UserInactiveError } from "../types/error"; +import { MissingMailError, UserInactiveError, DatabaseError } from "../types/error"; /** * Returns the details of a user from the given context @@ -27,7 +27,7 @@ export async function getUser(c: Context): Promise{ } catch(e){ - throw new Error(`Database error: ${e}`); + throw new DatabaseError(); } const dbResult = userData[0] ?? await createUser(db, mail); @@ -57,7 +57,7 @@ async function createUser(db: DrizzleD1Database, userMail: string): Promise { + const db = getDbContext(c) + + try{ + const themeparks = await db.select({ + id: themepark.id, + name: themepark.name, + countrycode: themepark.countrycode + }).from(themepark); + + return c.json(themeparks); + } + catch{ + throw new DatabaseError(); + } +}) + +/** + * Lists all attractions from a themepark with their id & name + */ +app.get('/list/:id/attraction', responseCache, async (c) => { + const parkId = parseInt(c.req.param('id')); + const db = getDbContext(c) + + try{ + const attractions = await db.select({ + id: attraction.id, + name: attraction.name, + }).from(attraction) + .where(eq(attraction.themeparkId, parkId)); + + return c.json(attractions); + } + catch{ + throw new DatabaseError(); + } +}) + +export default app \ No newline at end of file diff --git a/api/src/types/error.ts b/api/src/types/error.ts index fa072b6..b4a0c94 100644 --- a/api/src/types/error.ts +++ b/api/src/types/error.ts @@ -10,4 +10,10 @@ export class MissingMailError extends HTTPException{ constructor(){ super(400, { message: 'Mail address is missing in authorizaton header.' }) } +} + +export class DatabaseError extends HTTPException{ + constructor(){ + super(500, { message: 'Internal Database Error' }) + } } \ No newline at end of file