diff --git a/Client/reasn-client/.husky/pre-commit b/Client/reasn-client/.husky/pre-commit index 1d9a8b60..fe97ecab 100644 --- a/Client/reasn-client/.husky/pre-commit +++ b/Client/reasn-client/.husky/pre-commit @@ -3,7 +3,7 @@ (cd ./Client/reasn-client && yarn lint-staged) (cd ./Server/ReasnAPI && dotnet format) -if ! git diff --quiet; then +if git diff --name-only --quiet | grep '\.cs$'; then echo "🚫 dotnet format made changes, commit aborted." exit 1 fi \ No newline at end of file diff --git a/Client/reasn-client/apps/web/lib/request.ts b/Client/reasn-client/apps/web/lib/request.ts new file mode 100644 index 00000000..05c26ba1 --- /dev/null +++ b/Client/reasn-client/apps/web/lib/request.ts @@ -0,0 +1,79 @@ +import ApiAuthorizationError from "@reasn/common/src/errors/ApiAuthorizationError"; +import ApiConnectionError from "@reasn/common/src/errors/ApiConnectionError"; +import fetch from "cross-fetch"; +import { getToken } from "@/lib/token"; + +export enum HttpMethod { + GET = "GET", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", +} + +export type ProblemDetails = { + type?: string; + title: string; + status: number; + details?: string; + instance?: string; +}; + +export type RequestOptions = { + method: HttpMethod; + body?: Object | FormData; + authRequired?: boolean; +}; + +export const sendRequest = async ( + url: string | URL, + { method, body = {}, authRequired = false }: RequestOptions, +): Promise => { + try { + let headers: HeadersInit = new Headers(); + if (authRequired) { + const token = getToken(); + if (!token) { + throw new ApiAuthorizationError( + "Unauthorized access. No token found in cookies", + ); + } + headers.set("Authorization", `Bearer ${token}`); + } + + const fetchOptions: RequestInit = { + method: method, + headers, + }; + + if (method == HttpMethod.POST || method == HttpMethod.PUT) { + if (body instanceof FormData) { + fetchOptions.body = body; + } else { + headers.set("Content-Type", "application/json"); + fetchOptions.body = JSON.stringify(body); + } + } + + const response = await fetch(url, fetchOptions); + if (!response.ok) { + const problemDetails = (await response.json()) as ProblemDetails; + console.error( + `[HTTP ${problemDetails.instance ?? ""} ${response.status}]: ${ + problemDetails.details ?? problemDetails.title + }`, + ); + + throw new ApiConnectionError( + response.status, + problemDetails.details ?? problemDetails.title, + ); + } + + return response.json().catch(() => {}) as T; + } catch (error) { + console.error( + `Error while sending request to ${url} with method ${method}: ${error}`, + ); + throw Error; + } +}; diff --git a/Client/reasn-client/apps/web/lib/session.ts b/Client/reasn-client/apps/web/lib/session.ts new file mode 100644 index 00000000..ecdde0e4 --- /dev/null +++ b/Client/reasn-client/apps/web/lib/session.ts @@ -0,0 +1,51 @@ +import { jwtDecode, JwtPayload } from "jwt-decode"; +import { UserRole } from "@reasn/common/src/enums/modelsEnums"; +import { getToken } from "@/lib/token"; + +export const SESSION_DEFAULT = { + token: null, + isAuthenticated: () => false, +}; + +type User = { + email: string; + role: UserRole; +}; + +export type Session = { + token: string | null; + user?: User; + isAuthenticated: () => boolean; +}; + +interface ReasnJwtPayload extends JwtPayload { + email?: string; + role?: UserRole; +} + +export const getSession = (): Session => { + const token = getToken(); + if (!token) { + return SESSION_DEFAULT; + } + + try { + const decodedToken = jwtDecode(token); + const isUserValid = + decodedToken.email !== undefined && decodedToken.role !== undefined; + + return { + token: token, + user: isUserValid + ? { + email: decodedToken.email as string, + role: decodedToken.role as UserRole, + } + : undefined, + isAuthenticated: () => token !== null && isUserValid, + }; + } catch (error) { + console.error("[ERROR]: Failed to decode JWT token"); + return SESSION_DEFAULT; + } +}; diff --git a/Client/reasn-client/apps/web/lib/token.ts b/Client/reasn-client/apps/web/lib/token.ts new file mode 100644 index 00000000..afd60c19 --- /dev/null +++ b/Client/reasn-client/apps/web/lib/token.ts @@ -0,0 +1,23 @@ +import { cookies } from "next/headers"; +import { TokenPayload } from "@reasn/common/src/schemas/tokenPayload"; + +const TOKEN_KEY = "REASN_TOKEN"; + +export const setToken = (tokenPayload: TokenPayload): void => { + cookies().set(TOKEN_KEY, tokenPayload.accessToken, { + maxAge: tokenPayload.expiresIn, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + httpOnly: true, + }); +}; + +export const clearToken = (): void => { + cookies().set(TOKEN_KEY, "", { maxAge: 0 }); +}; + +export const getToken = (): string | null => { + const token = cookies().get(TOKEN_KEY)?.value; + if (!token) return null; + return token; +}; diff --git a/Client/reasn-client/apps/web/package.json b/Client/reasn-client/apps/web/package.json index 27233e01..7fdb9811 100644 --- a/Client/reasn-client/apps/web/package.json +++ b/Client/reasn-client/apps/web/package.json @@ -9,7 +9,10 @@ "lint": "next lint" }, "dependencies": { + "@reasn/common": "*", "@reasn/ui": "*", + "@types/url-search-params": "^1.1.2", + "jwt-decode": "^4.0.0", "next": "^14.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/Client/reasn-client/apps/web/services/auth.ts b/Client/reasn-client/apps/web/services/auth.ts new file mode 100644 index 00000000..bc20bf6b --- /dev/null +++ b/Client/reasn-client/apps/web/services/auth.ts @@ -0,0 +1,34 @@ +import { sendRequest, HttpMethod } from "@/lib/request"; +import { + TokenPayload, + TokenPayloadMapper, +} from "@reasn/common/src/schemas/TokenPayload"; +import { LoginRequest } from "@reasn/common/src/schemas/LoginRequest"; +import { RegisterRequest } from "@reasn/common/src/schemas/RegisterRequest"; +import { UserDto, UserDtoMapper } from "@reasn/common/src/schemas/UserDto"; + +const baseUrl = `${process.env.REASN_API_URL}/api/v1/auth`; + +export const login = async ( + loginRequest: LoginRequest, +): Promise => { + const url = new URL(`${baseUrl}/login`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: loginRequest, + }); + return TokenPayloadMapper.fromObject(response); +}; + +export const register = async ( + registerRequest: RegisterRequest, +): Promise => { + const url = new URL(`${baseUrl}/register`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: registerRequest, + }); + return UserDtoMapper.fromObject(response); +}; diff --git a/Client/reasn-client/apps/web/services/event.ts b/Client/reasn-client/apps/web/services/event.ts new file mode 100644 index 00000000..ea052b4b --- /dev/null +++ b/Client/reasn-client/apps/web/services/event.ts @@ -0,0 +1,181 @@ +import { sendRequest, HttpMethod } from "@/lib/request"; +import { EventRequest } from "@reasn/common/src/schemas/EventRequest"; +import { + EventResponse, + EventResponseMapper, +} from "@reasn/common/src/schemas/EventResponse"; +import { ParameterDto } from "@reasn/common/src/schemas/ParameterDto"; +import { TagDto } from "@reasn/common/src/schemas/TagDto"; + +const baseUrl = `${process.env.REASN_API_URL}/api/v1/events`; + +export const getEvents = async ( + params: Record = {}, +): Promise => { + const url = new URL(baseUrl); + url.search = new URLSearchParams(params).toString(); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + }); + return response; +}; + +export const createEvent = async ( + eventRequest: EventRequest, +): Promise => { + const url = new URL(baseUrl); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: eventRequest, + authRequired: true, + }); + return EventResponseMapper.fromObject(response); +}; + +export const getEventBySlug = async (slug: string): Promise => { + const url = new URL(`${baseUrl}/${slug}`); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + }); + return EventResponseMapper.fromObject(response); +}; + +export const updateEvent = async ( + slug: string, + event: any, +): Promise => { + const url = new URL(`${baseUrl}/${slug}`); + + const response = await sendRequest(url, { + method: HttpMethod.PUT, + body: event, + authRequired: true, + }); + return EventResponseMapper.fromObject(response); +}; + +export const getEventsRequests = async (): Promise => { + const url = new URL(`${baseUrl}/requests`); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return response; +}; + +export const approveEventRequest = async (slug: string): Promise => { + const url = new URL(`${baseUrl}/requests/${slug}`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + authRequired: true, + }); + return response; +}; + +export const addEventImage = async ( + slug: string, + images: Blob[], +): Promise => { + const url = new URL(`${baseUrl}/${slug}/images`); + + const formData = new FormData(); + images.forEach((image) => { + formData.append("images", image); + }); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: formData, + authRequired: true, + }); + return response; +}; + +export const updateEventImage = async ( + slug: string, + images: Blob[], +): Promise => { + const url = new URL(`${baseUrl}/${slug}/images`); + + const formData = new FormData(); + images.forEach((image) => { + formData.append("images", image); + }); + + const response = await sendRequest(url, { + method: HttpMethod.PUT, + body: formData, + authRequired: true, + }); + return response; +}; + +export const getEventImages = async (slug: string): Promise => { + const url = new URL(`${baseUrl}/${slug}/images`); + + const response = await sendRequest(url, { method: HttpMethod.GET }); + return response; +}; + +export const getEventParticipants = async (slug: string): Promise => { + const url = new URL(`${baseUrl}/${slug}/participants`); + + const response = await sendRequest(url, { method: HttpMethod.GET }); + return response; +}; + +export const getEventComments = async (slug: string): Promise => { + const url = new URL(`${baseUrl}/${slug}/comments`); + + const response = await sendRequest(url, { method: HttpMethod.GET }); + return response; +}; + +export const addEventComment = async ( + slug: string, + comment: any, +): Promise => { + const url = new URL(`${baseUrl}/${slug}/comments`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: comment, + authRequired: true, + }); + return response; +}; + +export const getEventsParameters = async (): Promise => { + const url = new URL(`${baseUrl}/parameters`); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return response; +}; + +export const getEventsTags = async (): Promise => { + const url = new URL(`${baseUrl}/tags`); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return response; +}; + +export const deleteEventsTag = async (tagId: number): Promise => { + const url = new URL(`${baseUrl}/tags/${tagId}`); + + const response = await sendRequest(url, { + method: HttpMethod.DELETE, + authRequired: true, + }); + return response; +}; diff --git a/Client/reasn-client/apps/web/services/user.ts b/Client/reasn-client/apps/web/services/user.ts new file mode 100644 index 00000000..80badc15 --- /dev/null +++ b/Client/reasn-client/apps/web/services/user.ts @@ -0,0 +1,174 @@ +import { sendRequest, HttpMethod } from "@/lib/request"; +import { UserDto, UserDtoMapper } from "@reasn/common/src/schemas/UserDto"; +import { + ParticipantDto, + ParticipantDtoMapper, +} from "@reasn/common/src/schemas/ParticipantDto"; + +const baseUrl = `${process.env.REASN_API_URL}/api/v1`; +const baseUsersUrl = `${baseUrl}/users`; +const baseMeUrl = `${baseUrl}/me`; + +export const getUsers = async ( + params: Record = {}, +): Promise => { + const url = new URL(baseUsersUrl); + url.search = new URLSearchParams(params).toString(); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return response; +}; + +export const getUserByUsername = async (username: string): Promise => { + const url = new URL(`${baseUsersUrl}/${username}`); + + const response = await sendRequest(url, { method: HttpMethod.GET }); + return UserDtoMapper.fromObject(response); +}; + +export const updateUser = async ( + username: string, + user: UserDto, +): Promise => { + const url = new URL(`${baseUsersUrl}/${username}`); + + const response = await sendRequest(url, { + method: HttpMethod.PUT, + body: user, + authRequired: true, + }); + return UserDtoMapper.fromObject(response); +}; + +export const getUsersInterests = async ( + params: Record = {}, +): Promise => { + const url = new URL(`${baseUsersUrl}/interests`); + url.search = new URLSearchParams(params).toString(); + + const response = await sendRequest(url, { method: HttpMethod.GET }); + return response; +}; + +export const deleteUserInterest = async ( + insterestId: number, +): Promise => { + const url = new URL(`${baseUsersUrl}/interests/${insterestId}`); + + const response = await sendRequest(url, { + method: HttpMethod.DELETE, + authRequired: true, + }); + return response; +}; + +export const getCurrentUser = async (): Promise => { + const url = new URL(baseMeUrl); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return UserDtoMapper.fromObject(response); +}; + +export const updateCurrentUser = async (user: UserDto): Promise => { + const url = new URL(baseMeUrl); + + const response = await sendRequest(url, { + method: HttpMethod.PUT, + body: user, + authRequired: true, + }); + return UserDtoMapper.fromObject(response); +}; + +export const addCurrentUserImage = async (image: Blob): Promise => { + const url = new URL(`${baseMeUrl}/image`); + + const formData = new FormData(); + formData.append("images", image); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + body: formData, + authRequired: true, + }); + return response; +}; + +export const updateCurrentUserImage = async (image: Blob): Promise => { + const url = new URL(`${baseMeUrl}/image`); + + const formData = new FormData(); + formData.append("images", image); + + const response = await sendRequest(url, { + method: HttpMethod.PUT, + body: formData, + authRequired: true, + }); + return response; +}; + +export const deleteCurrentUserImage = async (): Promise => { + const url = new URL(`${baseMeUrl}/image`); + + const response = await sendRequest(url, { + method: HttpMethod.DELETE, + authRequired: true, + }); + return response; +}; + +export const getCurrentUserEvents = async ( + params: Record = {}, +): Promise => { + const url = new URL(`${baseMeUrl}/events`); + url.search = new URLSearchParams(params).toString(); + + const response = await sendRequest(url, { + method: HttpMethod.GET, + authRequired: true, + }); + return response; +}; + +export const enrollCurrentUserInEvent = async ( + slug: string, +): Promise => { + const url = new URL(`${baseMeUrl}/events/${slug}/enroll`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + authRequired: true, + }); + return ParticipantDtoMapper.fromObject(response); +}; + +export const confirmCurrentUserAttendace = async ( + slug: string, +): Promise => { + const url = new URL(`${baseMeUrl}/events/${slug}/confirm`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + authRequired: true, + }); + return ParticipantDtoMapper.fromObject(response); +}; + +export const cancelCurrentUserAttendance = async ( + slug: string, +): Promise => { + const url = new URL(`${baseMeUrl}/events/${slug}/cancel`); + + const response = await sendRequest(url, { + method: HttpMethod.POST, + authRequired: true, + }); + return ParticipantDtoMapper.fromObject(response); +}; diff --git a/Client/reasn-client/apps/web/tsconfig.json b/Client/reasn-client/apps/web/tsconfig.json index aea6585a..d2299420 100644 --- a/Client/reasn-client/apps/web/tsconfig.json +++ b/Client/reasn-client/apps/web/tsconfig.json @@ -1,12 +1,16 @@ { "extends": "@reasn/typescript-config/nextjs.json", "compilerOptions": { + "baseUrl": ".", "plugins": [ { "name": "next" } ], - "strictNullChecks": true + "strictNullChecks": true, + "paths": { + "@/*": ["./*"] + } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] diff --git a/Client/reasn-client/packages/common/__tests__/models/AddressDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/AddressDto.test.ts similarity index 96% rename from Client/reasn-client/packages/common/__tests__/models/AddressDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/AddressDto.test.ts index 392bb9c4..eccb599b 100644 --- a/Client/reasn-client/packages/common/__tests__/models/AddressDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/AddressDto.test.ts @@ -1,5 +1,8 @@ -import { AddressDtoMapper, AddressDto } from "@reasn/common/models/AddressDto"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import { + AddressDtoMapper, + AddressDto, +} from "@reasn/common/src/schemas/AddressDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; describe("AddressDto", () => { const country = "Test Country"; diff --git a/Client/reasn-client/packages/common/__tests__/models/CommentDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/CommentDto.test.ts similarity index 97% rename from Client/reasn-client/packages/common/__tests__/models/CommentDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/CommentDto.test.ts index eefcf6d6..20645b92 100644 --- a/Client/reasn-client/packages/common/__tests__/models/CommentDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/CommentDto.test.ts @@ -1,5 +1,8 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { CommentDto, CommentDtoMapper } from "@reasn/common/models/CommentDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; +import { + CommentDto, + CommentDtoMapper, +} from "@reasn/common/src/schemas/CommentDto"; describe("CommentDto", () => { const eventId = 1; diff --git a/Client/reasn-client/packages/common/__tests__/models/EventDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/EventDto.test.ts similarity index 98% rename from Client/reasn-client/packages/common/__tests__/models/EventDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/EventDto.test.ts index 177960cd..73a7a9dd 100644 --- a/Client/reasn-client/packages/common/__tests__/models/EventDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/EventDto.test.ts @@ -1,7 +1,7 @@ -import { EventDto, EventDtoMapper } from "@reasn/common/models/EventDto"; -import { EventStatus } from "@reasn/common/enums/modelsEnums"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { TagDto } from "@reasn/common/models/TagDto"; +import { EventDto, EventDtoMapper } from "@reasn/common/src/schemas/EventDto"; +import { EventStatus } from "@reasn/common/src/enums/schemasEnums"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; +import { TagDto } from "@reasn/common/src/schemas/TagDto"; describe("EventDto", () => { const name = "Test Event"; diff --git a/Client/reasn-client/packages/common/__tests__/models/ImageDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/ImageDto.test.ts similarity index 93% rename from Client/reasn-client/packages/common/__tests__/models/ImageDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/ImageDto.test.ts index c98f03c8..8d8df35f 100644 --- a/Client/reasn-client/packages/common/__tests__/models/ImageDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/ImageDto.test.ts @@ -1,6 +1,6 @@ -import { ImageDto, ImageDtoMapper } from "@reasn/common/models/ImageDto"; -import { ObjectType } from "@reasn/common/enums/modelsEnums"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import { ImageDto, ImageDtoMapper } from "@reasn/common/src/schemas/ImageDto"; +import { ObjectType } from "@reasn/common/src/enums/schemasEnums"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; describe("ImageDto", () => { const imageData = "Test Image"; diff --git a/Client/reasn-client/packages/common/__tests__/models/InterestDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/InterestDto.test.ts similarity index 91% rename from Client/reasn-client/packages/common/__tests__/models/InterestDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/InterestDto.test.ts index 7f059890..af828f5f 100644 --- a/Client/reasn-client/packages/common/__tests__/models/InterestDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/InterestDto.test.ts @@ -1,8 +1,8 @@ import { InterestDto, InterestDtoMapper, -} from "@reasn/common/models/InterestDto"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +} from "@reasn/common/src/schemas/InterestDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; describe("InterestDto", () => { const name = "Interest"; diff --git a/Client/reasn-client/packages/common/__tests__/models/ParameterDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/ParameterDto.test.ts similarity index 94% rename from Client/reasn-client/packages/common/__tests__/models/ParameterDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/ParameterDto.test.ts index 69153166..68ee9c81 100644 --- a/Client/reasn-client/packages/common/__tests__/models/ParameterDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/ParameterDto.test.ts @@ -1,8 +1,8 @@ import { ParameterDto, ParameterDtoMapper, -} from "@reasn/common/models/ParameterDto"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +} from "@reasn/common/src/schemas/ParameterDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; describe("ParameterDto", () => { const key = "Test Key"; diff --git a/Client/reasn-client/packages/common/__tests__/models/ParticipantDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/ParticipantDto.test.ts similarity index 58% rename from Client/reasn-client/packages/common/__tests__/models/ParticipantDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/ParticipantDto.test.ts index 75c65e0a..8218b83a 100644 --- a/Client/reasn-client/packages/common/__tests__/models/ParticipantDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/ParticipantDto.test.ts @@ -1,28 +1,28 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; import { ParticipantDto, ParticipantDtoMapper, -} from "@reasn/common/models/ParticipantDto"; -import { ParticipantStatus } from "@reasn/common/enums/modelsEnums"; +} from "@reasn/common/src/schemas/ParticipantDto"; +import { ParticipantStatus } from "@reasn/common/src/enums/schemasEnums"; describe("ParticipantDto", () => { - const eventId = 1; - const userId = 2; + const eventSlug = "event-slug"; + const username = "username"; const status = ParticipantStatus.INTERESTED; describe("fromJson", () => { it("should create an instance of ParticipantDto from JSON string", () => { const json = `{ - "EventId": ${eventId}, - "UserId": ${userId}, + "EventSlug": "${eventSlug}", + "Username": "${username}", "Status": "${status}" }`; let participant = ParticipantDtoMapper.fromJSON(json); participant = participant as ParticipantDto; - expect(participant.EventId).toBe(eventId); - expect(participant.UserId).toBe(userId); + expect(participant.EventSlug).toBe(eventSlug); + expect(participant.Username).toBe(username); expect(participant.Status).toBe(status); }); @@ -33,28 +33,28 @@ describe("ParticipantDto", () => { }); it("should throw an error when providing JSON without each property individually", () => { - const jsonWithoutEventId = `{ - "UserId": ${userId}, + const jsonWithoutEventSlug = `{ + "Username": "${username}", "Status": "${status}" }`; - const jsonWithoutUserId = `{ - "EventId": ${eventId}, + const jsonWithoutUsername = `{ + "EventSlug": "${eventSlug}", "Status": "${status}" }`; - const jsonWithoutStatusId = `{ - "EventId": ${eventId}, - "UserId": ${userId} + const jsonWithoutStatus = `{ + "EventSlug": "${eventSlug}", + "Username": "${username}" }`; - expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutEventId)).toThrow( + expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutEventSlug)).toThrow( ModelMappingError, ); - expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutUserId)).toThrow( + expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutUsername)).toThrow( ModelMappingError, ); - expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutStatusId)).toThrow( + expect(() => ParticipantDtoMapper.fromJSON(jsonWithoutStatus)).toThrow( ModelMappingError, ); }); @@ -63,52 +63,52 @@ describe("ParticipantDto", () => { describe("fromObject", () => { it("should create an instance of ParticipantDto from an object", () => { const object = { - EventId: eventId, - UserId: userId, + EventSlug: eventSlug, + Username: username, Status: status, }; let participant = ParticipantDtoMapper.fromObject(object); participant = participant as ParticipantDto; - expect(participant.EventId).toBe(eventId); - expect(participant.UserId).toBe(userId); + expect(participant.EventSlug).toBe(eventSlug); + expect(participant.Username).toBe(username); expect(participant.Status).toBe(status); }); it("should throw an error if the object is invalid", () => { const object = { - EventId: true, - UserId: null, + EventStatus: true, + Username: null, Status: "invalid", }; - const objectWithoutEventId = { - UserId: userId, + const objectWithoutEventSlug = { + Username: username, Status: status, }; - const objectWithoutUserId = { - EventId: eventId, + const objectWithoutUsername = { + EventSlug: eventSlug, Status: status, }; - const objectWithoutStatusId = { - EventId: eventId, - UserId: userId, + const objectWithoutStatus = { + EventSlug: eventSlug, + Username: username, }; expect(() => ParticipantDtoMapper.fromObject(object)).toThrow( ModelMappingError, ); expect(() => - ParticipantDtoMapper.fromObject(objectWithoutEventId), + ParticipantDtoMapper.fromObject(objectWithoutEventSlug), ).toThrow(ModelMappingError); expect(() => - ParticipantDtoMapper.fromObject(objectWithoutUserId), + ParticipantDtoMapper.fromObject(objectWithoutUsername), ).toThrow(ModelMappingError); expect(() => - ParticipantDtoMapper.fromObject(objectWithoutStatusId), + ParticipantDtoMapper.fromObject(objectWithoutStatus), ).toThrow(ModelMappingError); }); }); diff --git a/Client/reasn-client/packages/common/__tests__/models/TagDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/TagDto.test.ts similarity index 90% rename from Client/reasn-client/packages/common/__tests__/models/TagDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/TagDto.test.ts index 65913622..aefba118 100644 --- a/Client/reasn-client/packages/common/__tests__/models/TagDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/TagDto.test.ts @@ -1,5 +1,5 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { TagDto, TagDtoMapper } from "@reasn/common/models/TagDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; +import { TagDto, TagDtoMapper } from "@reasn/common/src/schemas/TagDto"; describe("TagDto", () => { const name = "tag name"; diff --git a/Client/reasn-client/packages/common/__tests__/models/UserDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/UserDto.test.ts similarity index 81% rename from Client/reasn-client/packages/common/__tests__/models/UserDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/UserDto.test.ts index e0ed639d..31b26838 100644 --- a/Client/reasn-client/packages/common/__tests__/models/UserDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/UserDto.test.ts @@ -1,7 +1,8 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { UserDto, UserDtoMapper } from "@reasn/common/models/UserDto"; -import { UserInterestDto } from "@reasn/common/models/UserInterestDto"; -import { UserRole } from "@reasn/common/enums/modelsEnums"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; +import { UserDto, UserDtoMapper } from "@reasn/common/src/schemas/UserDto"; +import { UserInterestDto } from "@reasn/common/src/schemas/UserInterestDto"; +import { UserRole } from "@reasn/common/src/enums/schemasEnums"; +import { AddressDto } from "@reasn/common/src/schemas/AddressDto"; describe("UserDto", () => { const username = "john_doe"; @@ -11,6 +12,13 @@ describe("UserDto", () => { const phone = "+48 1234567890"; const role = UserRole.USER; const addressId = 2; + const address: AddressDto = { + Country: "Test Country", + City: "Test City", + Street: "Test Street", + State: "Test State", + ZipCode: "12345", + }; const interests: UserInterestDto[] = [ { Interest: { Name: "Programming" }, Level: 5 }, { Interest: { Name: "Music" }, Level: 3 }, @@ -26,6 +34,7 @@ describe("UserDto", () => { "Phone": "${phone}", "Role": "${role}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -39,6 +48,7 @@ describe("UserDto", () => { expect(user.Phone).toBe(phone); expect(user.Role).toBe(role); expect(user.AddressId).toBe(addressId); + expect(user.Address).toEqual(address); expect(user.Intrests).toEqual(interests); }); @@ -54,6 +64,7 @@ describe("UserDto", () => { "Phone": "${phone}", "Role": "${role}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -64,6 +75,7 @@ describe("UserDto", () => { "Phone": "${phone}", "Role": "${role}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -74,6 +86,7 @@ describe("UserDto", () => { "Phone": "${phone}", "Role": "${role}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -84,6 +97,7 @@ describe("UserDto", () => { "Phone": "${phone}", "Role": "${role}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -104,6 +118,7 @@ describe("UserDto", () => { "Email": "${email}", "Phone": "${phone}", "AddressId": ${addressId}, + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; @@ -114,9 +129,21 @@ describe("UserDto", () => { "Email": "${email}", "Phone": "${phone}", "Role": "${role}", + "Address": ${JSON.stringify(address)}, "Intrests": ${JSON.stringify(interests)} }`; + const jsonWithoutAddress = `{ + "Username": "${username}", + "Name": "${name}", + "Surname": "${surname}", + "Email": "${email}", + "Phone": "${phone}", + "AddressId": ${addressId}, + "Role": "${role}", + "Intrests": ${JSON.stringify(interests)} + }`; + const jsonWithoutInterests = `{ "Username": "${username}", "Name": "${name}", @@ -148,6 +175,9 @@ describe("UserDto", () => { expect(() => UserDtoMapper.fromJSON(jsonWithoutAddressId)).toThrow( ModelMappingError, ); + expect(() => UserDtoMapper.fromJSON(jsonWithoutAddress)).toThrow( + ModelMappingError, + ); expect(() => UserDtoMapper.fromJSON(jsonWithoutInterests)).toThrow( ModelMappingError, ); @@ -164,6 +194,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -177,6 +208,7 @@ describe("UserDto", () => { expect(user.Phone).toBe(phone); expect(user.Role).toBe(role); expect(user.AddressId).toBe(addressId); + expect(user.Address).toEqual(address); expect(user.Intrests).toEqual(interests); }); @@ -199,6 +231,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -209,6 +242,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -219,6 +253,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -229,6 +264,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -239,6 +275,7 @@ describe("UserDto", () => { Email: email, Role: role, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -249,6 +286,7 @@ describe("UserDto", () => { Email: email, Phone: phone, AddressId: addressId, + Address: address, Intrests: interests, }; @@ -259,6 +297,18 @@ describe("UserDto", () => { Email: email, Phone: phone, Role: role, + Address: address, + Intrests: interests, + }; + + const objectWithoutAddress = { + Username: username, + Name: name, + Surname: surname, + Email: email, + Phone: phone, + Role: role, + AddressId: addressId, Intrests: interests, }; @@ -270,6 +320,7 @@ describe("UserDto", () => { Phone: phone, Role: role, AddressId: addressId, + Address: address, }; expect(() => UserDtoMapper.fromObject(object)).toThrow(ModelMappingError); @@ -294,6 +345,9 @@ describe("UserDto", () => { expect(() => UserDtoMapper.fromObject(objectWithoutAddressId)).toThrow( ModelMappingError, ); + expect(() => UserDtoMapper.fromObject(objectWithoutAddress)).toThrow( + ModelMappingError, + ); expect(() => UserDtoMapper.fromObject(objectWithoutInterests)).toThrow( ModelMappingError, ); diff --git a/Client/reasn-client/packages/common/__tests__/models/UserInterestDto.test.ts b/Client/reasn-client/packages/common/__tests__/schemas/UserInterestDto.test.ts similarity index 92% rename from Client/reasn-client/packages/common/__tests__/models/UserInterestDto.test.ts rename to Client/reasn-client/packages/common/__tests__/schemas/UserInterestDto.test.ts index 31777023..e8c1283c 100644 --- a/Client/reasn-client/packages/common/__tests__/models/UserInterestDto.test.ts +++ b/Client/reasn-client/packages/common/__tests__/schemas/UserInterestDto.test.ts @@ -1,9 +1,9 @@ -import { InterestDto } from "@reasn/common/models/InterestDto"; +import { InterestDto } from "@reasn/common/src/schemas/InterestDto"; import { UserInterestDto, UserInterestDtoMapper, -} from "@reasn/common/models/UserInterestDto"; -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +} from "@reasn/common/src/schemas/UserInterestDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; describe("UserInterestDto", () => { const interest = { Name: "Interest" } as InterestDto; diff --git a/Client/reasn-client/packages/common/__tests__/services/apiServices.test.ts b/Client/reasn-client/packages/common/__tests__/services/apiServices.test.ts index 0f136c33..0c9327a9 100644 --- a/Client/reasn-client/packages/common/__tests__/services/apiServices.test.ts +++ b/Client/reasn-client/packages/common/__tests__/services/apiServices.test.ts @@ -1,14 +1,14 @@ -import { sendRequest } from "@reasn/common/services/apiServices"; -import { getAuthDataFromSessionStorage } from "@reasn/common/services/authorizationServices"; -import { AuthData } from "@reasn/common/interfaces/AuthData"; -import { HttpMethod } from "@reasn/common/enums/serviceEnums"; +import { sendRequest } from "@reasn/common/src/services/apiServices"; +import { getAuthDataFromSessionStorage } from "@reasn/common/src/services/authorizationServices"; +import { AuthData } from "@reasn/common/src/interfaces/AuthData"; +import { HttpMethod } from "@reasn/common/src/enums/serviceEnums"; import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import fetch from "cross-fetch"; -import ApiConnectionError from "@reasn/common/errors/ApiConnectionError"; -import ApiAuthorizationError from "@reasn/common/errors/ApiAuthorizationError"; +import ApiConnectionError from "@reasn/common/src/errors/ApiConnectionError"; +import ApiAuthorizationError from "@reasn/common/src/errors/ApiAuthorizationError"; jest.mock("cross-fetch"); -jest.mock("@reasn/common/services/authorizationServices"); +jest.mock("@reasn/common/src/services/authorizationServices"); describe("sendRequest", () => { beforeEach(() => { diff --git a/Client/reasn-client/packages/common/package.json b/Client/reasn-client/packages/common/package.json index 35064199..41f30a3a 100644 --- a/Client/reasn-client/packages/common/package.json +++ b/Client/reasn-client/packages/common/package.json @@ -1,8 +1,17 @@ { "name": "@reasn/common", "version": "0.0.0", - "private": true, - "publishConfig": { - "access": "public" + "scripts": { + "dev": "tsc --watch", + "build": "tsc" + }, + "dependencies": { + "zod": "^3.23.7" + }, + "devDependencies": { + "@reasn/typescript-config": "workspace:*", + "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", + "typescript": "^5.3.3" } } diff --git a/Client/reasn-client/packages/common/enums/modelsEnums.ts b/Client/reasn-client/packages/common/src/enums/schemasEnums.ts similarity index 100% rename from Client/reasn-client/packages/common/enums/modelsEnums.ts rename to Client/reasn-client/packages/common/src/enums/schemasEnums.ts diff --git a/Client/reasn-client/packages/common/enums/serviceEnums.ts b/Client/reasn-client/packages/common/src/enums/serviceEnums.ts similarity index 100% rename from Client/reasn-client/packages/common/enums/serviceEnums.ts rename to Client/reasn-client/packages/common/src/enums/serviceEnums.ts diff --git a/Client/reasn-client/packages/common/errors/ApiAuthorizationError.ts b/Client/reasn-client/packages/common/src/errors/ApiAuthorizationError.ts similarity index 100% rename from Client/reasn-client/packages/common/errors/ApiAuthorizationError.ts rename to Client/reasn-client/packages/common/src/errors/ApiAuthorizationError.ts diff --git a/Client/reasn-client/packages/common/errors/ApiConnectionError.ts b/Client/reasn-client/packages/common/src/errors/ApiConnectionError.ts similarity index 100% rename from Client/reasn-client/packages/common/errors/ApiConnectionError.ts rename to Client/reasn-client/packages/common/src/errors/ApiConnectionError.ts diff --git a/Client/reasn-client/packages/common/errors/ModelMappingError.ts b/Client/reasn-client/packages/common/src/errors/ModelMappingError.ts similarity index 100% rename from Client/reasn-client/packages/common/errors/ModelMappingError.ts rename to Client/reasn-client/packages/common/src/errors/ModelMappingError.ts diff --git a/Client/reasn-client/packages/common/interfaces/AuthData.ts b/Client/reasn-client/packages/common/src/interfaces/AuthData.ts similarity index 64% rename from Client/reasn-client/packages/common/interfaces/AuthData.ts rename to Client/reasn-client/packages/common/src/interfaces/AuthData.ts index e0cb3af4..3ee8a3bf 100644 --- a/Client/reasn-client/packages/common/interfaces/AuthData.ts +++ b/Client/reasn-client/packages/common/src/interfaces/AuthData.ts @@ -1,4 +1,4 @@ -import { UserRole } from "@reasn/common/enums/serviceEnums"; +import { UserRole } from "../enums/schemasEnums"; /** * Represents the authentication data. diff --git a/Client/reasn-client/packages/common/models/AddressDto.ts b/Client/reasn-client/packages/common/src/schemas/AddressDto.ts similarity index 88% rename from Client/reasn-client/packages/common/models/AddressDto.ts rename to Client/reasn-client/packages/common/src/schemas/AddressDto.ts index 0827e825..8ad11d68 100644 --- a/Client/reasn-client/packages/common/models/AddressDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/AddressDto.ts @@ -1,4 +1,4 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "../errors/ModelMappingError"; import { z } from "zod"; export const AddressDtoSchema = z.object({ @@ -20,8 +20,8 @@ export const AddressDtoSchema = z.object({ .regex(/^\p{Lu}\p{Ll}+(?:(\s|-)\p{L}+)*$/u), ZipCode: z .string() - .nullable() - .refine((value) => value === null || /^[\p{L}\d\s-]{3,}$/u.test(value)), + .regex(/^[\p{L}\d\s-]{3,}$/u) + .nullable(), }); export type AddressDto = z.infer; diff --git a/Client/reasn-client/packages/common/models/CommentDto.ts b/Client/reasn-client/packages/common/src/schemas/CommentDto.ts similarity index 93% rename from Client/reasn-client/packages/common/models/CommentDto.ts rename to Client/reasn-client/packages/common/src/schemas/CommentDto.ts index 4e1f8a87..abe54755 100644 --- a/Client/reasn-client/packages/common/models/CommentDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/CommentDto.ts @@ -1,4 +1,4 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "../errors/ModelMappingError"; import { z } from "zod"; export const CommentDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/models/EventDto.ts b/Client/reasn-client/packages/common/src/schemas/EventDto.ts similarity index 89% rename from Client/reasn-client/packages/common/models/EventDto.ts rename to Client/reasn-client/packages/common/src/schemas/EventDto.ts index 8c360bdd..a9c1e0ba 100644 --- a/Client/reasn-client/packages/common/models/EventDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/EventDto.ts @@ -1,6 +1,6 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { TagDtoSchema } from "@reasn/common/models/TagDto"; -import { EventStatus } from "@reasn/common/enums/modelsEnums"; +import ModelMappingError from "../errors/ModelMappingError"; +import { TagDtoSchema } from "./TagDto"; +import { EventStatus } from "../enums/schemasEnums"; import { z } from "zod"; export const EventDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/src/schemas/EventRequest.ts b/Client/reasn-client/packages/common/src/schemas/EventRequest.ts new file mode 100644 index 00000000..8b3eedf6 --- /dev/null +++ b/Client/reasn-client/packages/common/src/schemas/EventRequest.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { AddressDtoSchema } from "./AddressDto"; +import { TagDtoSchema } from "./TagDto"; +import { ParameterDtoSchema } from "./ParameterDto"; + +export const EventRequestSchema = z + .object({ + name: z.string().max(64), + address: AddressDtoSchema, + description: z.string().max(4048), + startAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + endAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + tags: z.array(TagDtoSchema).nullable(), + parameters: z.array(ParameterDtoSchema).nullable(), + }) + .refine((schema) => schema.startAt < schema.endAt); + +export type EventRequest = z.infer; diff --git a/Client/reasn-client/packages/common/src/schemas/EventResponse.ts b/Client/reasn-client/packages/common/src/schemas/EventResponse.ts new file mode 100644 index 00000000..3eecda7e --- /dev/null +++ b/Client/reasn-client/packages/common/src/schemas/EventResponse.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; +import { AddressDtoSchema } from "@reasn/common/src/schemas/AddressDto"; +import ModelMappingError from "@reasn/common/src/errors/ModelMappingError"; +import { TagDtoSchema } from "@reasn/common/src/schemas/TagDto"; +import { ParameterDtoSchema } from "@reasn/common/src/schemas/ParameterDto"; +import { EventStatus } from "@reasn/common/src/enums/schemasEnums"; + +export const EventRespsoneSchema = z.object({ + name: z.string().max(64), + addressId: z.number(), + address: AddressDtoSchema, + description: z.string().max(4048), + organizer: z.object({ + username: z.string(), + image: z.string(), + }), + startAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + endAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + createdAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + updatedAt: z + .string() + .datetime({ offset: true }) + .or(z.date()) + .transform((arg) => new Date(arg)), + slug: z + .string() + .max(128) + .regex(/^[\p{L}\d]+[\p{L}\d-]*$/u) + .nullable(), + status: z.nativeEnum(EventStatus), + tags: z.array(TagDtoSchema), + parameters: z.array(ParameterDtoSchema), + participants: z.object({ + interested: z.number(), + participating: z.number(), + }), +}); + +export type EventResponse = z.infer; + +export const EventResponseMapper = { + fromObject: (entity: object): EventResponse => { + const result = EventRespsoneSchema.safeParse(entity); + if (!result.success) { + throw new ModelMappingError( + "EventResponse", + result.error.message, + result.error, + ); + } + return result.data; + }, + fromJSON: (jsonEntity: string): any => { + if (!jsonEntity) { + throw new ModelMappingError("EventResponse", "Empty JSON string"); + } + const result = EventRespsoneSchema.safeParse(JSON.parse(jsonEntity)); + if (!result.success) { + throw new ModelMappingError( + "EventResponse", + result.error.message, + result.error, + ); + } + return result.data; + }, +}; diff --git a/Client/reasn-client/packages/common/models/ImageDto.ts b/Client/reasn-client/packages/common/src/schemas/ImageDto.ts similarity index 87% rename from Client/reasn-client/packages/common/models/ImageDto.ts rename to Client/reasn-client/packages/common/src/schemas/ImageDto.ts index f0f89e79..5af26865 100644 --- a/Client/reasn-client/packages/common/models/ImageDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/ImageDto.ts @@ -1,5 +1,5 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { ObjectType } from "@reasn/common/enums/modelsEnums"; +import ModelMappingError from "../errors/ModelMappingError"; +import { ObjectType } from "../enums/schemasEnums"; import { z } from "zod"; export const ImageDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/models/InterestDto.ts b/Client/reasn-client/packages/common/src/schemas/InterestDto.ts similarity index 92% rename from Client/reasn-client/packages/common/models/InterestDto.ts rename to Client/reasn-client/packages/common/src/schemas/InterestDto.ts index 9576f0ac..303e60c5 100644 --- a/Client/reasn-client/packages/common/models/InterestDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/InterestDto.ts @@ -1,4 +1,4 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "../errors/ModelMappingError"; import { z } from "zod"; export const InterestDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/src/schemas/LoginRequest.ts b/Client/reasn-client/packages/common/src/schemas/LoginRequest.ts new file mode 100644 index 00000000..3c02aac0 --- /dev/null +++ b/Client/reasn-client/packages/common/src/schemas/LoginRequest.ts @@ -0,0 +1,37 @@ +import ModelMappingError from "../errors/ModelMappingError"; +import { z } from "zod"; + +export const LoginRequestSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); + +export type LoginRequest = z.infer; + +export const LoginRequestMapper = { + fromObject: (entity: object): LoginRequest => { + const result = LoginRequestSchema.safeParse(entity); + if (!result.success) { + throw new ModelMappingError( + "LoginRequest", + result.error.message, + result.error, + ); + } + return result.data; + }, + fromJSON: (jsonEntity: string): any => { + if (!jsonEntity) { + throw new ModelMappingError("LoginRequest", "Empty JSON string"); + } + const result = LoginRequestSchema.safeParse(JSON.parse(jsonEntity)); + if (!result.success) { + throw new ModelMappingError( + "LoginRequest", + result.error.message, + result.error, + ); + } + return result.data; + }, +}; diff --git a/Client/reasn-client/packages/common/models/ParameterDto.ts b/Client/reasn-client/packages/common/src/schemas/ParameterDto.ts similarity index 93% rename from Client/reasn-client/packages/common/models/ParameterDto.ts rename to Client/reasn-client/packages/common/src/schemas/ParameterDto.ts index f35b0b7b..8006e2be 100644 --- a/Client/reasn-client/packages/common/models/ParameterDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/ParameterDto.ts @@ -1,4 +1,4 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "../errors/ModelMappingError"; import { z } from "zod"; export const ParameterDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/models/ParticipantDto.ts b/Client/reasn-client/packages/common/src/schemas/ParticipantDto.ts similarity index 83% rename from Client/reasn-client/packages/common/models/ParticipantDto.ts rename to Client/reasn-client/packages/common/src/schemas/ParticipantDto.ts index e9a50f1d..2f687e8b 100644 --- a/Client/reasn-client/packages/common/models/ParticipantDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/ParticipantDto.ts @@ -1,10 +1,10 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { ParticipantStatus } from "@reasn/common/enums/modelsEnums"; +import ModelMappingError from "../errors/ModelMappingError"; +import { ParticipantStatus } from "../enums/schemasEnums"; import { z } from "zod"; export const ParticipantDtoSchema = z.object({ - EventId: z.number(), - UserId: z.number(), + EventSlug: z.string(), + Username: z.string(), Status: z.nativeEnum(ParticipantStatus), }); diff --git a/Client/reasn-client/packages/common/src/schemas/RegisterRequest.ts b/Client/reasn-client/packages/common/src/schemas/RegisterRequest.ts new file mode 100644 index 00000000..bb0ab075 --- /dev/null +++ b/Client/reasn-client/packages/common/src/schemas/RegisterRequest.ts @@ -0,0 +1,58 @@ +import ModelMappingError from "../errors/ModelMappingError"; +import { AddressDtoSchema } from "../schemas/AddressDto"; +import { z } from "zod"; + +export const RegisterRequestSchema = z.object({ + name: z + .string() + .max(64) + .regex(/^[\p{L}\d._%+-]{4,}$/u), + surname: z + .string() + .max(64) + .regex(/^\p{L}+(?:[\s'-]\p{L}+)*$/u), + email: z.string().email(), + username: z + .string() + .max(64) + .regex(/^[\p{L}\d._%+-]{4,}$/u), + password: z + .string() + .regex(/^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9]).{6,})\S$/u), + phone: z + .string() + .regex(/^\+\d{1,3}\s\d{1,15}$/) + .nullable(), + address: AddressDtoSchema, + Role: z.enum(["User", "Organizer"]), +}); + +export type RegisterRequest = z.infer; + +export const RegisterRequestMapper = { + fromObject: (entity: object): RegisterRequest => { + const result = RegisterRequestSchema.safeParse(entity); + if (!result.success) { + throw new ModelMappingError( + "RegisterRequest", + result.error.message, + result.error, + ); + } + return result.data; + }, + fromJSON: (jsonEntity: string): any => { + if (!jsonEntity) { + throw new ModelMappingError("RegisterRequest", "Empty JSON string"); + } + const result = RegisterRequestSchema.safeParse(JSON.parse(jsonEntity)); + if (!result.success) { + throw new ModelMappingError( + "RegisterRequest", + result.error.message, + result.error, + ); + } + return result.data; + }, +}; diff --git a/Client/reasn-client/packages/common/models/TagDto.ts b/Client/reasn-client/packages/common/src/schemas/TagDto.ts similarity index 91% rename from Client/reasn-client/packages/common/models/TagDto.ts rename to Client/reasn-client/packages/common/src/schemas/TagDto.ts index 3b129d84..b48334ea 100644 --- a/Client/reasn-client/packages/common/models/TagDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/TagDto.ts @@ -1,4 +1,4 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; +import ModelMappingError from "../errors/ModelMappingError"; import { z } from "zod"; export const TagDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/src/schemas/TokenPayload.ts b/Client/reasn-client/packages/common/src/schemas/TokenPayload.ts new file mode 100644 index 00000000..7141efe7 --- /dev/null +++ b/Client/reasn-client/packages/common/src/schemas/TokenPayload.ts @@ -0,0 +1,38 @@ +import ModelMappingError from "../errors/ModelMappingError"; +import { z } from "zod"; + +export const TokenPayloadSchema = z.object({ + tokenType: z.string(), + accessToken: z.string(), + expiresIn: z.number(), +}); + +export type TokenPayload = z.infer; + +export const TokenPayloadMapper = { + fromObject: (entity: object): TokenPayload => { + const result = TokenPayloadSchema.safeParse(entity); + if (!result.success) { + throw new ModelMappingError( + "TokenPayload", + result.error.message, + result.error, + ); + } + return result.data; + }, + fromJSON: (jsonEntity: string): any => { + if (!jsonEntity) { + throw new ModelMappingError("TokenPayload", "Empty JSON string"); + } + const result = TokenPayloadSchema.safeParse(JSON.parse(jsonEntity)); + if (!result.success) { + throw new ModelMappingError( + "TokenPayload", + result.error.message, + result.error, + ); + } + return result.data; + }, +}; diff --git a/Client/reasn-client/packages/common/models/UserDto.ts b/Client/reasn-client/packages/common/src/schemas/UserDto.ts similarity index 83% rename from Client/reasn-client/packages/common/models/UserDto.ts rename to Client/reasn-client/packages/common/src/schemas/UserDto.ts index e6f57ed0..7053027c 100644 --- a/Client/reasn-client/packages/common/models/UserDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/UserDto.ts @@ -1,6 +1,7 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { UserInterestDtoSchema } from "@reasn/common/models/UserInterestDto"; -import { UserRole } from "@reasn/common/enums/modelsEnums"; +import ModelMappingError from "../errors/ModelMappingError"; +import { UserInterestDtoSchema } from "../schemas/UserInterestDto"; +import { AddressDtoSchema } from "../schemas/AddressDto"; +import { UserRole } from "../enums/schemasEnums"; import { z } from "zod"; export const UserDtoSchema = z.object({ @@ -23,6 +24,7 @@ export const UserDtoSchema = z.object({ .refine((value) => value === null || /^\+\d{1,3}\s\d{1,15}$/.test(value)), Role: z.nativeEnum(UserRole), AddressId: z.number(), + Address: AddressDtoSchema, Intrests: z.array(UserInterestDtoSchema), }); diff --git a/Client/reasn-client/packages/common/models/UserInterestDto.ts b/Client/reasn-client/packages/common/src/schemas/UserInterestDto.ts similarity index 86% rename from Client/reasn-client/packages/common/models/UserInterestDto.ts rename to Client/reasn-client/packages/common/src/schemas/UserInterestDto.ts index d1db352f..c561e470 100644 --- a/Client/reasn-client/packages/common/models/UserInterestDto.ts +++ b/Client/reasn-client/packages/common/src/schemas/UserInterestDto.ts @@ -1,5 +1,5 @@ -import ModelMappingError from "@reasn/common/errors/ModelMappingError"; -import { InterestDtoSchema } from "@reasn/common/models/InterestDto"; +import ModelMappingError from "../errors/ModelMappingError"; +import { InterestDtoSchema } from "../schemas/InterestDto"; import { z } from "zod"; export const UserInterestDtoSchema = z.object({ diff --git a/Client/reasn-client/packages/common/services/apiServices.ts b/Client/reasn-client/packages/common/src/services/apiServices.ts similarity index 80% rename from Client/reasn-client/packages/common/services/apiServices.ts rename to Client/reasn-client/packages/common/src/services/apiServices.ts index a51cf5ac..93eab258 100644 --- a/Client/reasn-client/packages/common/services/apiServices.ts +++ b/Client/reasn-client/packages/common/src/services/apiServices.ts @@ -1,8 +1,14 @@ -import { getAuthDataFromSessionStorage } from "@reasn/common/services/authorizationServices"; -import { HttpMethod } from "@reasn/common/enums/serviceEnums"; -import ApiConnectionError from "@reasn/common/errors/ApiConnectionError"; +import { getAuthDataFromSessionStorage } from "../services/authorizationServices"; +import { HttpMethod } from "../enums/serviceEnums"; +import ApiConnectionError from "../errors/ApiConnectionError"; import fetch from "cross-fetch"; -import ApiAuthorizationError from "@reasn/common/errors/ApiAuthorizationError"; +import ApiAuthorizationError from "../errors/ApiAuthorizationError"; + +type FetchOptions = { + method: HttpMethod; + headers: { [key: string]: string }; + body?: string; +}; /** * Sends an HTTP request to the specified URL. @@ -21,7 +27,7 @@ export const sendRequest = async ( authRequired: boolean = false, ): Promise => { try { - let headers = {}; + let headers: { [key: string]: string } = {}; if (authRequired) { const authData = getAuthDataFromSessionStorage(); if (!authData) { @@ -35,7 +41,7 @@ export const sendRequest = async ( const fetchOptions = { method: httpMethod, headers: headers, - }; + } as FetchOptions; if (httpMethod === HttpMethod.POST || httpMethod === HttpMethod.PUT) { fetchOptions["body"] = JSON.stringify(bodyData); diff --git a/Client/reasn-client/packages/common/services/authorizationServices.ts b/Client/reasn-client/packages/common/src/services/authorizationServices.ts similarity index 89% rename from Client/reasn-client/packages/common/services/authorizationServices.ts rename to Client/reasn-client/packages/common/src/services/authorizationServices.ts index a4c16ccc..07cb5128 100644 --- a/Client/reasn-client/packages/common/services/authorizationServices.ts +++ b/Client/reasn-client/packages/common/src/services/authorizationServices.ts @@ -1,5 +1,5 @@ -import { UserRole } from "@reasn/common/enums/modelsEnums"; -import { AuthData } from "@reasn/common/interfaces/AuthData"; +import { UserRole } from "../enums/schemasEnums"; +import { AuthData } from "../interfaces/AuthData"; const AUTH_DATA_KEY = "REASN_AUTH_DATA"; diff --git a/Client/reasn-client/packages/common/tsconfig.json b/Client/reasn-client/packages/common/tsconfig.json new file mode 100644 index 00000000..fddddc77 --- /dev/null +++ b/Client/reasn-client/packages/common/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@reasn/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/Client/reasn-client/packages/ui/src/icons/Fire.tsx b/Client/reasn-client/packages/ui/src/icons/Fire.tsx index 4ae6646b..19a04399 100644 --- a/Client/reasn-client/packages/ui/src/icons/Fire.tsx +++ b/Client/reasn-client/packages/ui/src/icons/Fire.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { IconProps } from "./IconProps"; export const Fire = (props: IconProps) => { const { className, colors, gradientTransform } = props; diff --git a/Client/reasn-client/packages/ui/src/icons/IconProps.ts b/Client/reasn-client/packages/ui/src/icons/IconProps.ts index 15c96d35..b8283f2b 100644 --- a/Client/reasn-client/packages/ui/src/icons/IconProps.ts +++ b/Client/reasn-client/packages/ui/src/icons/IconProps.ts @@ -1,4 +1,4 @@ -type IconProps = { +export type IconProps = { className?: string; colors?: string[]; gradientTransform?: string; diff --git a/Client/reasn-client/yarn.lock b/Client/reasn-client/yarn.lock index 0e2cc3a5..1326e348 100644 --- a/Client/reasn-client/yarn.lock +++ b/Client/reasn-client/yarn.lock @@ -4144,13 +4144,19 @@ __metadata: languageName: node linkType: hard -"@reasn/common@workspace:packages/common": +"@reasn/common@npm:*, @reasn/common@workspace:packages/common": version: 0.0.0-use.local resolution: "@reasn/common@workspace:packages/common" + dependencies: + "@reasn/typescript-config": "workspace:*" + jest: "npm:^29.7.0" + jest-fetch-mock: "npm:^3.0.3" + typescript: "npm:^5.3.3" + zod: "npm:^3.23.7" languageName: unknown linkType: soft -"@reasn/typescript-config@npm:*, @reasn/typescript-config@workspace:packages/typescript-config": +"@reasn/typescript-config@npm:*, @reasn/typescript-config@workspace:*, @reasn/typescript-config@workspace:packages/typescript-config": version: 0.0.0-use.local resolution: "@reasn/typescript-config@workspace:packages/typescript-config" languageName: unknown @@ -4764,6 +4770,13 @@ __metadata: languageName: node linkType: hard +"@types/url-search-params@npm:^1.1.2": + version: 1.1.2 + resolution: "@types/url-search-params@npm:1.1.2" + checksum: 10c0/3575548b8d94b7f4509fc1645d4c32d75c12a180ed86a67d3521f730fcdddba67de9e43d282ff22d00cddf2dbc1dbb91b3e3cda698fdf3acd697c5124ebecd3e + languageName: node + linkType: hard + "@types/ws@npm:^8.5.5": version: 8.5.10 resolution: "@types/ws@npm:8.5.10" @@ -11668,6 +11681,13 @@ __metadata: languageName: node linkType: hard +"jwt-decode@npm:^4.0.0": + version: 4.0.0 + resolution: "jwt-decode@npm:4.0.0" + checksum: 10c0/de75bbf89220746c388cf6a7b71e56080437b77d2edb29bae1c2155048b02c6b8c59a3e5e8d6ccdfd54f0b8bda25226e491a4f1b55ac5f8da04cfbadec4e546c + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -17452,15 +17472,18 @@ __metadata: version: 0.0.0-use.local resolution: "web@workspace:apps/web" dependencies: + "@reasn/common": "npm:*" "@reasn/typescript-config": "npm:*" "@reasn/ui": "npm:*" "@types/node": "npm:^20.10.6" "@types/react": "npm:^18.2.46" "@types/react-dom": "npm:^18.2.18" + "@types/url-search-params": "npm:^1.1.2" babel-plugin-react-native-web: "npm:^0.19.10" eslint: "npm:^8.57.0" eslint-config-next: "npm:14.0.4" eslint-config-prettier: "npm:^9.1.0" + jwt-decode: "npm:^4.0.0" next: "npm:^14.1.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" diff --git a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs index 3e3d3709..835f45d4 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/EventService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/EventService.cs @@ -20,9 +20,9 @@ public EventDto CreateEvent(EventDto eventDto) var newEvent = eventDto.ToEntity(); newEvent.CreatedAt = createdTime; newEvent.UpdatedAt = createdTime; - newEvent.Slug = CreateSlug(eventDto); - - + newEvent.Slug = CreateSlug(eventDto); + + context.Events.Add(newEvent); context.SaveChanges(); diff --git a/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs b/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs index f38c8534..50b75343 100644 --- a/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs +++ b/Server/ReasnAPI/ReasnAPI/Services/ParameterService.cs @@ -35,7 +35,7 @@ public ParameterDto UpdateParameter(int parameterId, ParameterDto parameterDto) var parameterCheck = parameters.FirstOrDefault(r => r.Parameters.Any(p => p.Id == parameterId)); - if (parameterCheck is not null) + if (parameterCheck is not null) { throw new BadRequestException("Parameter is associated with an event"); }