Skip to content

Commit

Permalink
Sync calendar with events (#626)
Browse files Browse the repository at this point in the history
# Screenshot 📸
<img width="500" alt="image"
src="https://github.com/dotkom/monoweb/assets/24441708/d551e090-61ae-4689-8c9e-7a52dc08e1cc">

# No auth endpoints
To test it locally you go to
- `http://localhost:3000/api/cal/all`
- ~~`http://localhost:3000/api/cal/user/01HB64XF7WXBPGVQKFKFGJBH4D` (or
another id ofc)~~
- `http://localhost:3000/api/cal/event/01HB64TWZK1N8ABMH8JAE12101`

# Auth (new :))))))
To get access to a users calendar, that signed in user must go to
`/api/cal/sign` and then use the returned token in
`/api/cal/user/${token}`

```sh
# not authed
$ curl http://localhost:3000/api/cal/sign
{"message":"Not signed in"}

# getting the token
$ curl 'http://localhost:3000/api/cal/sign' -H 'Cookie: next-auth.session-token=...'
{"token":"eyJhbGciOiJIUzI1NiJ9.MDFIUTZSNkdFWVcyWjNWV1ZCU0FYWFRLQlo.XklF4gPVajozOE8ZsImhHFZXzZc-fF6qQgeX5xNvOoM"}

# the contents
$ base64 -d <<< $(echo MDFIUTZSNkdFWVcyWjNWV1ZCU0FYWFRLQlo)
01HQ6R6GEYW2Z3VWVBSAXXTK

# downloading the calendar (can add this link to calendar app, google calendar, etc)
$ curl http://localhost:3000/api/cal/user/eyJhbGciOiJIUzI1NiJ9.MDFIUTZSNkdFWVcyWjNWV1ZCU0FYWFRLQlo.XklF4gPVajozOE8ZsImhHFZXzZc-fF6qQgeX5xNvOoM
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//sebbo.net//ical-generator//EN
NAME:01HQ6R6GEYW2Z3VWVBSAXXTKBZ online kalender
<...>

```
  • Loading branch information
joleeee authored Mar 13, 2024
1 parent 0ce106a commit 254bf28
Show file tree
Hide file tree
Showing 13 changed files with 1,877 additions and 3,493 deletions.
3 changes: 3 additions & 0 deletions apps/web/src/pages/api/cal/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CalendarAll } from "@dotkomonline/gateway-edge-nextjs"

export default CalendarAll
3 changes: 3 additions & 0 deletions apps/web/src/pages/api/cal/event/[eventid].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CalendarEvent } from "@dotkomonline/gateway-edge-nextjs"

export default CalendarEvent
3 changes: 3 additions & 0 deletions apps/web/src/pages/api/cal/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CalendarSign } from "@dotkomonline/gateway-edge-nextjs"

export default CalendarSign
3 changes: 3 additions & 0 deletions apps/web/src/pages/api/cal/user/[user].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CalendarUser } from "@dotkomonline/gateway-edge-nextjs"

export default CalendarUser
13 changes: 13 additions & 0 deletions packages/core/src/modules/event/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface EventRepository {
create(data: EventInsert): Promise<Event>
update(id: EventId, data: EventInsert): Promise<Event>
getAll(take: number, cursor?: Cursor): Promise<Event[]>
getAllByUserAttending(userId: string): Promise<Event[]>
getAllByCommitteeId(committeeId: string, take: number, cursor?: Cursor): Promise<Event[]>
getById(id: string): Promise<Event | undefined>
}
Expand Down Expand Up @@ -40,6 +41,17 @@ export class EventRepositoryImpl implements EventRepository {
return events.map((e) => mapToEvent(e))
}

async getAllByUserAttending(userId: string): Promise<Event[]> {
const eventsResult = await this.db
.selectFrom("attendance")
.leftJoin("attendee", "attendee.attendanceId", "attendance.id")
.innerJoin("event", "event.id", "attendance.eventId")
.selectAll("event")
.where("attendee.userId", "=", userId)
.execute()
return eventsResult.map(mapToEvent)
}

