Skip to content

Commit

Permalink
feat(api): add user metadata with timezone
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Sep 23, 2024
1 parent a230368 commit 0781de5
Show file tree
Hide file tree
Showing 6 changed files with 734 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "UserMetadata" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"timezone" TEXT NOT NULL,

CONSTRAINT "UserMetadata_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "UserMetadata_userId_key" ON "UserMetadata"("userId");

-- AddForeignKey
ALTER TABLE "UserMetadata" ADD CONSTRAINT "UserMetadata_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
12 changes: 12 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ model User {
createdFromInvitation BudgetUserInvitationResponse?
categories Category[]
uploadedBlobObjects BlobObject[]
metadata UserMetadata?
}

model UserMetadata {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
timezone String
}

model UserWalletAccount {
Expand Down
129 changes: 82 additions & 47 deletions apps/api/v1/routes/users.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,95 @@
import { zCreateUser } from '@6pm/validation'
import { zCreateUser, zUpdateUserMetadata } from '@6pm/validation'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { getLogger } from '../../lib/log'
import { getAuthUser } from '../middlewares/auth'
import { bootstrapUserDefaultCategories } from '../services/category.service'
import { deleteClerkUser } from '../services/clerk.service'
import {
canUserUpdateMetadata,
updateUserMetadata,
} from '../services/user-metadata.service'
import { createUser } from '../services/user.service'
import { bootstrapUserDefaultWalletAccounts } from '../services/wallet.service'
import { zDeviceCurrencyHeader, zDeviceLanguageHeader } from './utils'

const router = new Hono().post(
'/',
zValidator('json', zCreateUser),
zDeviceLanguageHeader(),
zDeviceCurrencyHeader(),
async (c) => {
const existingUser = getAuthUser(c)
const deviceLanguage = c.req.valid('header')['x-device-language']
const deviceCurrency = c.req.valid('header')['x-device-currency']
const logger = getLogger('POST /users')

if (existingUser) {
return c.json({ message: 'user already exists' }, 409)
}

const userId = c.get('userId')

if (!userId) {
logger.warn('Clerk userId not found from headers while creating user')
return c.json({ message: 'unauthorized' }, 401)
}

const data = c.req.valid('json')

try {
const user = await createUser({ data: { ...data, id: userId } })

// bootstrap user data
await Promise.all([
bootstrapUserDefaultCategories({ user, language: deviceLanguage }),
bootstrapUserDefaultWalletAccounts({
user,
preferredCurrency: deviceCurrency,
language: deviceLanguage,
}),
])

return c.json(user, 201)
} catch (e) {
logger.error('Failed to create user %o', e)

await deleteClerkUser(userId)

return c.json({ userId, message: 'failed to create user', cause: e }, 500)
}
},
const zUserIdParamValidator = zValidator(
'param',
z.object({ userId: z.string() }),
)

const router = new Hono()
.post(
'/',
zValidator('json', zCreateUser),
zDeviceLanguageHeader(),
zDeviceCurrencyHeader(),
async (c) => {
const existingUser = getAuthUser(c)
const deviceLanguage = c.req.valid('header')['x-device-language']
const deviceCurrency = c.req.valid('header')['x-device-currency']
const logger = getLogger('POST /users')

if (existingUser) {
return c.json({ message: 'user already exists' }, 409)
}

const userId = c.get('userId')

if (!userId) {
logger.warn('Clerk userId not found from headers while creating user')
return c.json({ message: 'unauthorized' }, 401)
}

const data = c.req.valid('json')

try {
const user = await createUser({ data: { ...data, id: userId } })

// bootstrap user data
await Promise.all([
bootstrapUserDefaultCategories({ user, language: deviceLanguage }),
bootstrapUserDefaultWalletAccounts({
user,
preferredCurrency: deviceCurrency,
language: deviceLanguage,
}),
])

return c.json(user, 201)
} catch (e) {
logger.error('Failed to create user %o', e)

await deleteClerkUser(userId)

return c.json(
{ userId, message: 'failed to create user', cause: e },
500,
)
}
},
)
.put(
'/:userId/metadata',
zUserIdParamValidator,
zValidator('json', zUpdateUserMetadata),
async (c) => {
const { userId } = c.req.valid('param')
const user = getAuthUser(c)
const data = c.req.valid('json')

if (!(user && canUserUpdateMetadata({ user, metadata: { userId } }))) {
return c.json({ message: 'unauthorized' }, 401)
}

const updatedUser = await updateUserMetadata({
userId,
data,
})

return c.json(updatedUser)
},
)

export default router
49 changes: 49 additions & 0 deletions apps/api/v1/services/user-metadata.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { User, UserMetadata } from '@prisma/client'
import prisma from '../../lib/prisma'
import { findUserById } from './user.service'

/**
* Checks if the user can update the user metadata
* @returns true if the user can update the metadata, false otherwise
*/
export function canUserUpdateMetadata({
user,
metadata,
}: {
/** The user who wants to update metadata */
user: User
/** The target metadata to update */
metadata: Pick<UserMetadata, 'userId'>
}) {
return user.id === metadata.userId
}

/**
* Updates the user metadata
* @returns the updated user with metadata
*/
export async function updateUserMetadata({
userId,
metadataId,
data,
}: {
/** The user id who wants to update metadata */
userId: string
/** The target metadata to update */
metadataId?: string
/** The data to update */
data: { timezone: string }
}) {
const metadata = await prisma.userMetadata.upsert({
where: {
id: metadataId,
},
create: {
...data,
userId,
},
update: data,
})

return findUserById(metadata.userId)
}
Loading

0 comments on commit 0781de5

Please sign in to comment.