diff --git a/backend/package-lock.json b/backend/package-lock.json index 939d6a52..db091ad4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -31,8 +31,7 @@ "passport-github": "^1.1.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "slugify": "^1.6.6" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -11957,14 +11956,6 @@ "node": "*" } }, - "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", diff --git a/backend/package.json b/backend/package.json index 6e79f84e..7d7ea753 100644 --- a/backend/package.json +++ b/backend/package.json @@ -43,8 +43,7 @@ "passport-github": "^1.1.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "slugify": "^1.6.6" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/backend/src/check/check.service.ts b/backend/src/check/check.service.ts index 5fd6a0f0..8306887d 100644 --- a/backend/src/check/check.service.ts +++ b/backend/src/check/check.service.ts @@ -1,12 +1,11 @@ import { Injectable } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import * as moment from "moment"; import { PrismaService } from "src/db/prisma.service"; -import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; -import slugify from "slugify"; +import { JwtPayload } from "src/utils/types/jwt.type"; import { CheckYorkieDto, YorkieMethod } from "./dto/check-yorkie.dto"; +import { CheckNameConflicReponse } from "./types/check-name-conflict-response.type"; import { CheckYorkieResponse } from "./types/check-yorkie-response.type"; -import { JwtService } from "@nestjs/jwt"; -import { JwtPayload } from "src/utils/types/jwt.type"; -import * as moment from "moment"; @Injectable() export class CheckService { @@ -16,15 +15,15 @@ export class CheckService { ) {} async checkNameConflict(name: string): Promise { - const slug = slugify(name, { lower: true }); + const encodedText = encodeURIComponent(name); const conflictUserList = await this.prismaService.user.findMany({ where: { - OR: [{ nickname: name }, { nickname: slug }], + OR: [{ nickname: name }, { nickname: encodedText }], }, }); const conflictWorkspaceList = await this.prismaService.workspace.findMany({ where: { - OR: [{ title: name }, { title: slug }], + OR: [{ title: name }, { title: encodedText }], }, }); diff --git a/backend/src/users/dto/change-nickname.dto.ts b/backend/src/users/dto/change-nickname.dto.ts index ee610194..fa089c90 100644 --- a/backend/src/users/dto/change-nickname.dto.ts +++ b/backend/src/users/dto/change-nickname.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from "@nestjs/swagger"; +import { MinLength } from "class-validator"; export class ChangeNicknameDto { - @ApiProperty({ type: String, description: "Nickname of user to update" }) + @ApiProperty({ type: String, description: "Nickname of user to update", minLength: 2 }) + @MinLength(2) nickname: string; } diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 04de8452..c7d58a21 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -1,10 +1,9 @@ import { ConflictException, Injectable } from "@nestjs/common"; import { User } from "@prisma/client"; -import { PrismaService } from "src/db/prisma.service"; -import { FindUserResponse } from "./types/find-user-response.type"; import { CheckService } from "src/check/check.service"; -import slugify from "slugify"; +import { PrismaService } from "src/db/prisma.service"; import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; +import { FindUserResponse } from "./types/find-user-response.type"; @Injectable() export class UsersService { @@ -95,7 +94,7 @@ export class UsersService { }, }); - const slug = slugify(nickname, { lower: true }); + const encodedText = encodeURIComponent(nickname); if (!userWorkspaceList.length) { const { id: workspaceId } = await this.prismaService.workspace.create({ @@ -104,7 +103,7 @@ export class UsersService { }, data: { title: nickname, - slug, + slug: encodedText, }, }); diff --git a/backend/src/workspaces/dto/create-workspace.dto.ts b/backend/src/workspaces/dto/create-workspace.dto.ts index 2397bf18..c8c57a35 100644 --- a/backend/src/workspaces/dto/create-workspace.dto.ts +++ b/backend/src/workspaces/dto/create-workspace.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from "@nestjs/swagger"; +import { MinLength } from "class-validator"; export class CreateWorkspaceDto { - @ApiProperty({ description: "Title of project to create", type: String }) + @ApiProperty({ description: "Title of project to create", type: String, minLength: 2 }) + @MinLength(2) title: string; } diff --git a/backend/src/workspaces/workspaces.controller.ts b/backend/src/workspaces/workspaces.controller.ts index bce4f6b4..a1e6371d 100644 --- a/backend/src/workspaces/workspaces.controller.ts +++ b/backend/src/workspaces/workspaces.controller.ts @@ -9,8 +9,6 @@ import { Query, Req, } from "@nestjs/common"; -import { WorkspacesService } from "./workspaces.service"; -import { CreateWorkspaceDto } from "./dto/create-workspace.dto"; import { ApiBearerAuth, ApiBody, @@ -24,15 +22,17 @@ import { ApiTags, ApiUnauthorizedResponse, } from "@nestjs/swagger"; +import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type"; import { AuthroizedRequest } from "src/utils/types/req.type"; +import { CreateInvitationTokenDto } from "./dto/create-invitation-token.dto"; +import { CreateWorkspaceDto } from "./dto/create-workspace.dto"; +import { JoinWorkspaceDto } from "./dto/join-workspace.dto"; +import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type"; import { CreateWorkspaceResponse } from "./types/create-workspace-response.type"; import { FindWorkspaceResponse } from "./types/find-workspace-response.type"; -import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type"; import { FindWorkspacesResponse } from "./types/find-workspaces-response.type"; -import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type"; -import { JoinWorkspaceDto } from "./dto/join-workspace.dto"; import { JoinWorkspaceResponse } from "./types/join-workspace-response.type"; -import { CreateInvitationTokenDto } from "./dto/create-invitation-token.dto"; +import { WorkspacesService } from "./workspaces.service"; @ApiTags("Workspaces") @ApiBearerAuth() @@ -68,7 +68,7 @@ export class WorkspacesController { @Req() req: AuthroizedRequest, @Param("workspace_slug") workspaceSlug: string ): Promise { - return this.workspacesService.findOneBySlug(req.user.id, workspaceSlug); + return this.workspacesService.findOneBySlug(req.user.id, encodeURIComponent(workspaceSlug)); } @Get("") diff --git a/backend/src/workspaces/workspaces.service.ts b/backend/src/workspaces/workspaces.service.ts index 978f66d0..aca25e8f 100644 --- a/backend/src/workspaces/workspaces.service.ts +++ b/backend/src/workspaces/workspaces.service.ts @@ -5,14 +5,13 @@ import { UnauthorizedException, } from "@nestjs/common"; import { Prisma, Workspace } from "@prisma/client"; +import * as moment from "moment"; +import { CheckService } from "src/check/check.service"; import { PrismaService } from "src/db/prisma.service"; -import { FindWorkspacesResponse } from "./types/find-workspaces-response.type"; -import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type"; import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; -import slugify from "slugify"; import { generateRandomKey } from "src/utils/functions/random-string"; -import * as moment from "moment"; -import { CheckService } from "src/check/check.service"; +import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type"; +import { FindWorkspacesResponse } from "./types/find-workspaces-response.type"; @Injectable() export class WorkspacesService { @@ -31,7 +30,7 @@ export class WorkspacesService { const workspace = await this.prismaService.workspace.create({ data: { title, - slug: slugify(title, { lower: true }), + slug: encodeURIComponent(title), }, }); diff --git a/frontend/src/components/modals/CreateModal.tsx b/frontend/src/components/modals/CreateModal.tsx index eb37068b..2942f85b 100644 --- a/frontend/src/components/modals/CreateModal.tsx +++ b/frontend/src/components/modals/CreateModal.tsx @@ -1,3 +1,4 @@ +import CloseIcon from "@mui/icons-material/Close"; import { Button, FormControl, @@ -8,11 +9,10 @@ import { Stack, Typography, } from "@mui/material"; -import { FormContainer, TextFieldElement } from "react-hook-form-mui"; -import CloseIcon from "@mui/icons-material/Close"; import { useMemo, useState } from "react"; -import { useCheckNameConflictQuery } from "../../hooks/api/check"; +import { FormContainer, TextFieldElement } from "react-hook-form-mui"; import { useDebounce } from "react-use"; +import { useCheckNameConflictQuery } from "../../hooks/api/check"; interface CreateRequest { title: string; @@ -29,12 +29,16 @@ function CreateModal(props: CreateModalProps) { const [nickname, setNickname] = useState(""); const [debouncedNickname, setDebouncedNickname] = useState(""); const { data: conflictResult } = useCheckNameConflictQuery(debouncedNickname); + const errorMessage = useMemo(() => { + if (nickname.length < 2) { + return "Title must be at least 2 characters"; + } if (conflictResult?.conflict) { return "Already Exists"; } return null; - }, [conflictResult?.conflict]); + }, [nickname.length, conflictResult?.conflict]); useDebounce( () => { @@ -49,8 +53,10 @@ function CreateModal(props: CreateModalProps) { }; const handleCreate = async (data: CreateRequest) => { - await onSuccess(data); - handleCloseModal(); + if (data.title.length >= 2) { + await onSuccess(data); + handleCloseModal(); + } }; const handleNicknameChange = (e: React.ChangeEvent) => {