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..a375971 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,8 +1,10 @@ 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' +import themepark from './routes/themepark' import cronRouter from './jobs/cron' // create app @@ -24,14 +26,15 @@ 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 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 new file mode 100644 index 0000000..fbc0ae9 --- /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, DatabaseError } 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 DatabaseError(); + } + + 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 DatabaseError(); + } + + return userData[0]; +} + +/** + * Getting the current time + * @returns Current unix timestamp + */ +const now = () => Math.floor(Date.now() / 1000); diff --git a/api/src/routes/themepark.ts b/api/src/routes/themepark.ts new file mode 100644 index 0000000..49d3060 --- /dev/null +++ b/api/src/routes/themepark.ts @@ -0,0 +1,51 @@ +import { Hono } from 'hono' +import { getDbContext } from '../db/client' +import { themepark, attraction } from '../db/schema' +import { responseCache } from '../lib/cache' +import { eq } from 'drizzle-orm' +import { DatabaseError } from '../types/error' + +const app = new Hono() + +/** + * Lists all available themeparks with their id, name & countrycode + */ +app.get('/list', responseCache, async (c) => { + 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 new file mode 100644 index 0000000..b4a0c94 --- /dev/null +++ b/api/src/types/error.ts @@ -0,0 +1,19 @@ +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.' }) + } +} + +export class DatabaseError extends HTTPException{ + constructor(){ + super(500, { message: 'Internal Database Error' }) + } +} \ No newline at end of file