BLOG-136 Dashboard route and frontend authentication #137
@ -1 +1 @@
|
||||
PUBLIC_API_BASE_URL=http://127.0.0.1:5173/api/
|
||||
PUBLIC_API_BASE_URL=http://localhost:5173/api/
|
||||
|
1
frontend/src/app.d.ts
vendored
1
frontend/src/app.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
5
frontend/src/lib/auth/adapter/gateway/authApiService.ts
Normal file
5
frontend/src/lib/auth/adapter/gateway/authApiService.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto';
|
||||
|
||||
export interface AuthApiService {
|
||||
getCurrentUser(): Promise<UserResponseDto | null>;
|
||||
}
|
12
frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts
Normal file
12
frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts
Normal file
@ -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<User | null> {
|
||||
const result = await this.authApiService.getCurrentUser();
|
||||
return result?.toEntity() ?? null;
|
||||
}
|
||||
}
|
37
frontend/src/lib/auth/adapter/gateway/userResponseDto.ts
Normal file
37
frontend/src/lib/auth/adapter/gateway/userResponseDto.ts
Normal file
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
59
frontend/src/lib/auth/adapter/presenter/authBloc.ts
Normal file
59
frontend/src/lib/auth/adapter/presenter/authBloc.ts
Normal file
@ -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<AuthViewModel>;
|
||||
export type AuthEvent = CurrentUserLoadedEvent;
|
||||
|
||||
export class AuthBloc {
|
||||
private readonly state = writable<AuthState>({
|
||||
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<AuthState> {
|
||||
switch (event.event) {
|
||||
case AuthEventType.CurrentUserLoadedEvent:
|
||||
return this.loadCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCurrentUser(): Promise<AuthState> {
|
||||
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;
|
||||
}
|
33
frontend/src/lib/auth/adapter/presenter/authViewModel.ts
Normal file
33
frontend/src/lib/auth/adapter/presenter/authViewModel.ts
Normal file
@ -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;
|
||||
}
|
43
frontend/src/lib/auth/adapter/presenter/userViewModel.ts
Normal file
43
frontend/src/lib/auth/adapter/presenter/userViewModel.ts
Normal file
@ -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;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import type { User } from '$lib/auth/domain/entity/user';
|
||||
|
||||
export interface AuthRepository {
|
||||
getCurrentUser(): Promise<User | null>;
|
||||
}
|
@ -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<User | null> {
|
||||
return await this.authRepository.getCurrentUser();
|
||||
}
|
||||
}
|
11
frontend/src/lib/auth/domain/entity/user.ts
Normal file
11
frontend/src/lib/auth/domain/entity/user.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
20
frontend/src/lib/auth/framework/api/authApiServiceImpl.ts
Normal file
20
frontend/src/lib/auth/framework/api/authApiServiceImpl.ts
Normal file
@ -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<UserResponseDto | null> {
|
||||
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);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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<PostInfoResponseDto[]> {
|
||||
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<PostResponseDto | null> {
|
||||
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;
|
||||
|
40
frontend/src/routes/dashboard/+layout.svelte
Normal file
40
frontend/src/routes/dashboard/+layout.svelte
Normal file
@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||
import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl';
|
||||
import { AuthBloc, AuthEventType } from '$lib/auth/adapter/presenter/authBloc';
|
||||
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||
import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import type { LayoutProps } from './$types';
|
||||
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
||||
|
||||
const { children }: LayoutProps = $props();
|
||||
|
||||
const authApiService: AuthApiService = new AuthApiServiceImpl(fetch);
|
||||
const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService);
|
||||
const getcurrentUserUseCase = new GetCurrentUserUseCase(authRepository);
|
||||
const authBloc = new AuthBloc(getcurrentUserUseCase);
|
||||
|
||||
setContext(AuthBloc.name, authBloc);
|
||||
|
||||
const authState = $derived($authBloc);
|
||||
|
||||
onMount(() => authBloc.dispatch({ event: AuthEventType.CurrentUserLoadedEvent }));
|
||||
|
||||
const hasError = $derived.by(() => {
|
||||
if (authState.status === StatusType.Error) {
|
||||
return true;
|
||||
}
|
||||
if (authState.status === StatusType.Success && !authState.data.isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hasError}
|
||||
<div>Error</div>
|
||||
{:else}
|
||||
{@render children()}
|
||||
{/if}
|
1
frontend/src/routes/dashboard/+page.svelte
Normal file
1
frontend/src/routes/dashboard/+page.svelte
Normal file
@ -0,0 +1 @@
|
||||
<div>Dashboard</div>
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user