diff --git a/frontend/.env.example b/frontend/.env.example index 76e9364..a80e001 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1 @@ -PUBLIC_API_BASE_URL=http://127.0.0.1:5173/api/ +PUBLIC_API_BASE_URL=http://localhost:5173/api/ diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index a20b327..3d0dcb6 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} interface Locals { + authBloc: import('$lib/auth/adapter/presenter/authBloc').AuthBloc; postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc; postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc; } diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 191c3d1..e6675b7 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -8,6 +8,14 @@ import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; import type { Handle } from '@sveltejs/kit'; import { Environment } from '$lib/environment'; +import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl'; +import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl'; +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; +import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc'; +import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; Sentry.init({ dsn: Environment.SENTRY_DSN, @@ -16,11 +24,16 @@ Sentry.init({ }); export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => { - const postApiService = new PostApiServiceImpl(event.fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const authApiService: AuthApiService = new AuthApiServiceImpl(event.fetch); + const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService); + const getCurrentUserUseCase = new GetCurrentUserUseCase(authRepository); + + const postApiService: PostApiService = new PostApiServiceImpl(event.fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); const getPostUseCase = new GetPostUseCase(postRepository); + event.locals.authBloc = new AuthBloc(getCurrentUserUseCase); event.locals.postListBloc = new PostListBloc(getAllPostsUseCase); event.locals.postBloc = new PostBloc(getPostUseCase); diff --git a/frontend/src/lib/auth/adapter/gateway/authApiService.ts b/frontend/src/lib/auth/adapter/gateway/authApiService.ts new file mode 100644 index 0000000..92cb76f --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/authApiService.ts @@ -0,0 +1,5 @@ +import type { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; + +export interface AuthApiService { + getCurrentUser(): Promise; +} diff --git a/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts b/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts new file mode 100644 index 0000000..40ea61c --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts @@ -0,0 +1,12 @@ +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { User } from '$lib/auth/domain/entity/user'; + +export class AuthRepositoryImpl implements AuthRepository { + constructor(private readonly authApiService: AuthApiService) {} + + async getCurrentUser(): Promise { + const result = await this.authApiService.getCurrentUser(); + return result?.toEntity() ?? null; + } +} diff --git a/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts new file mode 100644 index 0000000..8f64369 --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts @@ -0,0 +1,37 @@ +import { User } from '$lib/auth/domain/entity/user'; +import z from 'zod'; + +export const UserResponseSchema = z.object({ + id: z.int32(), + displayed_name: z.string(), + email: z.email(), +}); + +export class UserResponseDto { + readonly id: number; + readonly displayedName: string; + readonly email: string; + + private constructor(props: { id: number; displayedName: string; email: string }) { + this.id = props.id; + this.displayedName = props.displayedName; + this.email = props.email; + } + + static fromJson(json: unknown): UserResponseDto { + const parsedJson = UserResponseSchema.parse(json); + return new UserResponseDto({ + id: parsedJson.id, + displayedName: parsedJson.displayed_name, + email: parsedJson.email, + }); + } + + toEntity(): User { + return new User({ + id: this.id, + name: this.displayedName, + email: this.email, + }); + } +} diff --git a/frontend/src/lib/auth/adapter/presenter/authBloc.ts b/frontend/src/lib/auth/adapter/presenter/authBloc.ts new file mode 100644 index 0000000..a793cf3 --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/authBloc.ts @@ -0,0 +1,59 @@ +import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel'; +import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel'; +import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; +import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { get, writable } from 'svelte/store'; + +export type AuthState = AsyncState; +export type AuthEvent = CurrentUserLoadedEvent; + +export class AuthBloc { + private readonly state = writable({ + status: StatusType.Idle, + }); + + constructor( + private readonly getCurrentUserUseCase: GetCurrentUserUseCase, + initialData?: AuthViewModel + ) { + this.state.set({ + status: StatusType.Idle, + data: initialData, + }); + } + + get subscribe() { + return this.state.subscribe; + } + + async dispatch(event: AuthEvent): Promise { + switch (event.event) { + case AuthEventType.CurrentUserLoadedEvent: + return this.loadCurrentUser(); + } + } + + private async loadCurrentUser(): Promise { + this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + + const user = await this.getCurrentUserUseCase.execute(); + + const userViewModel = user ? UserViewModel.fromEntity(user) : null; + const authViewModel = AuthViewModel.fromEntity(userViewModel); + const result: AuthState = { + status: StatusType.Success, + data: authViewModel, + }; + + this.state.set(result); + return result; + } +} + +export enum AuthEventType { + CurrentUserLoadedEvent, +} + +interface CurrentUserLoadedEvent { + event: AuthEventType.CurrentUserLoadedEvent; +} diff --git a/frontend/src/lib/auth/adapter/presenter/authViewModel.ts b/frontend/src/lib/auth/adapter/presenter/authViewModel.ts new file mode 100644 index 0000000..ec983dc --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/authViewModel.ts @@ -0,0 +1,33 @@ +import { UserViewModel, type DehydratedUserProps } from '$lib/auth/adapter/presenter/userViewModel'; + +export class AuthViewModel { + readonly user: UserViewModel | null; + + constructor(params: { user: UserViewModel | null }) { + this.user = params.user; + } + + static fromEntity(user: UserViewModel | null): AuthViewModel { + return new AuthViewModel({ user }); + } + + static rehydrate(props: DehydratedAuthProps): AuthViewModel { + return new AuthViewModel({ + user: props.user ? UserViewModel.rehydrate(props.user) : null, + }); + } + + dehydrate(): DehydratedAuthProps { + return { + user: this.user ? this.user.dehydrate() : null, + }; + } + + get isAuthenticated(): boolean { + return this.user !== null; + } +} + +export interface DehydratedAuthProps { + user: DehydratedUserProps | null; +} diff --git a/frontend/src/lib/auth/adapter/presenter/userViewModel.ts b/frontend/src/lib/auth/adapter/presenter/userViewModel.ts new file mode 100644 index 0000000..b3def38 --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/userViewModel.ts @@ -0,0 +1,43 @@ +import type { User } from '$lib/auth/domain/entity/user'; + +export class UserViewModel { + readonly id: number; + readonly name: string; + readonly email: string; + + private constructor(props: { id: number; name: string; email: string }) { + this.id = props.id; + this.name = props.name; + this.email = props.email; + } + + static fromEntity(user: User): UserViewModel { + return new UserViewModel({ + id: user.id, + name: user.name, + email: user.email, + }); + } + + static rehydrate(props: DehydratedUserProps): UserViewModel { + return new UserViewModel({ + id: props.id, + name: props.name, + email: props.email, + }); + } + + dehydrate(): DehydratedUserProps { + return { + id: this.id, + name: this.name, + email: this.email, + }; + } +} + +export interface DehydratedUserProps { + id: number; + name: string; + email: string; +} diff --git a/frontend/src/lib/auth/application/gateway/authRepository.ts b/frontend/src/lib/auth/application/gateway/authRepository.ts new file mode 100644 index 0000000..fa3930b --- /dev/null +++ b/frontend/src/lib/auth/application/gateway/authRepository.ts @@ -0,0 +1,5 @@ +import type { User } from '$lib/auth/domain/entity/user'; + +export interface AuthRepository { + getCurrentUser(): Promise; +} diff --git a/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts b/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts new file mode 100644 index 0000000..0cc5711 --- /dev/null +++ b/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts @@ -0,0 +1,10 @@ +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { User } from '$lib/auth/domain/entity/user'; + +export class GetCurrentUserUseCase { + constructor(private readonly authRepository: AuthRepository) {} + + async execute(): Promise { + return await this.authRepository.getCurrentUser(); + } +} diff --git a/frontend/src/lib/auth/domain/entity/user.ts b/frontend/src/lib/auth/domain/entity/user.ts new file mode 100644 index 0000000..43a2157 --- /dev/null +++ b/frontend/src/lib/auth/domain/entity/user.ts @@ -0,0 +1,11 @@ +export class User { + id: number; + name: string; + email: string; + + constructor(props: { id: number; name: string; email: string }) { + this.id = props.id; + this.name = props.name; + this.email = props.email; + } +} diff --git a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts new file mode 100644 index 0000000..ed1ffb2 --- /dev/null +++ b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts @@ -0,0 +1,20 @@ +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; +import { Environment } from '$lib/environment'; + +export class AuthApiServiceImpl implements AuthApiService { + constructor(private readonly fetchFn: typeof fetch) {} + + async getCurrentUser(): Promise { + const url = new URL('me', Environment.API_BASE_URL); + + const response = await this.fetchFn(url); + + if (!response.ok) { + return null; + } + + const json = await response.json(); + return UserResponseDto.fromJson(json); + } +} diff --git a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts index 33723f3..e75dfed 100644 --- a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts +++ b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts @@ -1,5 +1,5 @@ import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; diff --git a/frontend/src/lib/post/application/repository/postRepository.ts b/frontend/src/lib/post/application/gateway/postRepository.ts similarity index 100% rename from frontend/src/lib/post/application/repository/postRepository.ts rename to frontend/src/lib/post/application/gateway/postRepository.ts diff --git a/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts b/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts index f55957c..b585fa5 100644 --- a/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts +++ b/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts @@ -1,4 +1,4 @@ -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export class GetAllPostsUseCase { diff --git a/frontend/src/lib/post/application/useCase/getPostUseCase.ts b/frontend/src/lib/post/application/useCase/getPostUseCase.ts index 7e2a60d..b13d017 100644 --- a/frontend/src/lib/post/application/useCase/getPostUseCase.ts +++ b/frontend/src/lib/post/application/useCase/getPostUseCase.ts @@ -1,4 +1,4 @@ -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; export class GetPostUseCase { diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index 3abeea5..d00a847 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -4,12 +4,12 @@ import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseD import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto'; export class PostApiServiceImpl implements PostApiService { - constructor(private fetchFn: typeof fetch) {} + constructor(private readonly fetchFn: typeof fetch) {} async getAllPosts(): Promise { const url = new URL('post', Environment.API_BASE_URL); - const response = await this.fetchFn(url.href); + const response = await this.fetchFn(url); if (!response.ok) { return []; @@ -22,7 +22,7 @@ export class PostApiServiceImpl implements PostApiService { async getPost(id: string): Promise { const url = new URL(`post/${id}`, Environment.API_BASE_URL); - const response = await this.fetchFn(url.href); + const response = await this.fetchFn(url); if (!response.ok) { return null; diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..296fef2 --- /dev/null +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,40 @@ + + +{#if hasError} +
Error
+{:else} + {@render children()} +{/if} diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..9e01032 --- /dev/null +++ b/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1 @@ +
Dashboard
diff --git a/frontend/src/routes/post/+page.svelte b/frontend/src/routes/post/+page.svelte index 1fb1cf3..965f7ed 100644 --- a/frontend/src/routes/post/+page.svelte +++ b/frontend/src/routes/post/+page.svelte @@ -7,13 +7,15 @@ import type { PageProps } from './$types'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte'; + import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; + import type { PostRepository } from '$lib/post/application/gateway/postRepository'; let { data }: PageProps = $props(); const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post)); - const postApiService = new PostApiServiceImpl(fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const postApiService: PostApiService = new PostApiServiceImpl(fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); const postListBloc = new PostListBloc(getAllPostsUseCase, initialData); diff --git a/frontend/src/routes/post/[id]/+page.svelte b/frontend/src/routes/post/[id]/+page.svelte index da4ea70..5e14b25 100644 --- a/frontend/src/routes/post/[id]/+page.svelte +++ b/frontend/src/routes/post/[id]/+page.svelte @@ -7,14 +7,16 @@ import { setContext } from 'svelte'; import type { PageProps } from './$types'; import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte'; + import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; + import type { PostRepository } from '$lib/post/application/gateway/postRepository'; const { data, params }: PageProps = $props(); const { id } = params; const initialData = PostViewModel.rehydrate(data.dehydratedData!); - const postApiService = new PostApiServiceImpl(fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const postApiService: PostApiService = new PostApiServiceImpl(fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getPostUseCase = new GetPostUseCase(postRepository); const postBloc = new PostBloc(getPostUseCase, initialData);