Skip to content

Commit

Permalink
feat(api-client): Added workspace controller (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b authored Sep 14, 2024
1 parent a97681e commit 2f4edec
Show file tree
Hide file tree
Showing 21 changed files with 629 additions and 88 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EnvSchema } from '@/common/env/env.schema'
import { IntegrationModule } from '@/integration/integration.module'
import { FeedbackModule } from '@/feedback/feedback.module'
import { CacheModule } from '@/cache/cache.module'
import { WorkspaceMembershipModule } from '@/workspace-membership/workspace-membership.module'

@Module({
controllers: [AppController],
Expand Down Expand Up @@ -55,7 +56,8 @@ import { CacheModule } from '@/cache/cache.module'
ProviderModule,
IntegrationModule,
FeedbackModule,
CacheModule
CacheModule,
WorkspaceMembershipModule
],
providers: [
{
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/auth/auth.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Authority, User } from '@prisma/client'
import { UserWithWorkspace } from '@/user/user.types'
import { Authority, User, Workspace } from '@prisma/client'

export type UserAuthenticatedResponse = User & {
export interface UserAuthenticatedResponse extends UserWithWorkspace {
token: string
}

export type AuthenticatedUserContext = User & {
isAuthViaApiKey?: boolean
apiKeyAuthorities?: Set<Authority>
defaultWorkspace: Workspace
}
25 changes: 14 additions & 11 deletions apps/api/src/auth/guard/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AuthenticatedUserContext } from '../../auth.types'
import { EnvSchema } from '@/common/env/env.schema'
import { CacheService } from '@/cache/cache.service'
import { toSHA256 } from '@/common/cryptography'
import { getUserByEmailOrId } from '@/common/user'

const X_E2E_USER_EMAIL = 'x-e2e-user-email'
const X_KEYSHADE_TOKEN = 'x-keyshade-token'
Expand Down Expand Up @@ -75,11 +76,7 @@ export class AuthGuard implements CanActivate {
throw new ForbiddenException()
}

user = await this.prisma.user.findUnique({
where: {
email
}
})
user = await getUserByEmailOrId(email, this.prisma)
} else {
const request = context.switchToHttp().getRequest()

Expand All @@ -102,7 +99,17 @@ export class AuthGuard implements CanActivate {
throw new ForbiddenException('Invalid API key')
}

user = apiKey.user
const defaultWorkspace = await this.prisma.workspace.findFirst({
where: {
ownerId: apiKey.userId,
isDefault: true
}
})

user = {
...apiKey.user,
defaultWorkspace
}
user.isAuthViaApiKey = true
user.apiKeyAuthorities = new Set(apiKey.authorities)
} else if (authType === 'JWT') {
Expand All @@ -118,11 +125,7 @@ export class AuthGuard implements CanActivate {
const cachedUser = await this.cache.getUser(payload['id'])
if (cachedUser) user = cachedUser
else {
user = await this.prisma.user.findUnique({
where: {
id: payload['id']
}
})
user = await getUserByEmailOrId(payload['id'], this.prisma)
}
} catch {
throw new ForbiddenException()
Expand Down
19 changes: 8 additions & 11 deletions apps/api/src/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { PrismaService } from '@/prisma/prisma.service'
import { AuthProvider } from '@prisma/client'
import { CacheService } from '@/cache/cache.service'
import { generateOtp } from '@/common/util'
import { createUser } from '@/common/user'
import { createUser, getUserByEmailOrId } from '@/common/user'
import { UserWithWorkspace } from '@/user/user.types'

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -62,7 +63,7 @@ export class AuthService {
email: string,
otp: string
): Promise<UserAuthenticatedResponse> {
const user = await this.findUserByEmail(email)
const user = await getUserByEmailOrId(email, this.prisma)
if (!user) {
this.logger.error(`User not found: ${email}`)
throw new NotFoundException('User not found')
Expand Down Expand Up @@ -175,7 +176,11 @@ export class AuthService {
name?: string,
profilePictureUrl?: string
) {
let user = await this.findUserByEmail(email)
let user: UserWithWorkspace | null

try {
user = await getUserByEmailOrId(email, this.prisma)
} catch (ignored) {}

// We need to create the user if it doesn't exist yet
if (!user) {
Expand Down Expand Up @@ -204,12 +209,4 @@ export class AuthService {
private async generateToken(id: string) {
return await this.jwt.signAsync({ id })
}

private async findUserByEmail(email: string) {
return await this.prisma.user.findUnique({
where: {
email
}
})
}
}
11 changes: 7 additions & 4 deletions apps/api/src/cache/cache.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common'
import { RedisClientType } from 'redis'
import { User } from '@prisma/client'
import { REDIS_CLIENT } from '@/provider/redis.provider'
import { UserWithWorkspace } from '@/user/user.types'

@Injectable()
export class CacheService implements OnModuleDestroy {
Expand All @@ -15,7 +15,10 @@ export class CacheService implements OnModuleDestroy {
return `${CacheService.USER_PREFIX}${userId}`
}

async setUser(user: User, expirationInSeconds?: number): Promise<void> {
async setUser(
user: UserWithWorkspace,
expirationInSeconds?: number
): Promise<void> {
const key = this.getUserKey(user.id)
const userJson = JSON.stringify(user)
if (expirationInSeconds) {
Expand All @@ -25,11 +28,11 @@ export class CacheService implements OnModuleDestroy {
}
}

async getUser(userId: string): Promise<User | null> {
async getUser(userId: string): Promise<UserWithWorkspace | null> {
const key = this.getUserKey(userId)
const userData = await this.redisClient.publisher.get(key)
if (userData) {
return JSON.parse(userData) as User
return JSON.parse(userData) as UserWithWorkspace
}
return null
}
Expand Down
58 changes: 39 additions & 19 deletions apps/api/src/common/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AuthProvider, User, Workspace } from '@prisma/client'
import { AuthProvider, User } from '@prisma/client'
import { PrismaService } from '@/prisma/prisma.service'
import { CreateUserDto } from '@/user/dto/create.user/create.user'
import { Logger, NotFoundException } from '@nestjs/common'
import { createWorkspace } from './workspace'
import { UserWithWorkspace } from '@/user/user.types'

/**
* Creates a new user and optionally creates a default workspace for them.
Expand All @@ -11,14 +12,15 @@ import { createWorkspace } from './workspace'
* @returns The created user and, if the user is not an admin, a default workspace.
*/
export async function createUser(
dto: Partial<CreateUserDto> & { authProvider: AuthProvider },
dto: Partial<CreateUserDto> & { authProvider: AuthProvider; id?: User['id'] },
prisma: PrismaService
): Promise<User & { defaultWorkspace?: Workspace }> {
): Promise<UserWithWorkspace> {
const logger = new Logger('createUser')

// Create the user
const user = await prisma.user.create({
data: {
id: dto.id,
email: dto.email,
name: dto.name,
profilePictureUrl: dto.profilePictureUrl,
Expand All @@ -31,7 +33,10 @@ export async function createUser(

if (user.isAdmin) {
logger.log(`Created admin user ${user.id}`)
return user
return {
...user,
defaultWorkspace: null
}
}

// Create the user's default workspace
Expand All @@ -51,26 +56,41 @@ export async function createUser(
}

/**
* Finds a user by their email address.
*
* @param email The email address to search for.
* @param prisma The Prisma client instance.
* @returns The user with the given email address, or null if no user is found.
* @throws NotFoundException if no user is found with the given email address.
* Finds a user by their email or ID.
* @param input The email or ID of the user to find.
* @param prisma The Prisma client to use for the database operation.
* @throws {NotFoundException} If the user is not found.
* @returns The user with their default workspace.
*/
export async function getUserByEmail(
email: User['email'],
export async function getUserByEmailOrId(
input: User['email'] | User['id'],
prisma: PrismaService
): Promise<User | null> {
const user = await prisma.user.findUnique({
): Promise<UserWithWorkspace> {
const user =
(await prisma.user.findUnique({
where: {
email: input
}
})) ??
(await prisma.user.findUnique({
where: {
id: input
}
}))

if (!user) {
throw new NotFoundException(`User ${input} not found`)
}

const defaultWorkspace = await prisma.workspace.findFirst({
where: {
email
ownerId: user.id,
isDefault: true
}
})

if (!user) {
throw new NotFoundException(`User ${email} not found`)
return {
...user,
defaultWorkspace
}

return user
}
3 changes: 2 additions & 1 deletion apps/api/src/common/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UserAuthenticatedResponse } from '@/auth/auth.types'
import { UserWithWorkspace } from '@/user/user.types'
import { Otp, PrismaClient, User } from '@prisma/client'
import { Response } from 'express'

Expand Down Expand Up @@ -26,7 +27,7 @@ export const limitMaxItemsPerPage = (
export const setCookie = (
response: Response,
data: UserAuthenticatedResponse
): User => {
): UserWithWorkspace => {
const { token, ...user } = data
response.cookie('token', `Bearer ${token}`, {
domain: process.env.DOMAIN ?? 'localhost',
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/decorators/user.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { UserWithWorkspace } from '@/user/user.types'
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { User as DBUser } from '@prisma/client'

export const CurrentUser = createParamDecorator<
unknown,
ExecutionContext,
DBUser
UserWithWorkspace
>((_: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.user
Expand Down
16 changes: 10 additions & 6 deletions apps/api/src/user/controller/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import {
} from '@nestjs/common'
import { UserService } from '../service/user.service'
import { CurrentUser } from '@/decorators/user.decorator'
import { Authority, User } from '@prisma/client'
import { Authority } from '@prisma/client'
import { UpdateUserDto } from '../dto/update.user/update.user'
import { AdminGuard } from '@/auth/guard/admin/admin.guard'
import { CreateUserDto } from '../dto/create.user/create.user'
import { BypassOnboarding } from '@/decorators/bypass-onboarding.decorator'
import { RequiredApiKeyAuthorities } from '@/decorators/required-api-key-authorities.decorator'
import { ForbidApiKey } from '@/decorators/forbid-api-key.decorator'
import { UserWithWorkspace } from '../user.types'

@Controller('user')
export class UserController {
Expand All @@ -27,21 +28,24 @@ export class UserController {
@Get()
@BypassOnboarding()
@RequiredApiKeyAuthorities(Authority.READ_SELF)
async getCurrentUser(@CurrentUser() user: User) {
async getCurrentUser(@CurrentUser() user: UserWithWorkspace) {
return this.userService.getSelf(user)
}

@Put()
@BypassOnboarding()
@RequiredApiKeyAuthorities(Authority.UPDATE_SELF)
async updateSelf(@CurrentUser() user: User, @Body() dto: UpdateUserDto) {
async updateSelf(
@CurrentUser() user: UserWithWorkspace,
@Body() dto: UpdateUserDto
) {
return await this.userService.updateSelf(user, dto)
}

@Delete()
@HttpCode(204)
@ForbidApiKey()
async deleteSelf(@CurrentUser() user: User) {
async deleteSelf(@CurrentUser() user: UserWithWorkspace) {
await this.userService.deleteSelf(user)
}

Expand Down Expand Up @@ -87,14 +91,14 @@ export class UserController {

@Post('validate-email-change-otp')
async validateEmailChangeOtp(
@CurrentUser() user: User,
@CurrentUser() user: UserWithWorkspace,
@Query('otp') otp: string
) {
return await this.userService.validateEmailChangeOtp(user, otp.trim())
}

@Post('resend-email-change-otp')
async resendEmailChangeOtp(@CurrentUser() user: User) {
async resendEmailChangeOtp(@CurrentUser() user: UserWithWorkspace) {
return await this.userService.resendEmailChangeOtp(user)
}
}
Loading

0 comments on commit 2f4edec

Please sign in to comment.