Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate backend to Elysia #641

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,5 @@ ENV TZ="Asia/Bangkok"
COPY package.json ./
COPY --chown=nonroot:nonroot --from=deps-prod /app/node_modules ./node_modules
COPY --chown=nonroot:nonroot --from=builder /app/dist ./dist
COPY server.mjs ./

CMD ["./server.mjs"]

8 changes: 4 additions & 4 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dotenv/config'
import { defineConfig } from 'astro/config'

/* Adapter */
import node from '@astrojs/node'
// import node from '@astrojs/node'

/* Integrations */
import react from '@astrojs/react'
Expand All @@ -14,9 +14,9 @@ import sentry from '@sentry/astro'
export default defineConfig({
output: 'server',
site: 'https://creatorsgarten.org',
adapter: node({
mode: 'middleware',
}),
// adapter: node({
// mode: 'middleware',
// }),
prefetch: true,
integrations: [
tailwind(),
Expand Down
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"version": "0.0.1",
"private": true,
"packageManager": "[email protected]",
"trustedDependencies": ["@sentry/astro"],
"lint-staged": {
"*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}": [
"prettier --write"
Expand All @@ -19,22 +19,22 @@
"prepare": "test -d node_modules/husky && husky install || echo \"husky is not installed\""
},
"dependencies": {
"@bogeychan/elysia-polyfills": "^0.6.4",
"@contentsgarten/html": "^1.3.0",
"@doist/typist": "^6.0.0",
"@fastify/middie": "^8.3.0",
"@fastify/static": "^7.0.0",
"@elysiajs/eden": "^1.0.12",
"@nanostores/logger": "^0.3.0",
"@nanostores/persistent": "^0.10.0",
"@nanostores/react": "^0.7.1",
"@sentry/astro": "^7.86.0",
"@sinclair/typebox": "^0.32.29",
"@tanstack/react-query": "^5.0.0",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"@trpc/client": "^10.45.2",
"astro-tweet": "^0.0.4",
"clsx": "^2.0.0",
"csrf": "^3.1.0",
"dayjs": "^1.11.9",
"fastify": "^4.22.1",
"elysia": "^1.0.16",
"google-auth-library": "^9.0.0",
"jose": "^5.0.0",
"jsonwebtoken": "^9.0.2",
Expand All @@ -47,9 +47,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-iconify-icon-wrapper": "^0.0.3",
"sitemap": "^7.1.1",
"zod": "^3.22.2",
"zod-validation-error": "^3.0.0"
"sitemap": "^7.1.1"
},
"devDependencies": {
"@astrojs/check": "^0.5.0",
Expand Down
21 changes: 0 additions & 21 deletions server.mjs

This file was deleted.

39 changes: 17 additions & 22 deletions src/backend/auth/authenticateDeviceAuthorizationSignature.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
import { collections } from '$constants/mongo'
import { TRPCError } from '@trpc/server'
import { collections } from '$constants/mongo.ts'
import { ObjectId } from 'mongodb'
import { generateMessageHash } from '../signatures/generateSignature'
import { verifySignature } from '../signatures/verifySignature'
import { finalizeAuthentication } from './finalizeAuthentication'
import { generateMessageHash } from '../signatures/generateSignature.ts'
import { verifySignature } from '../signatures/verifySignature.ts'
import { finalizeAuthentication } from './finalizeAuthentication.ts'
import type { Handler } from 'elysia'

type Set = Parameters<Handler>[0]['set']

export async function authenticateDeviceAuthorizationSignature(
deviceId: string,
signature: string
signature: string,
set: Set
) {
const result = await verifySignature(signature)
if (!result.verified) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Signature is not verified',
})
set.status = 401
return 'Signature is not verified'
}

const expectedMessage = `mobileAuthorize:${deviceId}`
const expectedMessageHash = generateMessageHash(expectedMessage)
if (result.messageHash !== expectedMessageHash) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Signature is not for mobile authorization',
})
set.status = 401
return 'Signature is not for mobile authorization'
}

if (Date.parse(result.timestamp) < Date.now() - 15 * 60e3) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Signature is expired',
})
set.status = 401
return 'Signature is expired'
}

const userId = result.userId
const user = await collections.users.findOne({ _id: new ObjectId(userId) })
if (!user) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'User not found in database. This should not happen.',
})
set.status = 500
return 'User not found in database. This should not happen.'
}

