feat: CRUD for oAuth users (#12853)
* user controller with oauth guard * crud repository and controller operations * add user controller to user module * refactor: oauth guard * connect user with oauth client upon creation * fix: test * wip: e2e test * refactor: findUniqueOrThrow -> findUnique user * test: POST user request * feat: permissions guard * e2e tests * e2e * e2e refactor * e2e refactor * reflector decorator check refactor * refactor oauth guard * remov unused imports * log message * delete permissions decorator and guard * remove delete user endpoint * remove delete user endpoint * refactor: route structure * remove get oauth client decorator * delete unecessary e2e config changes * remove set header in post test * fix: oauth guard test on empty db * revert: add previously removed constant to fulfill merge platform * fix: import to satisfy platform branch merge * use real implementation of access token guard * generate access & refresh tokens and fix e2e * fix: oauth client e2e test * refactor: variable naming * refactor * rename test file * remove oauth client from request
This commit is contained in:
parent
9ef24864b8
commit
e18eb6bc4b
|
@ -2,7 +2,8 @@
|
|||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"moduleNameMapper": {
|
||||
"@/(.*)": "<rootDir>/src/$1"
|
||||
"@/(.*)": "<rootDir>/src/$1",
|
||||
"test/(.*)": "<rootDir>/test/$1"
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
|
|
|
@ -9,7 +9,7 @@ export class AccessTokenGuard implements CanActivate {
|
|||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
const bearer = authHeader.replace("Bearer ", "").trim();
|
||||
const bearer = authHeader?.replace("Bearer ", "").trim();
|
||||
if (!bearer) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ export class OrganizationRolesGuard implements CanActivate {
|
|||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requiredRoles = this.reflector.get(Roles, context.getHandler());
|
||||
if (!requiredRoles) {
|
||||
|
||||
if (!requiredRoles.length || !Object.keys(requiredRoles).length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiKeyService } from "@/modules/api-key/api-key.service";
|
||||
import { UserRepository } from "@/modules/user/user.repository";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import type { Request } from "express";
|
||||
|
||||
|
@ -31,6 +31,10 @@ export class ApiKeyAuthStrategy extends PassportStrategy(BaseStrategy, "api-key"
|
|||
}
|
||||
|
||||
const user = await this.userRepository.findById(apiKey.userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
this.success(user);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return this.error(error);
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import { AppModule } from "@/app.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient } from "@prisma/client";
|
||||
import { PlatformOAuthClient, Team } from "@prisma/client";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { OAuthClientGuard } from "./oauth-client.guard";
|
||||
|
||||
describe("OAuthClientGuard", () => {
|
||||
let guard: OAuthClientGuard;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let oauthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
|
@ -21,9 +24,11 @@ describe("OAuthClientGuard", () => {
|
|||
}).compile();
|
||||
|
||||
guard = module.get<OAuthClientGuard>(OAuthClientGuard);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
|
||||
const organizationId = 1;
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
|
@ -32,7 +37,7 @@ describe("OAuthClientGuard", () => {
|
|||
};
|
||||
const secret = "secret";
|
||||
|
||||
oauthClient = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
oauthClient = await oauthClientRepositoryFixture.create(organization.id, data, secret);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
|
@ -41,41 +46,46 @@ describe("OAuthClientGuard", () => {
|
|||
});
|
||||
|
||||
it("should return true if client ID and secret are valid", async () => {
|
||||
const mockContext = createMockExecutionContext({
|
||||
[X_CAL_CLIENT_ID]: oauthClient.id,
|
||||
[X_CAL_SECRET_KEY]: oauthClient.secret,
|
||||
});
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: oauthClient.secret },
|
||||
{ clientId: oauthClient.id }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if client ID is invalid", async () => {
|
||||
const mockContext = createMockExecutionContext({
|
||||
[X_CAL_CLIENT_ID]: "invalid id",
|
||||
[X_CAL_SECRET_KEY]: oauthClient.secret,
|
||||
});
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: oauthClient.secret },
|
||||
{ clientId: "invalid id" }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it("should return false if secret key is invalid", async () => {
|
||||
const mockContext = createMockExecutionContext({
|
||||
[X_CAL_CLIENT_ID]: oauthClient.id,
|
||||
[X_CAL_SECRET_KEY]: "invalid secret",
|
||||
});
|
||||
const mockContext = createMockExecutionContext(
|
||||
{ [X_CAL_SECRET_KEY]: "invalid secret" },
|
||||
{ clientId: oauthClient.id }
|
||||
);
|
||||
|
||||
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oauthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(headers: Record<string, string>): ExecutionContext {
|
||||
function createMockExecutionContext(
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, string>
|
||||
): ExecutionContext {
|
||||
return createMock<ExecutionContext>({
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers,
|
||||
params,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientGuard implements CanActivate {
|
||||
constructor(private readonly oauthRepository: OAuthClientRepository) {}
|
||||
|
||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { headers } = request;
|
||||
const { headers, params } = request;
|
||||
|
||||
const oauthClientId = headers[X_CAL_CLIENT_ID];
|
||||
const oauthClientId = params.clientId;
|
||||
const oauthClientSecret = headers[X_CAL_SECRET_KEY];
|
||||
|
||||
return this.validateOauthClient(oauthClientId, oauthClientSecret);
|
||||
}
|
||||
if (!oauthClientId) {
|
||||
throw new UnauthorizedException("Missing client ID");
|
||||
}
|
||||
|
||||
private async validateOauthClient(oauthClientId: string, oauthClientSecret: string): Promise<boolean> {
|
||||
const oauthClient = await this.oauthRepository.getOAuthClient(oauthClientId);
|
||||
if (!oauthClientSecret) {
|
||||
throw new UnauthorizedException("Missing client secret");
|
||||
}
|
||||
|
||||
if (!oauthClient || oauthClient.secret !== oauthClientSecret) {
|
||||
return false;
|
||||
const client = await this.oauthRepository.getOAuthClient(oauthClientId);
|
||||
|
||||
if (!client || client.secret !== oauthClientSecret) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { getEnv } from "@/env";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { MembershipModule } from "@/modules/membership/membership.module";
|
||||
import { OAuthFlowModule } from "@/modules/oauth/flow/oauth-flow.module";
|
||||
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
|
||||
import { OAuthClientController } from "@/modules/oauth/oauth-client.controller";
|
||||
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
|
||||
import { OAuthUserController } from "@/modules/oauth/user/oauth-user.controller";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UserModule } from "@/modules/user/user.module";
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
|
@ -17,9 +20,11 @@ import { JwtModule } from "@nestjs/jwt";
|
|||
UserModule,
|
||||
MembershipModule,
|
||||
JwtModule.register({ secret: getEnv("JWT_SECRET") }),
|
||||
OAuthFlowModule,
|
||||
TokensModule,
|
||||
],
|
||||
providers: [OAuthClientRepository, OAuthClientGuard],
|
||||
controllers: [OAuthClientController],
|
||||
controllers: [OAuthClientController, OAuthUserController],
|
||||
exports: [OAuthClientRepository, OAuthClientGuard],
|
||||
})
|
||||
export class OAuthClientModule {}
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { CreateUserResponse } from "@/modules/oauth/user/oauth-user.controller";
|
||||
import { CreateUserInput } from "@/modules/user/input/create-user";
|
||||
import { UpdateUserInput } from "@/modules/user/input/update-user";
|
||||
import { UserModule } from "@/modules/user/user.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("User Endpoints", () => {
|
||||
describe("Not authenticated", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UserModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("secret header not set", () => {
|
||||
it(`/POST`, () => {
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/oauth-clients/100/users")
|
||||
.send({ email: "bob@gmail.com" })
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bearer access token not set", () => {
|
||||
it(`/GET/:id`, () => {
|
||||
return request(app.getHttpServer()).get("/api/v2/oauth-clients/100/users/200").expect(401);
|
||||
});
|
||||
it(`/PUT/:id`, () => {
|
||||
return request(app.getHttpServer()).put("/api/v2/oauth-clients/100/users/200").expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("User Authenticated", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
let postResponseData: CreateUserResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UserModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirect_uris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(oAuthClient).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/POST`, async () => {
|
||||
const requestBody: CreateUserInput = {
|
||||
email: "user-e2e-spec@gmail.com",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<{
|
||||
user: Omit<User, "password">;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}> = response.body;
|
||||
|
||||
postResponseData = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(requestBody.email);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(responseBody.data.user.email);
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(userEmail: string) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClient.id);
|
||||
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
|
||||
|
||||
expect(oAuthUsers?.length).toEqual(1);
|
||||
expect(newOAuthUser?.email).toEqual(userEmail);
|
||||
}
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<Omit<User, "password">> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.email).toEqual(postResponseData.user.email);
|
||||
});
|
||||
|
||||
it(`/PUT/:id`, async () => {
|
||||
const userUpdatedEmail = "pineapple-pizza@gmail.com";
|
||||
const body: UpdateUserInput = { email: userUpdatedEmail };
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.put(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
||||
.send(body)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<Omit<User, "password">> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.email).toEqual(userUpdatedEmail);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (postResponseData?.user.id) {
|
||||
await userRepositoryFixture.delete(postResponseData.user.id);
|
||||
}
|
||||
if (oAuthClient) {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
}
|
||||
if (organization) {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
}
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
import { AccessTokenGuard } from "@/modules/auth/guard/oauth/access-token.guard";
|
||||
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateUserInput } from "@/modules/user/input/create-user";
|
||||
import { UpdateUserInput } from "@/modules/user/input/update-user";
|
||||
import { UserRepository } from "@/modules/user/user.repository";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
Logger,
|
||||
UseGuards,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Put,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { DUPLICATE_RESOURCE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "oauth-clients/:clientId/users",
|
||||
version: "2",
|
||||
})
|
||||
export class OAuthUserController {
|
||||
private readonly logger = new Logger("UserController");
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly tokensRepository: TokensRepository
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async createUser(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Body() body: CreateUserInput
|
||||
): Promise<ApiResponse<CreateUserResponse>> {
|
||||
this.logger.log(
|
||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||
);
|
||||
|
||||
const existingUser = await this.userRepository.findByEmail(body.email);
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException(DUPLICATE_RESOURCE);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.create(body, oAuthClientId);
|
||||
const { access_token, refresh_token } = await this.tokensRepository.createOAuthTokens(
|
||||
oAuthClientId,
|
||||
user.id!
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
user,
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getUserById(@Param("userId") userId: number): Promise<ApiResponse<Partial<User>>> {
|
||||
const user = await this.userRepository.findById(userId);
|
||||
if (!user) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return { status: SUCCESS_STATUS, data: user };
|
||||
}
|
||||
|
||||
@Put("/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async updateUser(
|
||||
@Param("userId") userId: number,
|
||||
@Body() body: UpdateUserInput
|
||||
): Promise<ApiResponse<Partial<User>>> {
|
||||
this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`);
|
||||
|
||||
const user = await this.userRepository.update(userId, body);
|
||||
return { status: SUCCESS_STATUS, data: user };
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateUserResponse = { user: Partial<User>; accessToken: string; refreshToken: string };
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from "class-validator";
|
||||
|
||||
export class CreateUserInput {
|
||||
@IsString()
|
||||
email!: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class UpdateUserInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
}
|
|
@ -1,27 +1,67 @@
|
|||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { CreateUserInput } from "@/modules/user/input/create-user";
|
||||
import { UpdateUserInput } from "@/modules/user/input/update-user";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async create(user: CreateUserInput, oAuthClientId: string) {
|
||||
const newUser = await this.dbRead.prisma.user.create({
|
||||
data: {
|
||||
...user,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClientId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return this.sanitize(newUser);
|
||||
}
|
||||
|
||||
async findById(userId: number) {
|
||||
const user = await this.dbRead.prisma.user.findUniqueOrThrow({
|
||||
const user = await this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
return this.sanitize(user);
|
||||
|
||||
if (user) {
|
||||
return this.sanitize(user);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async findByEmail(email: string) {
|
||||
const user = await this.dbRead.prisma.user.findUniqueOrThrow({
|
||||
const user = await this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
return this.sanitize(user);
|
||||
|
||||
if (user) {
|
||||
return this.sanitize(user);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async update(userId: number, updateData: UpdateUserInput) {
|
||||
const updatedUser = await this.dbWrite.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: updateData,
|
||||
});
|
||||
return this.sanitize(updatedUser);
|
||||
}
|
||||
|
||||
async delete(userId: number): Promise<User> {
|
||||
return this.dbWrite.prisma.user.delete({
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
|
||||
sanitize(user: User): Partial<User> {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { CreateOAuthClientInput } from "@/modules/oauth/input/create-oauth-client";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
|
||||
export class OAuthClientRepositoryFixture {
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
@ -17,6 +18,17 @@ export class OAuthClientRepositoryFixture {
|
|||
return this.prismaReadClient.platformOAuthClient.findFirst({ where: { id: clientId } });
|
||||
}
|
||||
|
||||
async getUsers(clientId: PlatformOAuthClient["id"]) {
|
||||
const response = await this.prismaReadClient.platformOAuthClient.findFirst({
|
||||
where: { id: clientId },
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
|
||||
return response?.users;
|
||||
}
|
||||
|
||||
async create(organizationId: number, data: CreateOAuthClientInput, secret: string) {
|
||||
return this.prismaWriteClient.platformOAuthClient.create({
|
||||
data: {
|
||||
|
|
|
@ -213,7 +213,7 @@ describe("OAuth Client Endpoints", () => {
|
|||
});
|
||||
});
|
||||
it(`/DELETE/:id`, () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(204);
|
||||
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -295,7 +295,7 @@ describe("OAuth Client Endpoints", () => {
|
|||
});
|
||||
});
|
||||
it(`/DELETE/:id`, () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(204);
|
||||
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user