Skip to content

Commit

Permalink
feat: Add create organization endpoint with user handling
Browse files Browse the repository at this point in the history
  • Loading branch information
PooyaRaki committed Feb 12, 2025
1 parent 04d8e53 commit 764c63a
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/domain/users/users.repository.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ export interface IUsersRepository {
}): Promise<void>;

findByWalletAddressOrFail(address: `0x${string}`): Promise<User>;

findByWalletAddress(address: `0x${string}`): Promise<User | undefined>;
}
25 changes: 15 additions & 10 deletions src/domain/users/users.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,23 @@ export class UsersRepository implements IUsersRepository {
public async findByWalletAddressOrFail(
address: `0x${string}`,
): Promise<User> {
try {
const { user } = await this.walletsRepository.findOneByAddressOrFail(
address,
{ user: true },
);
return user;
} catch (error) {
if (error instanceof NotFoundException) {
const user = await this.findByWalletAddress(address);

if (!user) {
throw new NotFoundException('User not found.');
}
throw error;
}

return user;
}

public async findByWalletAddress(
address: `0x${string}`,
): Promise<User | undefined> {
const wallet = await this.walletsRepository.findOneByAddress(address, {
user: true,
});

return wallet?.user;
}

private assertSignerAddress(
Expand Down
89 changes: 89 additions & 0 deletions src/routes/organizations/organizations.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,95 @@ describe('OrganizationController', () => {
});
});

describe('POST /v1/organizations/create-with-user', () => {
it('Should create an organization when user exists', async () => {
const authPayloadDto = authPayloadDtoBuilder().build();
const accessToken = jwtService.sign(authPayloadDto);
const organizationName = faker.company.name();

await request(app.getHttpServer())
.post('/v1/users/wallet')
.set('Cookie', [`access_token=${accessToken}`]);

await request(app.getHttpServer())
.post('/v1/organizations/create-with-user')
.set('Cookie', [`access_token=${accessToken}`])
.send({ name: organizationName })
.expect(201)
.expect(({ body }) =>
expect(body).toEqual({
id: expect.any(Number),
name: organizationName,
}),
);
});

it('Should create an organization with user does not exist', async () => {
const authPayloadDto = authPayloadDtoBuilder().build();
const accessToken = jwtService.sign(authPayloadDto);
const organizationName = faker.company.name();

await request(app.getHttpServer())
.post('/v1/organizations/create-with-user')
.set('Cookie', [`access_token=${accessToken}`])
.send({ name: organizationName })
.expect(201)
.expect(({ body }) =>
expect(body).toEqual({
id: expect.any(Number),
name: organizationName,
}),
);
});

it('should return a 403 if not authenticated', async () => {
await request(app.getHttpServer())
.post('/v1/organizations/create-with-user')
.expect(403)
.expect({
statusCode: 403,
message: 'Forbidden resource',
error: 'Forbidden',
});
});

it('Should return a 403 if the AuthPayload is empty', async () => {
const authPayloadDto = authPayloadDtoBuilder()
.with('signer_address', undefined as unknown as `0x${string}`)
.build();
const accessToken = jwtService.sign(authPayloadDto);

await request(app.getHttpServer())
.post('/v1/organizations/create-with-user')
.set('Cookie', [`access_token=${accessToken}`])
.expect(403)
.expect({
statusCode: 403,
message: 'Forbidden resource',
error: 'Forbidden',
});
});

it('Should return a 422 if no name is provided', async () => {
const authPayloadDto = authPayloadDtoBuilder().build();
const accessToken = jwtService.sign(authPayloadDto);

await request(app.getHttpServer())
.post('/v1/organizations/create-with-user')
.set('Cookie', [`access_token=${accessToken}`])
.send()
.expect(422)
.expect({
statusCode: 422,
code: 'invalid_type',
expected: 'object',
received: 'undefined',
path: [],
message: 'Required',
});
});
});

describe('GET /organizations', () => {
it('Should return a list of organizations', async () => {
const authPayloadDto = authPayloadDtoBuilder().build();
Expand Down
21 changes: 21 additions & 0 deletions src/routes/organizations/organizations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { ValidationPipe } from '@/validation/pipes/validation.pipe';
import { RowSchema } from '@/datasources/db/v1/entities/row.entity';
import { getEnumKey } from '@/domain/common/utils/enum';
import { UserStatus } from '@/domain/users/entities/user.entity';

@ApiTags('organizations')
@UseGuards(AuthGuard)
Expand Down Expand Up @@ -69,6 +70,26 @@ export class OrganizationsController {
});
}

@Post('/create-with-user')
@ApiOkResponse({
description: 'Organizations created',
type: CreateOrganizationResponse,
})
@ApiForbiddenResponse({ description: 'Forbidden resource' })
@ApiUnauthorizedResponse({ description: 'Signer address not provided' })
public async createWithUser(
@Body(new ValidationPipe(CreateOrganizationSchema))
body: CreateOrganizationDto,
@Auth() authPayload: AuthPayload,
): Promise<CreateOrganizationResponse> {
return await this.organizationsService.createWithUser({
authPayload,
name: body.name,
status: getEnumKey(OrganizationStatus, OrganizationStatus.ACTIVE),
userStatuus: getEnumKey(UserStatus, UserStatus.ACTIVE),
});
}

@Get()
@ApiOkResponse({
description: 'Organizations found',
Expand Down
32 changes: 32 additions & 0 deletions src/routes/organizations/organizations.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Organization } from '@/datasources/organizations/entities/organizations.entity.db';
import { User } from '@/datasources/users/entities/users.entity.db';
import type { AuthPayload } from '@/domain/auth/entities/auth-payload.entity';
import { getEnumKey } from '@/domain/common/utils/enum';
import { IOrganizationsRepository } from '@/domain/organizations/organizations.repository.interface';
import { UserOrganizationRole } from '@/domain/users/entities/user-organization.entity';
import { UserStatus } from '@/domain/users/entities/user.entity';

Check failure on line 7 in src/routes/organizations/organizations.service.ts

View workflow job for this annotation

GitHub Actions / es-lint

'UserStatus' is defined but never used

Check failure on line 7 in src/routes/organizations/organizations.service.ts

View workflow job for this annotation

GitHub Actions / es-lint

'UserStatus' is defined but never used
import { IUsersRepository } from '@/domain/users/users.repository.interface';
import { CreateOrganizationResponse } from '@/routes/organizations/entities/create-organization.dto.entity';
import type { GetOrganizationResponse } from '@/routes/organizations/entities/get-organization.dto.entity';
Expand Down Expand Up @@ -33,6 +35,36 @@ export class OrganizationsService {
return await this.organizationsRepository.create({ userId, ...args });
}

public async createWithUser(args: {
name: Organization['name'];
status: Organization['status'];
userStatuus: User['status'];
authPayload: AuthPayload;
}): Promise<CreateOrganizationResponse> {
this.assertSignerAddress(args.authPayload);
const user = await this.userRepository.findByWalletAddress(
args.authPayload.signer_address,
);

let userId: number;

if (user) {
userId = user.id;
} else {
const user = await this.userRepository.createWithWallet({
status: args.userStatuus,
authPayload: args.authPayload,
});

userId = user.id;
}

return await this.organizationsRepository.create({
userId,
...args,
});
}

public async get(
authPayload: AuthPayload,
): Promise<Array<GetOrganizationResponse>> {
Expand Down

0 comments on commit 764c63a

Please sign in to comment.