Skip to content

Commit

Permalink
Feat: PATCH /api/users with { nickname: string } (#150)
Browse files Browse the repository at this point in the history
# Background
Want to provider ways to have `nickname`, so I would like to offer an
endpoint to change the nickname.

## What's done

successful nickname change (i.e `my-nickname`):
<img width="1059" alt="image"
src="https://github.com/user-attachments/assets/34da97a2-2e07-49df-b182-5e9be915aff9">

if setting nickname is already set by yours:
<img width="760" alt="image"
src="https://github.com/user-attachments/assets/ac4e5b1e-a98b-4b8e-9a1c-bc33fd8b317d">

If nickname already exists:
<img width="602" alt="image"
src="https://github.com/user-attachments/assets/3635b85e-9b39-4c0a-b7ed-396e6a4f2fbb">

Returns the following if body is empty:
<img width="479" alt="image"
src="https://github.com/user-attachments/assets/9233aa8f-262e-4068-a8da-7c562a2b7938">


## Checklist Before PR Review
- [x] The following has been handled:
  -  `Draft` is set for this PR
  - `Title` is checked
  - `Background` is filled
  - `Assignee` is set
  - `Labels` are set
  - `development` is linked if related issue exists

## Checklist (Right Before PR Review Request)
- [x] The following has been handled:
  - Final Operation Check is done
  - Mobile View Operation Check is done
  - Make this PR as an open PR

## Checklist (Reviewers)
- [x] Check if there are any other missing TODOs that are not yet listed
- [x] Review Code
- [x] Every item on the checklist has been addressed accordingly
- [x] If `development` is associated to this PR, you must check if every
TODOs are handled
  • Loading branch information
mlajkim authored Dec 3, 2024
1 parent 825354e commit 6e42639
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 3 deletions.
18 changes: 17 additions & 1 deletion src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Controller, Get, Param } from '@nestjs/common'
import { Body, Controller, Get, Param, Patch, Req } from '@nestjs/common'
import { AjkTownApiVersion } from './index.interface'
import { RitualService } from '@/services/ritual.service'
import { UserService } from '@/services/user.service'
import { ActionGroupService } from '@/services/action-group.service'
import { JwtService } from '@nestjs/jwt'
import { PatchUserDTO } from '@/dto/patch-user.dto'
import { AccessTokenDomain } from '@/domains/auth/access-token.domain'
import { Request } from 'express'

