Skip to content

Commit

Permalink
Use OpenAuth for authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Murderlon committed Jan 12, 2025
1 parent 4a41d19 commit b22b440
Show file tree
Hide file tree
Showing 40 changed files with 348 additions and 672 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion infra/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { auth } from './auth'
import { domain } from './dns'
import { email } from './email'
import { secret } from './secret'
Expand All @@ -6,7 +7,7 @@ import { webhook } from './stripe'
export const api = new sst.aws.Function('Api', {
url: true,
handler: 'packages/functions/api/index.handler',
link: [secret.DATABASE_URL, secret.STRIPE_SECRET_KEY, webhook, email],
link: [secret.DATABASE_URL, secret.STRIPE_SECRET_KEY, webhook, email, auth],
permissions: [
{
actions: ['ses:SendEmail'],
Expand Down
39 changes: 39 additions & 0 deletions infra/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { domain } from './dns'
import { email } from './email'
import { secret } from './secret'

export const authTable = new sst.aws.Dynamo('LambdaAuthTable', {
fields: {
pk: 'string',
sk: 'string',
},
ttl: 'expiry',
primaryIndex: {
hashKey: 'pk',
rangeKey: 'sk',
},
})

export const auth = new sst.aws.Auth('Auth', {
forceUpgrade: 'v2',
authorizer: {
handler: 'packages/functions/auth.handler',
link: [
email,
authTable,
secret.GITHUB_CLIENT_ID,
secret.GITHUB_CLIENT_SECRET,
secret.DATABASE_URL,
],
permissions: [
{
actions: ['ses:SendEmail'],
resources: ['*'],
},
],
},
domain: {
name: `auth.${domain}`,
dns: sst.cloudflare.dns(),
},
})
2 changes: 2 additions & 0 deletions infra/www.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { auth } from './auth'
import { domain } from './dns'
import { email } from './email'
import { secret } from './secret'
Expand All @@ -20,6 +21,7 @@ export const www = new sst.aws.React('ReactRouter', {
],
link: [
email,
auth,
secret.SESSION_SECRET,
secret.ENCRYPTION_SECRET,
secret.DATABASE_URL,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
"dependencies": {
"@aws-sdk/client-sesv2": "^3.687.0",
"@neondatabase/serverless": "^0.9.5",
"stripe": "^15.5.0",
"sst": "^3.3.27",
"@openauthjs/openauth": "^0.3.2",
"drizzle-orm": "^0.36.2",
"sst": "^3.3.27",
"stripe": "^15.5.0",
"ulid": "^2.3.0",
"valibot": "^1.0.0-beta.11",
"ws": "^8.18.0"
},
"devDependencies": {
Expand Down
21 changes: 13 additions & 8 deletions packages/core/src/plan/seed.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { Stripe } from '@company/core/src/stripe'
import { db, schema } from '../drizzle'
import { PRICING_PLANS } from '../constants'

export default async function seed() {
const prices = await Stripe.client.prices.list()
const products = await Stripe.client.products.list()
const stage = JSON.parse(process.env.SST_RESOURCE_App as string).stage
const activeProducts = products.data.filter(
(p) => p.active && p.metadata.stage === stage,
)

for (const { id, name, description } of products.data.filter((p) => p.active)) {
for (const { id, name, description } of Object.values(PRICING_PLANS)) {
const stripeProduct = activeProducts.find((p) => p.name === name)!
await db.transaction(async (tx) => {
const [plan] = await tx
.insert(schema.plan)
.values({ id, name, description })
.returning()

await tx.insert(schema.plan).values({ id, name, description })
await tx.insert(schema.price).values(
prices.data
.filter((price) => price.product === id)
.filter(
(price) =>
price.product === stripeProduct.id && price.metadata.stage === stage,
)
.map((price) => ({
id: price.id,
planId: plan!.id,
planId: id,
amount: price.unit_amount ?? 0,
currency: price.currency,
interval: price.recurring?.interval ?? 'month',
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/subscription/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ export namespace Subscription {
})
}

export const insert = async (userID: string, sub: Stripe.Subscription) => {
export const insert = async (
userID: string,
sub: Stripe.Subscription,
planId: string,
) => {
await db.insert(schema).values({
id: sub.id,
userId: userID,
planId: String(sub.items.data[0]!.plan.product),
planId,
priceId: String(sub.items.data[0]!.price.id),
interval: String(sub.items.data[0]!.plan.interval),
status: sub.status,
Expand Down
72 changes: 70 additions & 2 deletions packages/core/src/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
import { eq } from 'drizzle-orm'
import { createSubjects } from '@openauthjs/openauth/subject'
import { db } from '../drizzle'
import { user as schema, userImage as imageSchema } from './sql'
import { role, roleToUser } from '../role/sql'
import { Role } from '../role'
// TODO: switch to zod once @conform-to/zod can handle new zod version
import * as v from 'valibot'
import type { InferOutput } from 'valibot'

export namespace User {
export const info = v.object({
id: v.string(),
email: v.pipe(v.string(), v.email()),
username: v.nullable(v.string()),
customerId: v.nullable(v.string()),
roles: v.array(
v.object({
name: v.union(Role.roles.map((role) => v.literal(role))),
id: v.string(),
}),
),
})

export type info = InferOutput<typeof info>

export const subjects = createSubjects({ user: info })

export const insert = async (email: string) => {
return db.transaction(async (tx) => {
const result = await tx.insert(schema).values({ email }).returning()
const user = result[0]!
const roles = await tx
.select({ id: role.id, name: role.name })
.from(role)
.where(eq(role.name, 'user'))

await tx
.insert(roleToUser)
.values(roles.map((role) => ({ roleId: role.id, userId: user.id })))

return { ...user, roles }
})
}

export const update = async (
id: string,
partial: Partial<typeof schema.$inferInsert>,
Expand All @@ -27,7 +67,35 @@ export namespace User {
})
}

export const image = async (id: string) => {
return db.query.userImage.findFirst({ where: eq(imageSchema.userId, id) })
export const fromEmailWithRole = async (email: string) => {
const user = await db.query.user.findFirst({
where: eq(schema.email, email),
columns: { createdAt: false, updatedAt: false },
with: {
roles: {
columns: {},
with: {
role: {
columns: {
name: true,
id: true,
},
},
},
},
},
})
if (!user) return
return {
...user,
roles: user.roles.map(({ role }) => ({ id: role.id, name: role.name })),
}
}

export const imageID = async (id: string) => {
return db.query.userImage.findFirst({
where: eq(imageSchema.userId, id),
columns: { id: true },
})
}
}
8 changes: 8 additions & 0 deletions packages/core/sst-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ declare module "sst" {
"type": "sst.aws.Router"
"url": string
}
"Auth": {
"type": "sst.aws.Auth"
"url": string
}
"DATABASE_URL": {
"type": "sst.sst.Secret"
"value": string
Expand All @@ -40,6 +44,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"LambdaAuthTable": {
"name": string
"type": "sst.aws.Dynamo"
}
"ReactRouter": {
"type": "sst.aws.React"
"url": string
Expand Down
9 changes: 2 additions & 7 deletions packages/functions/api/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,17 @@ import { Stripe } from '@company/core/src/stripe'
import { Subscription } from '@company/core/src/subscription/index'
import { User } from '@company/core/src/user/index'
import { Hono } from 'hono'
import { Resource } from 'sst'
import { z } from 'zod'

export const route = new Hono().post('/', async (ctx) => {
const sig = ctx.req.header('stripe-signature')

if (!sig) throw new Error(Stripe.errors.MISSING_SIGNATURE)

console.log({
sig,
secret: Resource.StripeWebhook.secret,
id: Resource.StripeWebhook.id,
})

const event = await Stripe.createEvent(await ctx.req.text(), sig)

console.log(event.type)

try {
switch (event.type) {
/**
Expand Down
45 changes: 45 additions & 0 deletions packages/functions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { issuer } from '@openauthjs/openauth'
import { handle } from 'hono/aws-lambda'
import { DynamoStorage } from '@openauthjs/openauth/storage/dynamo'
import { Resource } from 'sst'
import { PasswordProvider } from '@openauthjs/openauth/provider/password'
import { PasswordUI } from '@openauthjs/openauth/ui/password'
import { Email } from '@company/core/src/email/index'
import { User } from '@company/core/src/user/index'
import type { Theme } from '@openauthjs/openauth/ui/theme'

const theme: Theme = {
title: 'My company',
radius: 'md',
primary: '#1e293b',
favicon: 'https://stack.merlijn.site/favicon.ico',
}

const app = issuer({
theme,
storage: DynamoStorage({
table: Resource.LambdaAuthTable.name,
}),
subjects: User.subjects,
providers: {
password: PasswordProvider(
PasswordUI({
sendCode: async (email, code) => {
await Email.sendAuth({ email, code })
},
}),
),
},
success: async (ctx, value) => {
if (value.provider === 'password') {
let user = await User.fromEmailWithRole(value.email)
user ??= await User.insert(value.email)
if (!user) throw new Error('Unable to create user')

return ctx.subject('user', user)
}
throw new Error('Invalid provider')
},
})

export const handler = handle(app)
5 changes: 3 additions & 2 deletions packages/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"typecheck": "tsc"
},
"dependencies": {
"sst": "^3.3.27",
"hono": "^4.6.3"
"@openauthjs/openauth": "^0.3.2",
"hono": "^4.6.3",
"sst": "^3.3.27"
},
"devDependencies": {
"typescript": "^5.7.2"
Expand Down
8 changes: 8 additions & 0 deletions packages/functions/sst-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ declare module "sst" {
"type": "sst.aws.Router"
"url": string
}
"Auth": {
"type": "sst.aws.Auth"
"url": string
}
"DATABASE_URL": {
"type": "sst.sst.Secret"
"value": string
Expand All @@ -40,6 +44,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"LambdaAuthTable": {
"name": string
"type": "sst.aws.Dynamo"
}
"ReactRouter": {
"type": "sst.aws.React"
"url": string
Expand Down
5 changes: 5 additions & 0 deletions packages/www/app/@types/lucide.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'lucide-react' {
// Only show type suggestions for Lucide icons with a prefix.
// Otherwise you editor will try to import an icon instead of some component you actually want.
export * from 'lucide-react/dist/lucide-react.prefixed'
}
Loading

0 comments on commit b22b440

Please sign in to comment.