Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add an ability to delete personal account gf-146 #185

Merged
merged 25 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8eb341b
feat: add delete query gf-146
HoroshkoMykhailo Sep 4, 2024
26a6e3a
feat: add service delete gf-146
HoroshkoMykhailo Sep 4, 2024
fe7f759
feat: add delete route gf-146
HoroshkoMykhailo Sep 4, 2024
d2343a5
feat: add soft delete gf-146
HoroshkoMykhailo Sep 4, 2024
3deecb3
feat: add delete account styles gf-146
HoroshkoMykhailo Sep 4, 2024
ced66a8
feat: add confirmation modal gf-146
HoroshkoMykhailo Sep 4, 2024
c1cf63c
feat: add deletion user after confirmation gf-146
HoroshkoMykhailo Sep 4, 2024
38f0171
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 4, 2024
c0eea80
feat: fix redirection to sign-in page gf-146
HoroshkoMykhailo Sep 4, 2024
9c73b5b
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 4, 2024
0fe2bbb
fix: modal props gf-146
HoroshkoMykhailo Sep 4, 2024
2ec92ae
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 4, 2024
3a19199
fix: suggested fixes gf-146
HoroshkoMykhailo Sep 4, 2024
0de15be
fix: confirmation modal design gf-146
HoroshkoMykhailo Sep 4, 2024
4d56796
fix: remove nothing_deleted constant gf-146
HoroshkoMykhailo Sep 5, 2024
8acbecf
refactor: change delete success status code gf-146
HoroshkoMykhailo Sep 5, 2024
1dfab12
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 5, 2024
d678943
fix: delete two findByEmail gf-146
HoroshkoMykhailo Sep 5, 2024
303c5da
feat: small package-lock.json addition gf-146
HoroshkoMykhailo Sep 5, 2024
94c7d6f
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 5, 2024
a07782e
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 5, 2024
6cc38b9
Merge branch 'main' into 146-feat-delete-personal-account
HoroshkoMykhailo Sep 5, 2024
1911604
fix: add type casting in profile gf-146
HoroshkoMykhailo Sep 5, 2024
d98a067
refactor: change include deleted gf-146
HoroshkoMykhailo Sep 5, 2024
7ab8487
Merge branch 'main' into 146-feat-delete-personal-account
liza-veis Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type Knex } from "knex";

const TABLE_NAME = "users";

const ColumnName = {
DELETED_AT: "deleted_at",
} as const;

function up(knex: Knex): Promise<void> {
return knex.schema.alterTable(TABLE_NAME, (table) => {
table.dateTime(ColumnName.DELETED_AT).nullable();
});
}

function down(knex: Knex): Promise<void> {
return knex.schema.alterTable(TABLE_NAME, (table) => {
table.dropColumn(ColumnName.DELETED_AT);
});
}

export { down, up };
40 changes: 40 additions & 0 deletions apps/backend/src/modules/users/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,46 @@ class UserController extends BaseController {
body: userPatchValidationSchema,
},
});

this.addRoute({
handler: (options) =>
this.delete(
options as APIHandlerOptions<{
params: { id: string };
}>,
),
method: "DELETE",
path: UsersApiPath.$ID,
});
}

/**
* @swagger
* /users/{id}:
* delete:
* tags:
* - Users
* description: Deletes a user
* parameters:
* - in: path
* name: id
* description: ID of the user to delete
* required: true
* schema:
* type: string
* responses:
* 204:
* description: User deleted successfully
*/
private async delete(
options: APIHandlerOptions<{
params: { id: string };
}>,
): Promise<APIHandlerResponse> {
return {
payload: await this.userService.delete(Number(options.params.id)),
status: HTTPCode.OK,
Fjortis marked this conversation as resolved.
Show resolved Hide resolved
};
}