async getAllByCommitteeId(committeeId: string, take: number, cursor?: Cursor): Promise<Event[]> {
const query = orderedQuery(
this.db
Expand All @@ -54,6 +66,7 @@ export class EventRepositoryImpl implements EventRepository {
const events = await query.execute()
return events.map((e) => mapToEvent(e))
}

async getById(id: string): Promise<Event | undefined> {
const event = await this.db.selectFrom("event").where("id", "=", id).selectAll().executeTakeFirst()
return event === undefined ? undefined : mapToEvent(event)
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/modules/event/event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface EventService {
updateEvent(id: EventId, payload: Omit<EventWrite, "id">): Promise<Event>
getEventById(id: EventId): Promise<Event>
getEvents(take: number, cursor?: Cursor): Promise<Event[]>
getEventsByUserAttending(userId: string): Promise<Event[]>
getEventsByCommitteeId(committeeId: string, take: number, cursor?: Cursor): Promise<Event[]>
createAttendance(eventId: EventId, attendanceWrite: AttendanceWrite): Promise<Attendance>
listAttendance(eventId: EventId): Promise<Attendance[]>
Expand Down Expand Up @@ -37,6 +38,11 @@ export class EventServiceImpl implements EventService {
return events
}

async getEventsByUserAttending(userId: string): Promise<Event[]> {
const events = await this.eventRepository.getAllByUserAttending(userId)
return events
}

async getEventsByCommitteeId(committeeId: string, take: number, cursor?: Cursor): Promise<Event[]> {
const events = await this.eventRepository.getAllByCommitteeId(committeeId, take, cursor)
return events
Expand Down
11 changes: 9 additions & 2 deletions packages/gateway-edge-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
"dependencies": {
"@dotkomonline/core": "workspace:*",
"@dotkomonline/db": "workspace:*",
"@dotkomonline/gateway-trpc": "workspace:^",
"@dotkomonline/types": "workspace:^",
"@trpc/react-query": "^10.45.0",
"ical-generator": "^5.0.1",
"jsonwebtoken": "^9.0.2",
"next-auth": "^4.24.5",
"stripe": "^13.11.0"
},
"peerDependencies": {
Expand All @@ -25,6 +31,7 @@
"@biomejs/biome": "^1.5.3",
"@vitest/ui": "^0.34.6",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
"vitest": "^0.34.6",
"@types/jsonwebtoken": "^9.0.5"
}
}
}
156 changes: 156 additions & 0 deletions packages/gateway-edge-nextjs/src/cal/cal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { type NextApiRequest, type NextApiResponse } from "next"
import ical, { type ICalEventData } from "ical-generator"
import { type Event } from "@dotkomonline/types"
import { createServerSideHelpers } from "@trpc/react-query/server"
import { appRouter, createContextInner, transformer } from "@dotkomonline/gateway-trpc"
import { getServerSession } from "next-auth"
import jwt from "jsonwebtoken"
import { authOptions } from "../../../auth/src/web.app"

const helpers = createServerSideHelpers({
router: appRouter,
ctx: await createContextInner({
auth: null,
}),
transformer, // optional - adds superjson serialization
})

function eventUrl(req: NextApiRequest, event: Pick<Event, "id">) {
const proto = req.headers["x-forwarded-proto"] || "http"
const host = req.headers["x-forwarded-host"] || "online.ntnu.no"

// a better to to get/configure the url?
return `${proto}://${host}/events/${event.id}`
}

// ALL events
export async function CalendarAll(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
res.status(405).end()
return
}

const instance = ical({ name: "Online Linjeforening Arrangementer" })

const events = await helpers.event.all.fetch()

for (const event of events) {
instance.createEvent(toICal(req, event))
}

res.status(200).send(instance.toString())
}

// a single event
export async function CalendarEvent(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
res.status(405).end()
return
}

const eventid = req.query.eventid as string
if (!eventid) {
res.status(400).json({ message: "Missing eventid" })
return
}

const event = (await helpers.event.get.fetch(eventid)).event

const instance = ical()
instance.createEvent(toICal(req, event))

res.status(200).send(instance.toString())
}

// all events a user is attending
export async function CalendarUser(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
res.status(405).end()
return
}

const token = req.query.user as string
if (!token) {
res.status(400).json({ message: "Missing token" })
return
}

const cal_key = process.env.CAL_KEY
if (!cal_key) {
res.status(500).json({ message: "Missing key" })
}

let userid = ""
try {
userid = jwt.verify(token, cal_key as string) as string
} catch {
res.status(400).json({ message: "bad key" })
}

const events = await helpers.event.allByUserId.fetch({ id: userid })
const instance = ical({ name: `${userid} online kalender` })

for (const event of events) {
instance.createEvent(toICal(req, event))
instance.createEvent(toICal(req, toRegistration(event)))
}

res.status(200).send(instance.toString())
}