return finalizeAuthentication(user.uid)
Expand Down
8 changes: 4 additions & 4 deletions src/backend/auth/authenticateDiscord.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ObjectId } from 'mongodb'

import { collections } from '$constants/mongo'
import { discordClient } from '$constants/secrets/discordClient'
import { collections } from '$constants/mongo.ts'
import { discordClient } from '$constants/secrets/discordClient.ts'

import { getAuthenticatedUser } from './getAuthenticatedUser'
import { finalizeAuthentication } from './finalizeAuthentication'
import { getAuthenticatedUser } from './getAuthenticatedUser.ts'
import { finalizeAuthentication } from './finalizeAuthentication.ts'

import type { User } from '$types/mongo/User'

Expand Down
11 changes: 5 additions & 6 deletions src/backend/auth/authenticateEventpopUser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { finalizeAuthentication } from './finalizeAuthentication'
import { collections } from '$constants/mongo.ts'
import { eventpopClient } from '$constants/secrets/eventpopClient.ts'

import { collections } from '$constants/mongo'
import { eventpopClient } from '$constants/secrets/eventpopClient'

import { getEventpopUser } from 'src/backend/auth/getEventpopUser'
import { getEventpopUserTickets } from 'src/backend/auth/getEventpopUserTickets'
import { finalizeAuthentication } from './finalizeAuthentication.ts'
import { getEventpopUser } from './getEventpopUser'
import { getEventpopUserTickets } from './getEventpopUserTickets'

interface EventpopAuthorizationResponse {
access_token: string
Expand Down
8 changes: 4 additions & 4 deletions src/backend/auth/authenticateGitHub.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ObjectId } from 'mongodb'

import { githubClient } from '$constants/secrets/githubClient'
import { collections } from '$constants/mongo'
import { githubClient } from '$constants/secrets/githubClient.ts'
import { collections } from '$constants/mongo.ts'

import { getAuthenticatedUser } from './getAuthenticatedUser'
import { finalizeAuthentication } from './finalizeAuthentication'
import { getAuthenticatedUser } from './getAuthenticatedUser.ts'
import { finalizeAuthentication } from './finalizeAuthentication.ts'

import type { User } from '$types/mongo/User'

Expand Down
8 changes: 4 additions & 4 deletions src/backend/auth/finalizeAuthentication.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import jwt from 'jsonwebtoken'

import { collections } from '$constants/mongo'
import { maxSessionAge } from '$constants/maxSessionAge'
import { privateKey } from '$constants/secrets/privateKey'
import { collections } from '$constants/mongo.ts'
import { maxSessionAge } from '$constants/maxSessionAge.ts'
import { privateKey } from '$constants/secrets/privateKey.ts'

import type { AuthenticatedUser } from '$types/AuthenticatedUser'
import type { AuthenticatedUser } from '$types/AuthenticatedUser.ts'