/**
Expand Down
9 changes: 9 additions & 0 deletions apps/backend/src/modules/users/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { type UserAuthResponseDto } from "./libs/types/types.js";
class UserEntity implements Entity {
private createdAt: null | string;

private deletedAt: null | string;

private email: string;

private id: null | number;
Expand All @@ -17,13 +19,15 @@ class UserEntity implements Entity {

private constructor({
createdAt,
deletedAt,
email,
id,
name,
passwordHash,
passwordSalt,
}: {
createdAt: null | string;
deletedAt: null | string;
email: string;
id: null | number;
name: string;
Expand All @@ -36,17 +40,20 @@ class UserEntity implements Entity {
this.passwordHash = passwordHash;
this.passwordSalt = passwordSalt;
this.createdAt = createdAt;
this.deletedAt = deletedAt;
}

public static initialize({
createdAt,
deletedAt,
email,
id,
name,
passwordHash,
passwordSalt,
}: {
createdAt: string;
deletedAt: null | string;
email: string;
id: number;
name: string;
Expand All @@ -55,6 +62,7 @@ class UserEntity implements Entity {
}): UserEntity {
return new UserEntity({
createdAt,
deletedAt,
email,
id,
name,
Expand All @@ -76,6 +84,7 @@ class UserEntity implements Entity {
}): UserEntity {
return new UserEntity({
createdAt: null,
deletedAt: null,
email,
id: null,
name,
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/modules/users/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
} from "~/libs/modules/database/database.js";

class UserModel extends AbstractModel {
public deletedAt!: null | string;

public email!: string;

public name!: string;
Expand Down
26 changes: 22 additions & 4 deletions apps/backend/src/modules/users/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ class UserRepository implements Repository {
return UserEntity.initialize(user);
}

public delete(): ReturnType<Repository["delete"]> {
return Promise.resolve(true);
public async delete(id: number): Promise<boolean> {
const deletedRowsCount = await this.userModel
.query()
.patch({ deletedAt: new Date().toISOString() })
.where({ id })
.whereNull("deletedAt")
.execute();

return Boolean(deletedRowsCount);
}

public async find(id: number): Promise<null | UserEntity> {
const user = await this.userModel
.query()
.findById(id)
.whereNull("deletedAt")
.returning("*")
.execute();

Expand All @@ -51,6 +59,7 @@ class UserRepository implements Repository {
}: PaginationQueryParameters): Promise<PaginationResponseDto<UserEntity>> {
const { results, total } = await this.userModel
.query()
.whereNull("deletedAt")
.page(page, pageSize);

return {
Expand All @@ -59,8 +68,17 @@ class UserRepository implements Repository {
};
}

public async findByEmail(email: string): Promise<null | UserEntity> {
const user = await this.userModel.query().findOne({ email });
public async findByEmail(
liza-veis marked this conversation as resolved.
Show resolved Hide resolved
email: string,
hasDeleted = false,
): Promise<null | UserEntity> {
const query = this.userModel.query().findOne({ email });

if (!hasDeleted) {
query.whereNull("deletedAt");
}

const user = await query.execute();

return user ? UserEntity.initialize(user) : null;
}
Expand Down
17 changes: 13 additions & 4 deletions apps/backend/src/modules/users/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class UserService implements Service {
payload: UserSignUpRequestDto,
): Promise<UserAuthResponseDto> {
const { email, name, password } = payload;
const existingUser = await this.userRepository.findByEmail(email);
const existingUser = await this.userRepository.findByEmail(email, true);

if (existingUser) {
throw new UserError({
Expand All @@ -55,8 +55,17 @@ class UserService implements Service {
return item.toObject();
}

public delete(): ReturnType<Service["delete"]> {
return Promise.resolve(true);
public async delete(id: number): Promise<boolean> {
const isDeleted = await this.userRepository.delete(id);

if (!isDeleted) {
throw new UserError({
message: ExceptionMessage.USER_NOT_FOUND,
status: HTTPCode.NOT_FOUND,
});
}

return isDeleted;
}

public async find(id: number): Promise<UserAuthResponseDto> {
Expand Down Expand Up @@ -91,7 +100,7 @@ class UserService implements Service {

if (!item) {
throw new UserError({
message: ExceptionMessage.USER_NOT_FOUND,
message: ExceptionMessage.INVALID_CREDENTIALS,
status: HTTPCode.NOT_FOUND,
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.confirmation-text {
padding: 24px 0;
margin: 0;
font-size: 16px;
font-weight: 400;
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/libs/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Header = (): JSX.Element => {

return (
<header className={styles["header"]}>
<NavLink className={styles["logo-link"] ?? ""} to={AppRoute.ROOT}>
<NavLink className={styles["logo-link"] as string} to={AppRoute.ROOT}>
<div className={styles["logo-container"]}>
<img alt="GitFit logo" className={styles["logo-img"]} src={logoSrc} />
<span className={styles["logo-text"]}>Logo</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ const UserPopover = ({
<p className={styles["user-email"]}>{email}</p>
</div>
<div className={styles["buttons"]}>
<NavLink className={styles["button"] ?? ""} to={AppRoute.PROFILE}>
<NavLink
className={styles["button"] as string}
to={AppRoute.PROFILE}
>
Profile
</NavLink>
<button className={styles["button"]} onClick={handleLogout}>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/libs/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Properties = {
};

const Link = ({ children, to }: Properties): JSX.Element => (
<NavLink className={styles["link"] ?? ""} to={to}>
<NavLink className={styles["link"] as string} to={to}>
{children}
</NavLink>
);
Expand Down
12 changes: 6 additions & 6 deletions apps/frontend/src/libs/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ const Modal = ({
ref={dialogReference}
>
<div className={styles["modal-content"]}>
<div className={styles["modal-header-title"]}>
<h3>{title}</h3>
<div className={styles["modal-close"]}>
<IconButton iconName="cross" label="Close" onClick={onClose} />
</div>
<div className={styles["modal-close"]}>
<IconButton iconName="cross" label="Close" onClick={onClose} />
</div>
<div className={styles["modal-body"]}>
<h3 className={styles["modal-header-title"]}>{title}</h3>
{children}
</div>
<div className={styles["modal-body"]}>{children}</div>
</div>
</dialog>
</>
Expand Down
12 changes: 2 additions & 10 deletions apps/frontend/src/libs/components/modal/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,17 @@
transform: translate(-50%, -50%);
}

.modal-header {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
}

.modal-header-title {
font-family: Inter, sans-serif;
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 1.3;
color: var(--color-text-primary);
}

.modal-body {
display: flex;
flex-direction: column;
gap: 8px;
gap: 24px;
width: 100%;
font-family: Inter, sans-serif;
font-size: 16px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const ToastContainer = (): JSX.Element => {
return (
<LibraryToastContainer
autoClose={5000}
className={styles["toast-container"] ?? ""}
className={styles["toast-container"] as string}
closeOnClick
draggable
hideProgressBar={false}
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/libs/enums/notification-message.enum.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const NotificationMessage = {
PROJECT_CREATE_SUCCESS: "Project was successfully created.",
PROFILE_UPDATE_SUCCESS: "Successfully updated profile information.",
PROJECT_CREATE_SUCCESS: "Project was successfully created",
PROJECT_DELETE_SUCCESS: "Project was successfully deleted.",
PROJECT_UPDATE_SUCCESS: "Project was successfully updated.",
SUCCESS_PROFILE_UPDATE: "Your profile has been successfully updated.",
USER_DELETE_SUCCESS: "User deleted successfully.",
} as const;

export { NotificationMessage };
7 changes: 6 additions & 1 deletion apps/frontend/src/libs/hooks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export {
useController as useFormController,
useWatch as useFormWatch,
} from "react-hook-form";
export { useLocation, useParams, useSearchParams } from "react-router-dom";
export {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
2 changes: 1 addition & 1 deletion apps/frontend/src/libs/modules/api/base-http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class BaseHTTPApi implements HTTPApi {
if (hasAuth) {
const token = await this.storage.get<string>(StorageKey.TOKEN);

headers.append(HTTPHeader.AUTHORIZATION, `Bearer ${token ?? ""}`);
headers.append(HTTPHeader.AUTHORIZATION, `Bearer ${token as string}`);
}

return headers;
Expand Down
16 changes: 14 additions & 2 deletions apps/frontend/src/modules/users/slices/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ import {

import { name as sliceName } from "./users.slice.js";

const deleteById = createAsyncThunk<number, number, AsyncThunkConfig>(
`${sliceName}/delete`,
async (userId, { extra }) => {
const { toastNotifier, userApi } = extra;

await userApi.delete(userId);
toastNotifier.showSuccess(NotificationMessage.USER_DELETE_SUCCESS);

return userId;
},
);

const loadAll = createAsyncThunk<
UserGetAllResponseDto,
PaginationQueryParameters,
Expand All @@ -34,9 +46,9 @@ const updateProfile = createAsyncThunk<
const user = await userApi.patch(id, payload);
void dispatch(authActions.getAuthenticatedUser());

toastNotifier.showSuccess(NotificationMessage.SUCCESS_PROFILE_UPDATE);
toastNotifier.showSuccess(NotificationMessage.PROFILE_UPDATE_SUCCESS);

return user;
});

export { loadAll, updateProfile };
export { deleteById, loadAll, updateProfile };
Loading