diff --git a/README.md b/README.md index 3413738..6db8be8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # themepark-assistant A tool for improving your trips to themeparks - once developed +> HINT: The tool is currently under development. The API endpoints are subject to change at any time. Use with caution. + ## Repo structure - /api: API implementation - ./: config files diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index 7267f91..922d49f 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -43,7 +43,8 @@ export const notificationMethod = sqliteTable('notification_method', { export const notificationProvider = sqliteTable('notification_provider', { id: integer().primaryKey({ autoIncrement: true }), - name: text().notNull().unique() + name: text().notNull().unique(), + isActive: integer({ mode: 'boolean' }).notNull().default(false), }) export const themepark = sqliteTable('themepark', { diff --git a/api/src/errors/http-error.ts b/api/src/errors/http-error.ts index 23c4767..988f04f 100644 --- a/api/src/errors/http-error.ts +++ b/api/src/errors/http-error.ts @@ -14,8 +14,12 @@ export class MissingMailError extends HTTPException{ } export class MissingParameter extends HTTPException{ - constructor(paramName: string){ - super(400, { message: `Request parameter '${paramName}' missing` }) + constructor(paramName?: string){ + super(400, { message: + paramName + ? `Request parameter '${paramName}' missing` + : 'Request parameter missing' + }) } } diff --git a/api/src/index.ts b/api/src/index.ts index f461e9e..b284adf 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -3,7 +3,7 @@ import { authHandler, initAuthConfig, verifyAuth } from '@hono/auth-js' import { getUser } from './lib/user-auth' import GitHub from '@auth/core/providers/github' import attraction from './routes/attraction' -import notification from './routes/notification' +import notification from './routes/notification-method' import logbook from './routes/logbook' import themepark from './routes/themepark' import cronRouter from './jobs/cron' @@ -34,7 +34,7 @@ app.get('/protected', async (c) => { // define routes & export app app.route('/attraction', attraction) -app.route('/notification', notification) +app.route('/notification-method', notification) app.route('/logbook', logbook) app.route('/themepark', themepark) export default { diff --git a/api/src/lib/check-notification-method-owner.ts b/api/src/lib/check-notification-method-owner.ts new file mode 100644 index 0000000..da9b5ea --- /dev/null +++ b/api/src/lib/check-notification-method-owner.ts @@ -0,0 +1,30 @@ +import { getDbContext, getDbEnv } from '../db/client' +import { notificationMethod } from '../db/schema'; +import { NotificationMethodSelect } from '../types/notification-method' +import { DatabaseError, InvalidParameter } from '../errors' +import { eq } from 'drizzle-orm'; + +type NotificationMethodUser = Pick; + +/** + * Checks if a specified user is the owner of a notification method and returns the owner if valid + * @param db DB connection (already defined as variable/const) + * @param methodId notificationMethodId to check + * @param userId User to check wheter it is the owner + * @returns Object with the owners userId + */ +export async function getNotificationMethodOwner(db: ReturnType, methodId: number, userId: number): Promise{ + try{ + const method = await db.select({ + userId: notificationMethod.userId + }).from(notificationMethod) + .where(eq(notificationMethod.id, methodId)).get(); + + if(!method || method.userId !== userId) throw new InvalidParameter('notificationMethodId'); + else return method; + } + catch(e){ + if(e instanceof InvalidParameter) throw e; + throw new DatabaseError(); + } +} \ No newline at end of file diff --git a/api/src/routes/attraction.ts b/api/src/routes/attraction.ts index bf59d49..a9f2a02 100644 --- a/api/src/routes/attraction.ts +++ b/api/src/routes/attraction.ts @@ -1,13 +1,11 @@ import { Hono, Context } from 'hono' import { getDbContext } from '../db/client' -import { attractionNotification, notificationMethod } from '../db/schema' +import { attractionNotification } from '../db/schema' import { and, eq } from 'drizzle-orm' import { DatabaseError, InvalidParameter, MissingParameter } from '../errors' import { getUser } from '../lib/user-auth' import { Message } from '../types/response' -import { NotificationMethodSelect } from '../types/notification-method' - -type NotificationMethodUser = Pick; +import { getNotificationMethodOwner } from '../lib/check-notification-method-owner' const app = new Hono() @@ -22,29 +20,6 @@ function getNotificationMethodId(c: Context): number | undefined{ return parseInt(str); } -/** - * Checks if a specified user is the owner of a notification method and returns the owner if valid - * @param db DB connection (already defined as variable/const) - * @param methodId notificationMethodId to check - * @param userId User to check wheter it is the owner - * @returns Object with the owners userId - */ -async function getNotificationMethodOwner(db: ReturnType, methodId: number, userId: number): Promise{ - try{ - const method = await db.select({ - userId: notificationMethod.userId - }).from(notificationMethod) - .where(eq(notificationMethod.id, methodId)).get(); - - if(!method || method.userId !== userId) throw new InvalidParameter('notificationMethodId'); - else return method; - } - catch(e){ - if(e instanceof InvalidParameter) throw e; - throw new DatabaseError(); - } -} - /** * Checks if drizzle db error has a message cause * @param e Thrown error diff --git a/api/src/routes/notification-method.ts b/api/src/routes/notification-method.ts new file mode 100644 index 0000000..7dde507 --- /dev/null +++ b/api/src/routes/notification-method.ts @@ -0,0 +1,135 @@ +import { Hono } from 'hono' +import { getDbContext } from '../db/client' +import { notificationMethod, notificationProvider } from '../db/schema' +import { eq, and } from 'drizzle-orm' +import { getUser } from '../lib/user-auth' +import { DatabaseError, InvalidParameter, MissingParameter } from '../errors' +import { Message } from '../types/response' + +const app = new Hono() + +/** + * Gets you the id of a notificaton provider by name + * @param db DB connection (as const) + * @param name Name of the notification provider + * @returns Id of the specified notification provider's name (undefined if not exists) + */ +async function getProviderId(db: ReturnType, name: string){ + try{ + const provider = await db.select({ + id: notificationProvider.id + }).from(notificationProvider) + .where(eq(notificationProvider.name, name)).get(); + + return provider?.id; + } + catch(e){ + throw new InvalidParameter('provider'); + } +} + +/** Returns a list of all notification methods a user owns */ +app.get('/list', async (c) => { + const db = getDbContext(c); + const user = await getUser(c); + + try{ + const methods = await db.select({ + id: notificationMethod.id, + webhook: notificationMethod.webhookUrl, + name: notificationMethod.shownName, + provider: notificationProvider.name + }).from(notificationMethod) + .where(eq(notificationMethod.userId, user.id)) + .innerJoin(notificationProvider, eq(notificationMethod.notificationProviderId, notificationProvider.id)); + + return c.json(methods); + } + catch(e){ + throw new DatabaseError(); + } +}) + +/** Lists all available notification providers */ +app.get('/list-providers', async (c) => { + const db = getDbContext(c); + + try{ + const providers = await db.selectDistinct({ + name: notificationProvider.name + }).from(notificationProvider) + .where(eq(notificationProvider.isActive, true)); + + return c.json(providers); + } + catch{ + throw new DatabaseError(); + } +}) + +/** Creates a new notification method from url, name & provider */ +app.post('/add-method', async (c) => { + const db = getDbContext(c); + const user = await getUser(c); + + const { url, name, provider } = c.req.query(); + + if(!url || !name || !provider) throw new MissingParameter(); + + const providerId = await getProviderId(db, provider); + if(!providerId) throw new InvalidParameter('provider'); + + try{ + const newMethod = await db.insert(notificationMethod).values({ + webhookUrl: url, + shownName: name, + userId: user.id, + notificationProviderId: providerId + }).returning().onConflictDoNothing().get(); + + return c.json( + newMethod + ? new Message('Successfull created new notification method.', { + id: newMethod.id, + webhook: newMethod.webhookUrl, + name: newMethod.shownName + }) + : new Message('Notification method with this URL already exists. No changes made.') + ); + } + catch(e){ + throw new DatabaseError(); + } +}) + +/** Removes a existing notification method by id (has to be owned by the current user) */ +app.delete('/remove-method/:id', async (c) => { + const db = getDbContext(c); + const user = await getUser(c); + const methodId = parseInt(c.req.param('id')); + + if(!methodId) throw new InvalidParameter('id'); + + try{ + const res = await db.delete(notificationMethod).where( + and( + eq(notificationMethod.id, methodId), + eq(notificationMethod.userId, user.id) + ) + ).returning(); + + return c.json(new Message( + res.length > 0 + ? `Notification method ${methodId} was removed.` + : `No matching notification method with id ${methodId}. No changes made.`, + { + notificationMethodId: methodId + } + )) + } + catch{ + throw new DatabaseError(); + } +}) + +export default app \ No newline at end of file diff --git a/api/src/routes/notification.ts b/api/src/routes/notification.ts deleted file mode 100644 index 6cf3575..0000000 --- a/api/src/routes/notification.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Hono } from 'hono' -import { getDbContext } from '../db/client' -import { notificationMethod, user, themepark } from '../db/schema' - -const app = new Hono() - -app.get('/list', async (c) => { - const db = getDbContext(c) - await db.insert(user).values({ username: 'notification'}); - //await db.insert(themepark).values({ name: 'Test', countrycode: 'CH'}); - return c.json( - { - message: 'List all notification methods' - } - ) -}) - -export default app \ No newline at end of file