Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ForgotPasswordPage as default } from 'pages/forgot-password';
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ const eslintConfig = defineConfig([
'PASCAL_CASE',
'**/*{Error,Type,Types,Interface,Props,Dto,Response,Request,Contract,Contracts}.ts':
'PASCAL_CASE',
'**/!(*{Error,Type,Types,Interface,Props,Dto,Response,Request,Contract,Contracts}*).ts':
'**/!(*{Error,Type,Types,Interface,Props,Dto,Response,Request,Contract,Contracts,use}*).ts':
'KEBAB_CASE',
'**/*.{js,mjs,cjs,mts,cts}': 'KEBAB_CASE',
'**/use*.{ts,tsx}': 'CAMEL_CASE',
},
{
ignoreMiddleExtensions: true,
Expand Down
2 changes: 1 addition & 1 deletion infra/dev/compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ services:
hostname: api
container_name: api
platform: linux/amd64
image: ghcr.io/task-tracker-lab/backend:sha-9b66ff7
image: ghcr.io/task-tracker-lab/backend:dev
env_file:
- .env
ports:
Expand Down
89 changes: 89 additions & 0 deletions src/entities/auth/api/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { api } from 'shared/api';
import * as SAuth from '../model/schemas';
import * as TAuth from '../model/types';

export class AuthHttp {
static signin(data: TAuth.SigninBody) {
return api<TAuth.SigninResponse>({
url: '/auth/sign-in',
method: 'POST',
data: data,
skipAuthRefresh: true,
contracts: {
body: SAuth.SigninBody,
response: SAuth.SigninResponse,
},
});
}

static signout() {
return api<TAuth.SignoutResponse>({
url: '/auth/sign-out',
method: 'POST',
contracts: {
response: SAuth.SignoutResponse,
},
});
}

static signup(data: TAuth.SignupBody) {
return api<TAuth.SignupResponse>({
url: '/auth/sign-up',
method: 'POST',
data: data,
contracts: {
body: SAuth.SignupBody,
response: SAuth.SignupResponse,
},
});
}

static signupConfirm(data: TAuth.SignupConfirmBody) {
return api<TAuth.SignupConfirmResponse>({
url: '/auth/sign-up/confirm',
method: 'POST',
data: data,
skipAuthRefresh: true,
contracts: {
body: SAuth.SignupConfirmBody,
response: SAuth.SignupConfirmResponse,
},
});
}

static resetPassword(data: TAuth.ResetPasswordBody) {
return api<TAuth.ResetPasswordResponse>({
url: '/auth/password/reset',
method: 'POST',
data: data,
contracts: {
body: SAuth.ResetPasswordBody,
response: SAuth.ResetPasswordResponse,
},
});
}

static resetPasswordVerify(data: TAuth.ResetPasswordVerifyBody) {
return api<TAuth.ResetPasswordVerifyResponse>({
url: '/auth/password/reset/verify',
method: 'POST',
data: data,
contracts: {
body: SAuth.ResetPasswordVerifyBody,
response: SAuth.ResetPasswordVerifyResponse,
},
});
}

static resetPasswordConfirm(data: TAuth.ResetPasswordConfirmBody) {
return api<TAuth.ResetPasswordConfirmResponse>({
url: '/auth/password/reset/confirm',
method: 'POST',
data: data,
contracts: {
body: SAuth.ResetPasswordConfirmBody,
response: SAuth.ResetPasswordConfirmResponse,
},
});
}
}
3 changes: 3 additions & 0 deletions src/entities/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * as SAuth from './model/schemas';
export * as TAuth from './model/types';
export { AuthHttp } from './api/http';
67 changes: 67 additions & 0 deletions src/entities/auth/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { z } from 'zod/v4';
import { GlobalSuccess } from 'shared/api';

const MIN_PASS_LENGTH = 8;
const MAX_PASS_LENGTH = 32;
const OTP_LENGTH = 6;

export const Email = z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email'));

export const Password = z
.string()
.min(1, 'Обязательное поле')
.min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`)
.max(MAX_PASS_LENGTH, 'Слишком длинный пароль');

export const OTPCode = z.string().min(OTP_LENGTH, 'Обязательное поле').max(OTP_LENGTH);

export const SigninBody = z.object({
email: Email,
password: Password,
});

export const SigninResponse = GlobalSuccess.extend({
token: z.string(),
});

export const SignoutResponse = GlobalSuccess;

export const SignupBody = z.object({
email: Email,
password: Password,
firstName: z.string().min(2, 'Имя должно содержать минимум 2 символа').max(50).trim(),
lastName: z.string().min(2, 'Фамилия должна содержать минимум 2 символа').max(50).trim(),
middleName: z.string().max(50).trim().optional().or(z.literal('')),
});

export const SignupResponse = GlobalSuccess;

export const SignupConfirmBody = z.object({
email: Email,
code: OTPCode,
});

export const SignupConfirmResponse = GlobalSuccess.extend({
token: z.string(),
});

export const ResetPasswordBody = z.object({
email: Email,
});

export const ResetPasswordResponse = GlobalSuccess;

export const ResetPasswordVerifyBody = z.object({
email: Email,
code: OTPCode,
});

export const ResetPasswordVerifyResponse = GlobalSuccess;

export const ResetPasswordConfirmBody = z.object({
email: Email,
password: Password,
confirmPassword: Password,
});

export const ResetPasswordConfirmResponse = GlobalSuccess;
22 changes: 22 additions & 0 deletions src/entities/auth/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod/v4';
import * as SAuth from './schemas';

export type Email = z.infer<typeof SAuth.Email>;
export type Password = z.infer<typeof SAuth.Password>;
export type OTPCode = z.infer<typeof SAuth.OTPCode>;

export type SigninBody = z.infer<typeof SAuth.SigninBody>;
export type SigninResponse = z.infer<typeof SAuth.SigninResponse>;
export type SignoutResponse = z.infer<typeof SAuth.SignoutResponse>;

export type SignupBody = z.infer<typeof SAuth.SignupBody>;
export type SignupResponse = z.infer<typeof SAuth.SignupResponse>;
export type SignupConfirmBody = z.infer<typeof SAuth.SignupConfirmBody>;
export type SignupConfirmResponse = z.infer<typeof SAuth.SignupConfirmResponse>;

export type ResetPasswordBody = z.infer<typeof SAuth.ResetPasswordBody>;
export type ResetPasswordResponse = z.infer<typeof SAuth.ResetPasswordResponse>;
export type ResetPasswordVerifyBody = z.infer<typeof SAuth.ResetPasswordVerifyBody>;
export type ResetPasswordVerifyResponse = z.infer<typeof SAuth.ResetPasswordVerifyResponse>;
export type ResetPasswordConfirmBody = z.infer<typeof SAuth.ResetPasswordConfirmBody>;
export type ResetPasswordConfirmResponse = z.infer<typeof SAuth.ResetPasswordConfirmResponse>;
68 changes: 68 additions & 0 deletions src/entities/user/api/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { api } from 'shared/api';
import * as TUser from '../model/types';
import * as SUser from '../model/schemas';

export class UserHttp {
static getUser(signal?: AbortSignal) {
return api<TUser.UserResponse>({
url: '/users/me',
method: 'GET',
contracts: {
response: SUser.UserResponse,
},
signal,
});
}

//todo ручка пока в разработке
static getUserActivity(signal?: AbortSignal) {
return api<unknown>({
url: '/users/me/activity',
method: 'GET',
contracts: {},
signal,
});
}

static updateAvatar(file: File) {
const formData = new FormData();

formData.append('file', file);

return api<TUser.AvatarUpdateResponse>({
url: '/users/me/avatar',
method: 'POST',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
contracts: {
response: SUser.AvatarUpdateResponse,
},
});
}

static updateNotificationsConfig(data: TUser.NotificationsUpdateBody) {
return api<TUser.NotificationsUpdateResponse>({
url: '/users/me/notifications',
method: 'PATCH',
data,
contracts: {
body: SUser.NotificationsUpdateBody,
response: SUser.NotificationsUpdateResponse,
},
});
}

static updateUserConfig(data: TUser.ProfileUpdateBody) {
return api<TUser.ProfileUpdateResponse>({
url: '/users/me',
method: 'PATCH',
data,
contracts: {
body: SUser.ProfileUpdateBody,
response: SUser.ProfileUpdateResponse,
},
});
}
}
23 changes: 23 additions & 0 deletions src/entities/user/api/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { userFabricKeys } from '../model/const';
import { queryOptions } from '@tanstack/react-query';
import { UserHttp } from './http';

export class UserQueries {
static getMe() {
return queryOptions({
queryKey: userFabricKeys.me(),
queryFn: async ({ signal }) => UserHttp.getUser(signal),
staleTime: 60_000,
refetchOnMount: false,
});
}

static getMeActivity() {
return queryOptions({
queryKey: userFabricKeys.meActivity(),
queryFn: async ({ signal }) => UserHttp.getUserActivity(signal),
staleTime: 60_000,
refetchOnMount: false,
});
}
}
5 changes: 5 additions & 0 deletions src/entities/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * as SUser from './model/schemas';
export * as TUser from './model/types';
export { UserHttp } from './api/http';
export { UserQueries } from './api/queries';
export { userFabricKeys } from './model/const';
6 changes: 6 additions & 0 deletions src/entities/user/model/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createEntityKeys } from 'shared/lib/utils';

export const userFabricKeys = createEntityKeys('user', {
me: () => ['users', 'me'],
meActivity: () => ['users', 'me', 'activity'],
});
64 changes: 64 additions & 0 deletions src/entities/user/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { z } from 'zod/v4';
import { GlobalSuccess } from 'shared/api';

export const UserResponse = z.object({
id: z.string(),
email: z.email(),
profile: z.object({
firstName: z.string(),
lastName: z.string(),
middleName: z.string().nullable(),
bio: z.string().nullable(),
avatarUrl: z.url().nullable(),
timezone: z.string(),
language: z.string(),
createdAt: z.iso.datetime({}),
updatedAt: z.iso.datetime({}),
}),
security: z.object({
is2faEnabled: z.boolean(),
lastPasswordChange: z.iso.datetime({}),
}),
notifications: z.object({
email: z.object({
task_assigned: z.boolean(),
mentions: z.boolean(),
daily_summary: z.boolean(),
}),
push: z.object({
task_assigned: z.boolean(),
reminders: z.boolean(),
}),
}),
});

export const AvatarUpdateResponse = GlobalSuccess;

export const NotificationsUpdateBody = z.object({
email: z
.object({
task_assigned: z.boolean(),
mentions: z.boolean(),
daily_summary: z.boolean(),
})
.optional(),
push: z
.object({
task_assigned: z.boolean(),
reminders: z.boolean(),
})
.optional(),
});

export const NotificationsUpdateResponse = GlobalSuccess;

export const ProfileUpdateBody = z.object({
firstName: z.string().min(1).max(50).optional(),
lastName: z.string().min(1).max(50).optional(),
middleName: z.string().max(50).nullish(),
bio: z.string().max(512).nullish(),
timezone: z.string().max(50).optional(),
language: z.string().min(2).max(2).optional(),
});

export const ProfileUpdateResponse = GlobalSuccess;
9 changes: 9 additions & 0 deletions src/entities/user/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod/v4';
import * as SUser from './schemas';

export type UserResponse = z.infer<typeof SUser.UserResponse>;
export type AvatarUpdateResponse = z.infer<typeof SUser.AvatarUpdateResponse>;
export type NotificationsUpdateBody = z.infer<typeof SUser.NotificationsUpdateBody>;
export type NotificationsUpdateResponse = z.infer<typeof SUser.NotificationsUpdateResponse>;
export type ProfileUpdateBody = z.infer<typeof SUser.ProfileUpdateBody>;
export type ProfileUpdateResponse = z.infer<typeof SUser.ProfileUpdateResponse>;
1 change: 1 addition & 0 deletions src/features/otp-form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { OTPForm } from './ui/OTPForm';
Loading
Loading