From 92376a5ae54f51de25c11e5fd3d4d2c20720abd4 Mon Sep 17 00:00:00 2001 From: Yourim Cha <81357083+chacha912@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:13:34 +0900 Subject: [PATCH] Add account deletion feature (#166) Co-authored-by: Youngteac Hong --- .prettierrc.js | 3 +- src/api/index.ts | 5 + src/api/yorkie/v1/admin.proto | 40 ++++ src/api/yorkie/v1/admin_connect.d.ts | 38 +++- src/api/yorkie/v1/admin_connect.js | 38 +++- src/api/yorkie/v1/admin_pb.d.ts | 212 ++++++++++++++++++ src/api/yorkie/v1/admin_pb.js | 81 +++++++ src/assets/styles/components/button.scss | 1 + src/assets/styles/components/input.scss | 15 +- .../styles/pages/admin_setting_account.scss | 2 +- src/components/Input/InputTextBox.tsx | 3 + src/components/Modal/Modal.tsx | 11 +- src/features/users/AccountDropdown.tsx | 2 +- src/features/users/DangerZone.tsx | 125 +++++++++++ src/features/users/MobileGnbDropdown.tsx | 2 +- src/features/users/usersSlice.ts | 42 ++++ src/pages/SettingsPage.tsx | 18 +- 17 files changed, 624 insertions(+), 14 deletions(-) create mode 100644 src/features/users/DangerZone.tsx diff --git a/.prettierrc.js b/.prettierrc.js index b422419..136379f 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,7 +1,8 @@ -module.exports = { +const config = { bracketSpacing: true, singleQuote: true, trailingComma: 'all', printWidth: 120, tabWidth: 2, }; +export default config; diff --git a/src/api/index.ts b/src/api/index.ts index b7bff02..2c3361e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -52,6 +52,11 @@ export async function signUp(username: string, password: string): Promise return converter.fromUser(res.user!); } +// deleteAccount deletes the account. +export async function deleteAccount(username: string, password: string) { + await client.deleteAccount({ username, password }); +} + // createProject creates a new project. export async function createProject(name: string): Promise { const res = await client.createProject({ name }); diff --git a/src/api/yorkie/v1/admin.proto b/src/api/yorkie/v1/admin.proto index 76500de..5a57d1f 100644 --- a/src/api/yorkie/v1/admin.proto +++ b/src/api/yorkie/v1/admin.proto @@ -28,6 +28,8 @@ option java_package = "dev.yorkie.api.v1"; service AdminService { rpc SignUp(SignUpRequest) returns (SignUpResponse) {} rpc LogIn(LogInRequest) returns (LogInResponse) {} + rpc DeleteAccount(DeleteAccountRequest) returns (DeleteAccountResponse) {} + rpc ChangePassword(ChangePasswordRequest) returns (ChangePasswordResponse) {} rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse) {} rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) {} @@ -36,11 +38,14 @@ service AdminService { rpc ListDocuments (ListDocumentsRequest) returns (ListDocumentsResponse) {} rpc GetDocument (GetDocumentRequest) returns (GetDocumentResponse) {} + rpc GetDocuments (GetDocumentsRequest) returns (GetDocumentsResponse) {} rpc RemoveDocumentByAdmin (RemoveDocumentByAdminRequest) returns (RemoveDocumentByAdminResponse) {} rpc GetSnapshotMeta (GetSnapshotMetaRequest) returns (GetSnapshotMetaResponse) {} rpc SearchDocuments (SearchDocumentsRequest) returns (SearchDocumentsResponse) {} rpc ListChanges (ListChangesRequest) returns (ListChangesResponse) {} + + rpc GetServerVersion (GetServerVersionRequest) returns (GetServerVersionResponse) {} } message SignUpRequest { @@ -61,6 +66,23 @@ message LogInResponse { string token = 1; } +message DeleteAccountRequest { + string username = 1; + string password = 2; +} + +message DeleteAccountResponse { +} + +message ChangePasswordRequest { + string username = 1; + string current_password = 2; + string new_password = 3; +} + +message ChangePasswordResponse { +} + message CreateProjectRequest { string name = 1; } @@ -113,6 +135,16 @@ message GetDocumentResponse { DocumentSummary document = 1; } +message GetDocumentsRequest { + string project_name = 1; + repeated string document_keys = 2; + bool include_snapshot = 3; +} + +message GetDocumentsResponse { + repeated DocumentSummary documents = 1; +} + message RemoveDocumentByAdminRequest { string project_name = 1; string document_key = 2; @@ -154,3 +186,11 @@ message ListChangesRequest { message ListChangesResponse { repeated Change changes = 1; } + +message GetServerVersionRequest {} + +message GetServerVersionResponse { + string yorkie_version = 1; + string go_version = 2; + string build_date = 3; +} diff --git a/src/api/yorkie/v1/admin_connect.d.ts b/src/api/yorkie/v1/admin_connect.d.ts index a09f24e..b917390 100644 --- a/src/api/yorkie/v1/admin_connect.d.ts +++ b/src/api/yorkie/v1/admin_connect.d.ts @@ -18,7 +18,7 @@ /* eslint-disable */ // @ts-nocheck -import { CreateProjectRequest, CreateProjectResponse, GetDocumentRequest, GetDocumentResponse, GetProjectRequest, GetProjectResponse, GetSnapshotMetaRequest, GetSnapshotMetaResponse, ListChangesRequest, ListChangesResponse, ListDocumentsRequest, ListDocumentsResponse, ListProjectsRequest, ListProjectsResponse, LogInRequest, LogInResponse, RemoveDocumentByAdminRequest, RemoveDocumentByAdminResponse, SearchDocumentsRequest, SearchDocumentsResponse, SignUpRequest, SignUpResponse, UpdateProjectRequest, UpdateProjectResponse } from "./admin_pb.js"; +import { ChangePasswordRequest, ChangePasswordResponse, CreateProjectRequest, CreateProjectResponse, DeleteAccountRequest, DeleteAccountResponse, GetDocumentRequest, GetDocumentResponse, GetDocumentsRequest, GetDocumentsResponse, GetProjectRequest, GetProjectResponse, GetServerVersionRequest, GetServerVersionResponse, GetSnapshotMetaRequest, GetSnapshotMetaResponse, ListChangesRequest, ListChangesResponse, ListDocumentsRequest, ListDocumentsResponse, ListProjectsRequest, ListProjectsResponse, LogInRequest, LogInResponse, RemoveDocumentByAdminRequest, RemoveDocumentByAdminResponse, SearchDocumentsRequest, SearchDocumentsResponse, SignUpRequest, SignUpResponse, UpdateProjectRequest, UpdateProjectResponse } from "./admin_pb.js"; import { MethodKind } from "@bufbuild/protobuf"; /** @@ -47,6 +47,24 @@ export declare const AdminService: { readonly O: typeof LogInResponse, readonly kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.DeleteAccount + */ + readonly deleteAccount: { + readonly name: "DeleteAccount", + readonly I: typeof DeleteAccountRequest, + readonly O: typeof DeleteAccountResponse, + readonly kind: MethodKind.Unary, + }, + /** + * @generated from rpc yorkie.v1.AdminService.ChangePassword + */ + readonly changePassword: { + readonly name: "ChangePassword", + readonly I: typeof ChangePasswordRequest, + readonly O: typeof ChangePasswordResponse, + readonly kind: MethodKind.Unary, + }, /** * @generated from rpc yorkie.v1.AdminService.CreateProject */ @@ -101,6 +119,15 @@ export declare const AdminService: { readonly O: typeof GetDocumentResponse, readonly kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.GetDocuments + */ + readonly getDocuments: { + readonly name: "GetDocuments", + readonly I: typeof GetDocumentsRequest, + readonly O: typeof GetDocumentsResponse, + readonly kind: MethodKind.Unary, + }, /** * @generated from rpc yorkie.v1.AdminService.RemoveDocumentByAdmin */ @@ -137,6 +164,15 @@ export declare const AdminService: { readonly O: typeof ListChangesResponse, readonly kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.GetServerVersion + */ + readonly getServerVersion: { + readonly name: "GetServerVersion", + readonly I: typeof GetServerVersionRequest, + readonly O: typeof GetServerVersionResponse, + readonly kind: MethodKind.Unary, + }, } }; diff --git a/src/api/yorkie/v1/admin_connect.js b/src/api/yorkie/v1/admin_connect.js index da95465..16f28be 100644 --- a/src/api/yorkie/v1/admin_connect.js +++ b/src/api/yorkie/v1/admin_connect.js @@ -18,7 +18,7 @@ /* eslint-disable */ // @ts-nocheck -import { CreateProjectRequest, CreateProjectResponse, GetDocumentRequest, GetDocumentResponse, GetProjectRequest, GetProjectResponse, GetSnapshotMetaRequest, GetSnapshotMetaResponse, ListChangesRequest, ListChangesResponse, ListDocumentsRequest, ListDocumentsResponse, ListProjectsRequest, ListProjectsResponse, LogInRequest, LogInResponse, RemoveDocumentByAdminRequest, RemoveDocumentByAdminResponse, SearchDocumentsRequest, SearchDocumentsResponse, SignUpRequest, SignUpResponse, UpdateProjectRequest, UpdateProjectResponse } from "./admin_pb.js"; +import { ChangePasswordRequest, ChangePasswordResponse, CreateProjectRequest, CreateProjectResponse, DeleteAccountRequest, DeleteAccountResponse, GetDocumentRequest, GetDocumentResponse, GetDocumentsRequest, GetDocumentsResponse, GetProjectRequest, GetProjectResponse, GetServerVersionRequest, GetServerVersionResponse, GetSnapshotMetaRequest, GetSnapshotMetaResponse, ListChangesRequest, ListChangesResponse, ListDocumentsRequest, ListDocumentsResponse, ListProjectsRequest, ListProjectsResponse, LogInRequest, LogInResponse, RemoveDocumentByAdminRequest, RemoveDocumentByAdminResponse, SearchDocumentsRequest, SearchDocumentsResponse, SignUpRequest, SignUpResponse, UpdateProjectRequest, UpdateProjectResponse } from "./admin_pb.js"; import { MethodKind } from "@bufbuild/protobuf"; /** @@ -47,6 +47,24 @@ export const AdminService = { O: LogInResponse, kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.DeleteAccount + */ + deleteAccount: { + name: "DeleteAccount", + I: DeleteAccountRequest, + O: DeleteAccountResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc yorkie.v1.AdminService.ChangePassword + */ + changePassword: { + name: "ChangePassword", + I: ChangePasswordRequest, + O: ChangePasswordResponse, + kind: MethodKind.Unary, + }, /** * @generated from rpc yorkie.v1.AdminService.CreateProject */ @@ -101,6 +119,15 @@ export const AdminService = { O: GetDocumentResponse, kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.GetDocuments + */ + getDocuments: { + name: "GetDocuments", + I: GetDocumentsRequest, + O: GetDocumentsResponse, + kind: MethodKind.Unary, + }, /** * @generated from rpc yorkie.v1.AdminService.RemoveDocumentByAdmin */ @@ -137,6 +164,15 @@ export const AdminService = { O: ListChangesResponse, kind: MethodKind.Unary, }, + /** + * @generated from rpc yorkie.v1.AdminService.GetServerVersion + */ + getServerVersion: { + name: "GetServerVersion", + I: GetServerVersionRequest, + O: GetServerVersionResponse, + kind: MethodKind.Unary, + }, } }; diff --git a/src/api/yorkie/v1/admin_pb.d.ts b/src/api/yorkie/v1/admin_pb.d.ts index b93c629..96f1ccb 100644 --- a/src/api/yorkie/v1/admin_pb.d.ts +++ b/src/api/yorkie/v1/admin_pb.d.ts @@ -128,6 +128,107 @@ export declare class LogInResponse extends Message { static equals(a: LogInResponse | PlainMessage | undefined, b: LogInResponse | PlainMessage | undefined): boolean; } +/** + * @generated from message yorkie.v1.DeleteAccountRequest + */ +export declare class DeleteAccountRequest extends Message { + /** + * @generated from field: string username = 1; + */ + username: string; + + /** + * @generated from field: string password = 2; + */ + password: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.DeleteAccountRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DeleteAccountRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): DeleteAccountRequest; + + static fromJsonString(jsonString: string, options?: Partial): DeleteAccountRequest; + + static equals(a: DeleteAccountRequest | PlainMessage | undefined, b: DeleteAccountRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message yorkie.v1.DeleteAccountResponse + */ +export declare class DeleteAccountResponse extends Message { + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.DeleteAccountResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): DeleteAccountResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): DeleteAccountResponse; + + static fromJsonString(jsonString: string, options?: Partial): DeleteAccountResponse; + + static equals(a: DeleteAccountResponse | PlainMessage | undefined, b: DeleteAccountResponse | PlainMessage | undefined): boolean; +} + +/** + * @generated from message yorkie.v1.ChangePasswordRequest + */ +export declare class ChangePasswordRequest extends Message { + /** + * @generated from field: string username = 1; + */ + username: string; + + /** + * @generated from field: string current_password = 2; + */ + currentPassword: string; + + /** + * @generated from field: string new_password = 3; + */ + newPassword: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.ChangePasswordRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): ChangePasswordRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): ChangePasswordRequest; + + static fromJsonString(jsonString: string, options?: Partial): ChangePasswordRequest; + + static equals(a: ChangePasswordRequest | PlainMessage | undefined, b: ChangePasswordRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message yorkie.v1.ChangePasswordResponse + */ +export declare class ChangePasswordResponse extends Message { + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.ChangePasswordResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): ChangePasswordResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): ChangePasswordResponse; + + static fromJsonString(jsonString: string, options?: Partial): ChangePasswordResponse; + + static equals(a: ChangePasswordResponse | PlainMessage | undefined, b: ChangePasswordResponse | PlainMessage | undefined): boolean; +} + /** * @generated from message yorkie.v1.CreateProjectRequest */ @@ -441,6 +542,64 @@ export declare class GetDocumentResponse extends Message { static equals(a: GetDocumentResponse | PlainMessage | undefined, b: GetDocumentResponse | PlainMessage | undefined): boolean; } +/** + * @generated from message yorkie.v1.GetDocumentsRequest + */ +export declare class GetDocumentsRequest extends Message { + /** + * @generated from field: string project_name = 1; + */ + projectName: string; + + /** + * @generated from field: repeated string document_keys = 2; + */ + documentKeys: string[]; + + /** + * @generated from field: bool include_snapshot = 3; + */ + includeSnapshot: boolean; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.GetDocumentsRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetDocumentsRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetDocumentsRequest; + + static fromJsonString(jsonString: string, options?: Partial): GetDocumentsRequest; + + static equals(a: GetDocumentsRequest | PlainMessage | undefined, b: GetDocumentsRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message yorkie.v1.GetDocumentsResponse + */ +export declare class GetDocumentsResponse extends Message { + /** + * @generated from field: repeated yorkie.v1.DocumentSummary documents = 1; + */ + documents: DocumentSummary[]; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.GetDocumentsResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetDocumentsResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetDocumentsResponse; + + static fromJsonString(jsonString: string, options?: Partial): GetDocumentsResponse; + + static equals(a: GetDocumentsResponse | PlainMessage | undefined, b: GetDocumentsResponse | PlainMessage | undefined): boolean; +} + /** * @generated from message yorkie.v1.RemoveDocumentByAdminRequest */ @@ -688,3 +847,56 @@ export declare class ListChangesResponse extends Message { static equals(a: ListChangesResponse | PlainMessage | undefined, b: ListChangesResponse | PlainMessage | undefined): boolean; } +/** + * @generated from message yorkie.v1.GetServerVersionRequest + */ +export declare class GetServerVersionRequest extends Message { + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.GetServerVersionRequest"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetServerVersionRequest; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetServerVersionRequest; + + static fromJsonString(jsonString: string, options?: Partial): GetServerVersionRequest; + + static equals(a: GetServerVersionRequest | PlainMessage | undefined, b: GetServerVersionRequest | PlainMessage | undefined): boolean; +} + +/** + * @generated from message yorkie.v1.GetServerVersionResponse + */ +export declare class GetServerVersionResponse extends Message { + /** + * @generated from field: string yorkie_version = 1; + */ + yorkieVersion: string; + + /** + * @generated from field: string go_version = 2; + */ + goVersion: string; + + /** + * @generated from field: string build_date = 3; + */ + buildDate: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "yorkie.v1.GetServerVersionResponse"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): GetServerVersionResponse; + + static fromJson(jsonValue: JsonValue, options?: Partial): GetServerVersionResponse; + + static fromJsonString(jsonString: string, options?: Partial): GetServerVersionResponse; + + static equals(a: GetServerVersionResponse | PlainMessage | undefined, b: GetServerVersionResponse | PlainMessage | undefined): boolean; +} + diff --git a/src/api/yorkie/v1/admin_pb.js b/src/api/yorkie/v1/admin_pb.js index d6f8469..8b7b2ac 100644 --- a/src/api/yorkie/v1/admin_pb.js +++ b/src/api/yorkie/v1/admin_pb.js @@ -63,6 +63,45 @@ export const LogInResponse = /*@__PURE__*/ proto3.makeMessageType( ], ); +/** + * @generated from message yorkie.v1.DeleteAccountRequest + */ +export const DeleteAccountRequest = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.DeleteAccountRequest", + () => [ + { no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "password", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ], +); + +/** + * @generated from message yorkie.v1.DeleteAccountResponse + */ +export const DeleteAccountResponse = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.DeleteAccountResponse", + [], +); + +/** + * @generated from message yorkie.v1.ChangePasswordRequest + */ +export const ChangePasswordRequest = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.ChangePasswordRequest", + () => [ + { no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "current_password", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "new_password", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ], +); + +/** + * @generated from message yorkie.v1.ChangePasswordResponse + */ +export const ChangePasswordResponse = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.ChangePasswordResponse", + [], +); + /** * @generated from message yorkie.v1.CreateProjectRequest */ @@ -187,6 +226,28 @@ export const GetDocumentResponse = /*@__PURE__*/ proto3.makeMessageType( ], ); +/** + * @generated from message yorkie.v1.GetDocumentsRequest + */ +export const GetDocumentsRequest = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.GetDocumentsRequest", + () => [ + { no: 1, name: "project_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "document_keys", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 3, name: "include_snapshot", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ], +); + +/** + * @generated from message yorkie.v1.GetDocumentsResponse + */ +export const GetDocumentsResponse = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.GetDocumentsResponse", + () => [ + { no: 1, name: "documents", kind: "message", T: DocumentSummary, repeated: true }, + ], +); + /** * @generated from message yorkie.v1.RemoveDocumentByAdminRequest */ @@ -277,3 +338,23 @@ export const ListChangesResponse = /*@__PURE__*/ proto3.makeMessageType( ], ); +/** + * @generated from message yorkie.v1.GetServerVersionRequest + */ +export const GetServerVersionRequest = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.GetServerVersionRequest", + [], +); + +/** + * @generated from message yorkie.v1.GetServerVersionResponse + */ +export const GetServerVersionResponse = /*@__PURE__*/ proto3.makeMessageType( + "yorkie.v1.GetServerVersionResponse", + () => [ + { no: 1, name: "yorkie_version", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "go_version", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "build_date", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ], +); + diff --git a/src/assets/styles/components/button.scss b/src/assets/styles/components/button.scss index 1ff2c7c..110959d 100644 --- a/src/assets/styles/components/button.scss +++ b/src/assets/styles/components/button.scss @@ -243,6 +243,7 @@ $line: '.btn_line'; cursor: pointer; transition: background-color 0.2s; box-sizing: border-box; + white-space: nowrap; &:hover { background-color: var(--gray-100); diff --git a/src/assets/styles/components/input.scss b/src/assets/styles/components/input.scss index 164364c..89c20e3 100644 --- a/src/assets/styles/components/input.scss +++ b/src/assets/styles/components/input.scss @@ -109,6 +109,10 @@ } } + &.full_width { + width: 100%; + } + &.is_disabled { .label { color: var(--gray-300); @@ -518,7 +522,9 @@ margin: 3px 0 0 3px; border-radius: 50%; background-color: var(--gray-600); - transition: transform cubic-bezier(0.98, 0.02, 0.3, 1.52) 0.3s, background-color 0.2s; + transition: + transform cubic-bezier(0.98, 0.02, 0.3, 1.52) 0.3s, + background-color 0.2s; svg { display: block; @@ -527,7 +533,9 @@ padding: 2px; color: var(--gray-600); transform: scale(0.5); - transition: transform 0.3s 0.1s, color 0.3s 0.2s; + transition: + transform 0.3s 0.1s, + color 0.3s 0.2s; path { fill: currentColor; @@ -683,7 +691,8 @@ background-color: var(--blue-0); transform: scale(0.7); opacity: 0; - transition: transform cubic-bezier(0.98, 0.02, 0.3, 1.52) 0.2s 0.1s, + transition: + transform cubic-bezier(0.98, 0.02, 0.3, 1.52) 0.2s 0.1s, opacity cubic-bezier(0.98, 0.02, 0.3, 1.52) 0.2s 0.1s; svg { diff --git a/src/assets/styles/pages/admin_setting_account.scss b/src/assets/styles/pages/admin_setting_account.scss index 7ed11fc..054bd86 100644 --- a/src/assets/styles/pages/admin_setting_account.scss +++ b/src/assets/styles/pages/admin_setting_account.scss @@ -252,7 +252,7 @@ } } - .btn_close { + .setting_account_close { position: absolute; top: 0; right: 0; diff --git a/src/components/Input/InputTextBox.tsx b/src/components/Input/InputTextBox.tsx index d418736..7689438 100644 --- a/src/components/Input/InputTextBox.tsx +++ b/src/components/Input/InputTextBox.tsx @@ -28,6 +28,7 @@ type InputTextBoxProps = { helperText?: string; placeholder?: string; inputRef?: any; + fullWidth?: boolean; } & InputHTMLAttributes; export const InputTextBox = React.forwardRef((props: InputTextBoxProps, ref) => { @@ -45,12 +46,14 @@ function InputTextBoxInner({ helperText, placeholder, inputRef, + fullWidth, ...restProps }: InputTextBoxProps) { const inputTextBoxClassName = classNames('input_box', { is_disabled: state === 'disabled', is_error: state === 'error', is_success: state === 'success', + full_width: fullWidth, }); return ( diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 2c2f5f4..2c0c6fb 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -14,11 +14,18 @@ * limitations under the License. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Icon } from 'components'; import classNames from 'classnames'; export function Modal({ children, large }: { children: React.ReactNode; large?: boolean }) { + useEffect(() => { + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = 'auto'; + }; + }, []); + return ( <>
@@ -40,7 +47,7 @@ function Title({ children }: { children: React.ReactNode }) { } function Description({ children }: { children: React.ReactNode }) { - return

{children}

; + return

{children}

; } function Bottom({ children, combine }: { children: React.ReactNode; combine?: boolean }) { diff --git a/src/features/users/AccountDropdown.tsx b/src/features/users/AccountDropdown.tsx index b5f5ee6..604f82f 100644 --- a/src/features/users/AccountDropdown.tsx +++ b/src/features/users/AccountDropdown.tsx @@ -63,7 +63,7 @@ export function AccountDropdown() { - Settings + Account Settings Sign out diff --git a/src/features/users/DangerZone.tsx b/src/features/users/DangerZone.tsx new file mode 100644 index 0000000..a7d95d8 --- /dev/null +++ b/src/features/users/DangerZone.tsx @@ -0,0 +1,125 @@ +/* + * Copyright 2024 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState } from 'react'; +import { Button, Icon, Modal, InputTextBox } from 'components'; +import { deleteUser, selectUsers } from './usersSlice'; +import { useAppDispatch, useAppSelector } from 'app/hooks'; + +export function DangerZone() { + const dispatch = useAppDispatch(); + const { + username, + deleteAccount: { isSuccess, status, error }, + } = useAppSelector(selectUsers); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const handleChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + }; + const handleSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + dispatch(deleteUser({ username, password: inputValue })); + setInputValue(''); + }, + [dispatch, inputValue], + ); + + return ( +
+
+ Danger zone +
+
+
Delete Account
+
+

+ Permanently remove your account and all of its contents. +
+ There is no going back. Please continue with caution. +

+
+ +
+
+
+ {isModalOpen && ( + + + + +
+ + Delete your account + + This action cannot be undone. This will permanently delete your account and all of its contents. +
+
+ Please enter your password to confirm. +
+ +
+ + + + + + + { + setIsModalOpen(false); + }} + /> + +
+ )} +
+ ); +} diff --git a/src/features/users/MobileGnbDropdown.tsx b/src/features/users/MobileGnbDropdown.tsx index d7ee89c..269f983 100644 --- a/src/features/users/MobileGnbDropdown.tsx +++ b/src/features/users/MobileGnbDropdown.tsx @@ -64,7 +64,7 @@ export function MobileGnbDropdown() {
- Settings + Account Settings Sign out diff --git a/src/features/users/usersSlice.ts b/src/features/users/usersSlice.ts index 7f48108..2ab4924 100644 --- a/src/features/users/usersSlice.ts +++ b/src/features/users/usersSlice.ts @@ -41,6 +41,11 @@ export interface UsersState { status: 'idle' | 'loading' | 'failed'; error: Array | null; }; + deleteAccount: { + status: 'idle' | 'loading' | 'failed'; + isSuccess: boolean; + error: { message: string } | null; + }; preferences: { theme: { useSystem: boolean; @@ -86,6 +91,11 @@ const initialState: UsersState = { logout: { isSuccess: false, }, + deleteAccount: { + isSuccess: false, + status: 'idle', + error: null, + }, signup: { isSuccess: false, status: 'idle', @@ -123,6 +133,10 @@ export const signupUser = createAppThunk('users/signup', asy return await api.signUp(username, password); }); +export const deleteUser = createAppThunk('users/deleteAccount', async ({ username, password }) => { + return await api.deleteAccount(username, password); +}); + export const usersSlice = createSlice({ name: 'users', initialState, @@ -242,6 +256,34 @@ export const usersSlice = createSlice({ action.meta.isHandledError = true; } }); + builder.addCase(deleteUser.fulfilled, (state) => { + state.deleteAccount.status = 'idle'; + state.deleteAccount.isSuccess = true; + localStorage.removeItem('token'); + api.setToken(''); + state.token = ''; + state.isValidToken = false; + state.username = ''; + state.logout.isSuccess = true; + }); + builder.addCase(deleteUser.pending, (state) => { + state.deleteAccount.status = 'loading'; + }); + builder.addCase(deleteUser.rejected, (state, action) => { + state.deleteAccount.status = 'failed'; + const error = action.payload!.error; + if (!(error instanceof RPCError)) { + return; + } + const statusCode = Number(error.code); + if (statusCode === RPCStatusCode.NOT_FOUND || statusCode === RPCStatusCode.UNAUTHENTICATED) { + state.deleteAccount.error = { + message: 'Please verify your account information and try again.', + }; + action.meta.isHandledError = true; + return; + } + }); }, }); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ef079d6..c0180f3 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -19,6 +19,7 @@ import { useNavigate } from 'react-router-dom'; import { PageTemplate } from './PageTemplate'; import { Button, Icon, Navigator } from 'components'; import { Preferences } from 'features/users/Preferences'; +import { DangerZone } from 'features/users/DangerZone'; export function SettingsPage() { const navigate = useNavigate(); @@ -29,15 +30,26 @@ export function SettingsPage() { return (

- Settings + Account Settings

- +
+
-