/**
* Every endpoints of UserController is public and does not require any authentication.
Expand All @@ -11,13 +15,15 @@ import { ActionGroupService } from '@/services/action-group.service'
*/
export enum UserControllerPath {
GetUsers = `users`,
PatchUsers = `users`,
GetUserByNickname = `users/mlajkim`,
GetRitualsOfUserByNickname = `users/mlajkim/rituals`, // it is fixed to mlajkim as this point
GetActionGroupsOfUserById = `users/mlajkim/action-groups/:id`, // it is fixed to mlajkim as this point
}
@Controller(AjkTownApiVersion.V1)
export class UserController {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
private readonly ritualService: RitualService,
private readonly actionGroupService: ActionGroupService,
Expand All @@ -28,6 +34,16 @@ export class UserController {
return this.userService.getUsers()
}

@Patch(UserControllerPath.PatchUsers)
async patchUsers(@Body() body: PatchUserDTO, @Req() req: Request) {
return (
await this.userService.patchUser(
await AccessTokenDomain.fromReq(req, this.jwtService),
body,
)
).toResDTO()
}

@Get(UserControllerPath.GetUserByNickname)
async getUserByNickname() {
return (await this.userService.byNickname('mlajkim')).toSharedResDTO()
Expand Down
4 changes: 4 additions & 0 deletions src/domains/auth/oauth-payload.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ export class OauthPayloadDomain {
}
}

/**
* This method is used to create a new user from the OAuth payload from trusted provider like Google etc
*/
toUserModel(userModel: UserModel): UserDoc {
const props: UserProps = {
federalProvider: this.props.federalProvider,
federalID: this.props.federalId,
nickname: undefined, // nickname is first undefined, and users will be asked to set it later
firstName: this.props.firstName,
lastName: this.props.lastName,
email: this.props.userEmail,
Expand Down
6 changes: 4 additions & 2 deletions src/domains/ritual/ritual-group.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { GetRitualsRes } from '@/responses/get-ritual.res'
import { RitualDomain } from './ritual.domain'
import { UserDomain } from '../user/user.domain'
import { RitualModel } from '@/schemas/ritual.schema'
import { BadRequestError } from '@/errors/400/index.error'
import { NotExistOrNoPermissionError } from '@/errors/404/not-exist-or-no-permission.error'
import { ParentRitualDomain } from './parent-ritual.domain'
import { ArchiveModel } from '@/schemas/archive.schema'
import { ArchiveDomain } from '../archive/archive.domain'
import { GetRitualQueryDTO } from '@/dto/get-rituals-query.dto'
import { CriticalError } from '@/errors/500/critical.error'

export class RitualGroupDomain extends DomainRoot {
private readonly domains: ParentRitualDomain[]
Expand Down Expand Up @@ -43,7 +43,9 @@ export class RitualGroupDomain extends DomainRoot {

if (ritualDocs.length === 0) {
if (disableRecursion) {
throw new BadRequestError('Something went wrong critically')
throw new CriticalError(
'RitualDocs are still not found when recursion is disabled',
)
}

await RitualDomain.postDefault(atd, ritualModel)
Expand Down
1 change: 1 addition & 0 deletions src/domains/user/index.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DataBasicsDate } from 'src/global.interface'
// TODO: Modify this into a cleaner sense.
export interface IUser extends DataBasicsDate {
id: string
nickname: undefined | string // undefined if not yet set
federalProvider: 'google'
federalID: string // '1163553634208XXXXXXXX'
familyName: string
Expand Down
2 changes: 2 additions & 0 deletions src/domains/user/user.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class UserDomain {
return new UserDomain({
id: 'abc',
federalID: 'abc',
nickname: 'mlajkim',
givenName: 'AJ',
familyName: 'Kim',
email: '[email protected]',
Expand Down Expand Up @@ -89,6 +90,7 @@ export class UserDomain {
return new UserDomain({
id: props.id,
federalID: props.federalID,
nickname: props.nickname,
givenName: props.firstName,
familyName: props.lastName,
email: props.email,
Expand Down
6 changes: 6 additions & 0 deletions src/dto/patch-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsOptional } from 'class-validator'

export class PatchUserDTO {
@IsOptional()
nickname: string
}
9 changes: 9 additions & 0 deletions src/errors/500/critical.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InternalServerError } from './index.error'

/** Thrown when things should never happen.
*/
export class CriticalError extends InternalServerError {
constructor(reasonWhyCritical: string) {
super(`CRITICAL_ERROR: ${reasonWhyCritical}`) // i.e) User email is not present
}
}
3 changes: 3 additions & 0 deletions src/schemas/deprecated-user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export class UserProps {
@Prop()
federalID: string

@Prop()
nickname: undefined | string // undefined if not yet set

@Prop()
lastName: string

Expand Down
45 changes: 45 additions & 0 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AccessTokenDomain } from '@/domains/auth/access-token.domain'
import { UserDomain } from '@/domains/user/user.domain'
import { PatchUserDTO } from '@/dto/patch-user.dto'
import { BadRequestError } from '@/errors/400/index.error'
import { CriticalError } from '@/errors/500/critical.error'
import { envLambda } from '@/lambdas/get-env.lambda'
import { GetUsersRes } from '@/responses/get-users.res'
import { UserModel, UserProps } from '@/schemas/deprecated-user.schema'
Expand All @@ -26,6 +29,48 @@ export class UserService {
return { totalNumberOfUsers, lastFiveJoinedDate }
}

async patchUser(
atd: AccessTokenDomain,
body: PatchUserDTO,
): Promise<UserDomain> {
// if body length is 0, it is a bad request:
if (Object.keys(body).length === 0)
throw new BadRequestError('Body [PatchUserDTO] is empty')

// nicname update requires the following check:
// the nickname must be unique:

// TODO: Right now only can update the nickname, and therefore only check the nickname.
const users = await this.userModel.find({ nickname: body.nickname })
if (users.length > 1)
throw new CriticalError(
`Multiple users with nickname [${body.nickname}] found`,
)

if (users.length > 0) {
if (users[0].email === atd.email) {
throw new BadRequestError(
`Your nickname [${users[0].nickname}] is already set as [${body.nickname}]`,
)
}

throw new BadRequestError(`Nickname [${body.nickname}] already exists`)
}

// update based on the user's email, as email is the unique identifier
return UserDomain.fromMdb(
await this.userModel
.findOneAndUpdate(
{ email: atd.email },
{
nickname: body.nickname,
},
{ new: true },
)
.exec(),
)
}

/** Returns user by nickname */
async byNickname(nickname: string): Promise<UserDomain> {
// TODO: Only returns dev user domain at this point, if it is non-prod env. Fix it.
Expand Down

0 comments on commit 6e42639

Please sign in to comment.