export const finalizeAuthentication = async (uid: number) => {
// get mongo document
Expand Down
4 changes: 2 additions & 2 deletions src/backend/auth/getAuthenticatedUser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'

import { privateKey } from '$constants/secrets/privateKey'
import { privateKey } from '$constants/secrets/privateKey.ts'

import type { AuthenticatedUser } from '$types/AuthenticatedUser'
import type { AuthenticatedUser } from '$types/AuthenticatedUser.ts'

export const getAuthenticatedUser = async (
token?: string
Expand Down
2 changes: 2 additions & 0 deletions src/backend/auth/getBearer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getBearer = (input?: string) =>
input?.startsWith('Bearer ') ? input.slice(7) : undefined
95 changes: 95 additions & 0 deletions src/backend/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Elysia } from 'elysia'

import {
auditInputSchema,
authorizationCodeInputSchema,
deviceAuthorizationSignatureInputSchema,
mintIdTokenInputSchema,
} from './models'

import { getAuthenticatedUser } from './getAuthenticatedUser'
import { checkOAuthAudit, recordOAuthAudit } from './oAuthAudit'
import { mintIdToken } from './mintIdToken.ts'
import { authenticateEventpopUser } from './authenticateEventpopUser.ts'
import { authenticateDeviceAuthorizationSignature } from './authenticateDeviceAuthorizationSignature.ts'
import { authenticateGitHub } from './authenticateGitHub.ts'
import { authenticateDiscord } from './authenticateDiscord.ts'
import { createPrivateKey, createPublicKey } from 'crypto'
import { privateKey } from '$constants/secrets/privateKey.ts'
import { exportJWK } from 'jose'
import { getBearer } from './getBearer.ts'
import { authenticatedHandler } from '../handler/authenticated.ts'

export const auth = new Elysia({
name: 'auth',
prefix: '/auth',
})
.derive(({ headers }) => ({
bearer: getBearer(headers['authorization']),
}))
.get('/user', ({ bearer }) => getAuthenticatedUser(bearer))

// OAuth
.get('/oauth/check', ({ bearer, body }) => checkOAuthAudit(bearer, body), {
beforeHandle: authenticatedHandler,
body: auditInputSchema,
})
.post('/oauth/record', ({ bearer, body }) => recordOAuthAudit(bearer, body), {
beforeHandle: authenticatedHandler,
body: auditInputSchema,
})

// Handler to mint ID token
.post(
'/mintIdToken',
async ({ bearer, body }) =>
mintIdToken((await getAuthenticatedUser(bearer))!, body),
{
beforeHandle: authenticatedHandler,
body: mintIdTokenInputSchema,
}
)

// Sign-in handler
.post(
'/signIn/eventpopAuthorizationCode',
({ body }) => authenticateEventpopUser(body.code),
{
body: authorizationCodeInputSchema,
}
)
.post(
'/signIn/deviceAuthorizationSignature',
({ body, set }) =>
authenticateDeviceAuthorizationSignature(
body.deviceId,
body.signature,
set
),
{
body: deviceAuthorizationSignatureInputSchema,
}
)

// Account linking
.post(
'/link/github',
({ bearer, body }) => authenticateGitHub(body.code, bearer),
{
body: authorizationCodeInputSchema,
}
)
.post(
'/link/discord',
({ bearer, body }) => authenticateDiscord(body.code, bearer),
{
body: authorizationCodeInputSchema,
}
)

// JWKs
.get('/publicKeys', async () => {
const privateKeyObj = createPrivateKey(privateKey)
const publicKeyObj = createPublicKey(privateKeyObj)
return [{ ...(await exportJWK(publicKeyObj)), kid: 'riffy1' }]
})
24 changes: 12 additions & 12 deletions src/backend/auth/mintIdToken.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import jwt from 'jsonwebtoken'
import type { AuthenticatedUser } from '$types/AuthenticatedUser'
import { privateKey } from '$constants/secrets/privateKey'
import { getJoinedEvents } from '../events/getJoinedEvents'
import type { GitHubConnection } from '$types/mongo/User/GitHubConnection'
import type { DiscordConnection } from '$types/mongo/User/DiscordConnection'
import type { Static } from 'elysia'
import type { AuthenticatedUser } from '$types/AuthenticatedUser.ts'
import { privateKey } from '$constants/secrets/privateKey.ts'
import { getJoinedEvents } from '../events/getJoinedEvents.ts'
import type { GitHubConnection } from '$types/mongo/User/GitHubConnection.ts'
import type { DiscordConnection } from '$types/mongo/User/DiscordConnection.ts'
import type { mintIdTokenInputSchema } from './models.ts'

/**
* Data contained in ID token returned by Authgarten OIDC provider.
Expand Down Expand Up @@ -67,9 +69,7 @@ export interface AuthgartenOidcClaims {

export async function mintIdToken(
user: AuthenticatedUser,
audience: string,
nonce: string | undefined,
scopes: string[]
input: Static<typeof mintIdTokenInputSchema>
): Promise<{ idToken: string; claims: AuthgartenOidcClaims }> {
const claims: AuthgartenOidcClaims = {
sub: String(user.sub),
Expand All @@ -82,16 +82,16 @@ export async function mintIdToken(
github: user.connections.github,
discord: user.connections.discord,
},
nonce,
nonce: input.nonce,
}

if (scopes.includes('email')) {
if (input.scopes.includes('email')) {
claims.email = user.email
}

const eventpopEventRegex = /^https:\/\/eventpop\.me\/e\/(\d+)$/
const eventIds = new Set<number>()
for (const scope of scopes) {
for (const scope of input.scopes) {
const match = eventpopEventRegex.exec(scope)
if (match) {
eventIds.add(Number(match[1]))
Expand All @@ -117,7 +117,7 @@ export async function mintIdToken(

// https://openid.net/specs/openid-connect-basic-1_0.html#IDToken
issuer: 'https://creatorsgarten.org',
audience: audience,
audience: input.audience,
expiresIn: 3600,

header: {
Expand Down
Loading
Loading