// make a token for the signed in user
export async function CalendarSign(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
res.status(405).end()
return
}

const session = await getServerSession(req, res, authOptions)
const authed_id = session?.user.id
if (!authed_id) {
res.status(400).json({ message: "Not signed in" })
}

const cal_key = process.env.CAL_KEY
if (!cal_key) {
res.status(500).json({ message: "Missing key" })
}

const token = jwt.sign(authed_id as string, cal_key as string, {})

res.status(200).json({ token })
}

function toICal(
req: NextApiRequest,
event: Pick<Event, "description" | "end" | "id" | "location" | "start" | "title">
): ICalEventData {
return {
start: event.start,
end: event.end,
summary: event.title,
description: event.description,
location: event.location,
url: eventUrl(req, event),
}
}

function toRegistration(
event: Pick<Event, "description" | "id" | "start" | "title">
): Pick<Event, "description" | "end" | "id" | "location" | "start" | "title"> {
// 5 days before
// TODO when db has this, we can use the actual start value, this is just for testing
const start = new Date(event.start.getTime() - 5 * 24 * 60 * 60 * 1000)

const title = `Påmelding for ${event.title}`
const location = "På OW"

return {
start,
end: start, // 0 length
title,
description: event.description,
location,
id: event.id,
}
}
1 change: 1 addition & 0 deletions packages/gateway-edge-nextjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./stripe/stripe-webhook"
export * from "./cal/cal"
5 changes: 4 additions & 1 deletion packages/gateway-trpc/src/modules/event/event-router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PaginateInputSchema } from "@dotkomonline/core"
import { CompanySchema, EventCommitteeSchema, EventSchema, EventWriteSchema } from "@dotkomonline/types"
import { CompanySchema, EventCommitteeSchema, EventSchema, EventWriteSchema, UserSchema } from "@dotkomonline/types"
import { z } from "zod"
import { attendanceRouter } from "./attendance-router"
import { eventCompanyRouter } from "./event-company-router"
Expand Down Expand Up @@ -63,6 +63,9 @@ export const eventRouter = t.router({
.query(async ({ input, ctx }) =>
ctx.companyEventService.getEventsByCompanyId(input.id, input.paginate.take, input.paginate.cursor)
),
allByUserId: publicProcedure
.input(z.object({ id: UserSchema.shape.id }))
.query(async ({ input, ctx }) => ctx.eventService.getEventsByUserAttending(input.id)),
allByCommittee: publicProcedure
.input(z.object({ id: CompanySchema.shape.id, paginate: PaginateInputSchema }))
.query(async ({ input, ctx }) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/gateway-trpc/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { committeeRouter } from "./modules/committee/committee-router"
import { companyRouter } from "./modules/company/company-router"
import { eventRouter } from "./modules/event/event-router"
import { attendanceRouter } from "./modules/event/attendance-router"
import { markRouter } from "./modules/mark/mark-router"
import { paymentRouter } from "./modules/payment/payment-router"
import { t } from "./trpc"
Expand All @@ -14,6 +15,7 @@ import { interestGroupRouter } from "./modules/interest-group/interest-group-rou
export const appRouter = t.router({
committee: committeeRouter,
event: eventRouter,
attendance: attendanceRouter,
user: userRouter,
company: companyRouter,
payment: paymentRouter,
Expand Down
Loading

0 comments on commit 254bf28

Please sign in to comment.