diff --git a/app/actions/authentication/user-authentication-action.ts b/app/actions/authentication/user-authentication-action.ts index 9fc2925..db67dd1 100644 --- a/app/actions/authentication/user-authentication-action.ts +++ b/app/actions/authentication/user-authentication-action.ts @@ -27,10 +27,8 @@ export default class UserAuthenticationAction { /** * ログアウトする。 - * @returns ログアウトに成功したかどうか。 */ - public async logout(token: string): Promise { - const response = await this.userAccountManager.logout(token); - return response; + public async logout(token: string): Promise { + await this.userAccountManager.logout(token); } } diff --git a/app/actions/authentication/user-registration-action.ts b/app/actions/authentication/user-registration-action.ts index 94bf87d..a8aa94e 100644 --- a/app/actions/authentication/user-registration-action.ts +++ b/app/actions/authentication/user-registration-action.ts @@ -18,20 +18,19 @@ export default class UserRegistrationAction { * ユーザーを登録する。 * @param mailAddress メールアドレス。 * @param password パスワード。 + * @param confirmPassword 再確認パスワード。 * @returns サインアップのレスポンス。 */ - public async register(mailAddress: string, password: string): Promise { - const response = await this.userAccountManager.register(mailAddress, password); + public async register(mailAddress: string, password: string, confirmPassword: string): Promise { + const response = await this.userAccountManager.register(mailAddress, password, confirmPassword); return response; } /** * ユーザーを削除する。 * @param token トークン。 - * @returns 削除に成功したかどうか。 */ - public async delete(token: string): Promise { - const response = await this.userAccountManager.delete(token); - return response; + public async delete(token: string): Promise { + await this.userAccountManager.delete(token); } } diff --git a/app/actions/user/sns-user-registration-action.ts b/app/actions/user/sns-user-registration-action.ts index 7fa5e53..d8c3b52 100644 --- a/app/actions/user/sns-user-registration-action.ts +++ b/app/actions/user/sns-user-registration-action.ts @@ -18,20 +18,16 @@ export default class SnsUserRegistrationAction { * @param authenticationProviderId 認証プロバイダID。 * @param userName ユーザー名。 * @param currentReleaseInformationId 現在のリリース情報ID。 - * @returns 登録に成功したかどうか。 */ - public async register(authenticationProviderId: string, userName: string, currentReleaseInformationId: number): Promise { - const response = await this.userProfileManager.register(authenticationProviderId, userName, currentReleaseInformationId); - return response; + public async register(authenticationProviderId: string, userName: string, currentReleaseInformationId: number): Promise { + await this.userProfileManager.register(authenticationProviderId, userName, currentReleaseInformationId); } /** * ユーザーを削除する。 * @param id ユーザーID。 - * @returns 削除に成功したかどうか。 */ - public async delete(id: number): Promise { - const response = await this.userProfileManager.delete(id); - return response; + public async delete(id: number): Promise { + await this.userProfileManager.delete(id); } } \ No newline at end of file diff --git a/app/actions/user/user-setting-action.ts b/app/actions/user/user-setting-action.ts index 7544885..55f0b5a 100644 --- a/app/actions/user/user-setting-action.ts +++ b/app/actions/user/user-setting-action.ts @@ -17,10 +17,8 @@ export default class UserSettingAction { /** * ユーザー設定を更新する。 * @param userSetting ユーザー設定。 - * @returns 更新に成功したかどうか。 */ - public async editUserSetting(userSetting: UserSetting): Promise { - const response = await this.userProfileManager.editUserSetting(userSetting); - return response; + public async editUserSetting(userSetting: UserSetting): Promise { + await this.userProfileManager.editUserSetting(userSetting); } } \ No newline at end of file diff --git a/app/contexts/system-message/system-message-context.ts b/app/contexts/system-message/system-message-context.ts new file mode 100644 index 0000000..4c0673b --- /dev/null +++ b/app/contexts/system-message/system-message-context.ts @@ -0,0 +1,9 @@ +import { createContext } from "react"; + +/** + * システムメッセージコンテキスト。 + */ +const SystemMessageContext = createContext({ + showSystemMessage: (status: "success" | "error", message: string) => {} +}); +export default SystemMessageContext; \ No newline at end of file diff --git a/app/contexts/system-message/system-message-provider.tsx b/app/contexts/system-message/system-message-provider.tsx new file mode 100644 index 0000000..fae0150 --- /dev/null +++ b/app/contexts/system-message/system-message-provider.tsx @@ -0,0 +1,49 @@ +import SystemMessageContext from "./system-message-context"; +import { ReactNode } from "react"; +import toast, { Toaster, useToaster } from "react-hot-toast"; + +/** + * システムメッセージプロバイダー。 + * @param children 子要素。 + * @returns システムメッセージプロバイダー。 + */ +const SystemMessageProvider = ({ + children, +}: { + children: ReactNode, +}) => { + const showSystemMessage = (status: "success" | "error", message: string) => { + if (!message) return; + switch (status) { + case "success": + toast.success(message); + break; + case "error": + toast.error(message); + break; + } + }; + + return ( + + {children} + + + ); +}; +export default SystemMessageProvider; \ No newline at end of file diff --git a/app/libraries/authentication/firebase-client.ts b/app/libraries/authentication/firebase-client.ts index 14ad855..dda24da 100644 --- a/app/libraries/authentication/firebase-client.ts +++ b/app/libraries/authentication/firebase-client.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import IAuthenticationClient from "./i-authentication-client"; import HttpClient from "../http/http-client"; import SignUpResponse from "../../models/authentication/signup-response"; @@ -24,22 +25,17 @@ export default class FirebaseClient implements IAuthenticationClient { constructor() { if (process.env.RUN_INFRA_TESTS) { if (!process.env.TEST_FIREBASE_API_KEY) { - throw new Error("TEST_FIREBASE_API_KEYが設定されていません。"); + throw new Error(systemMessages.error.authenticationProviderTestEnvironmentVariableError); } process.env.FIREBASE_API_KEY = process.env.TEST_FIREBASE_API_KEY; } if (!process.env.FIREBASE_API_KEY) { - throw new Error("FIREBASE_API_KEYが設定されていません。"); + throw new Error(systemMessages.error.authenticationProviderEnvironmentVariableError); } this.firebaseApiKey = process.env.FIREBASE_API_KEY; } public async signUp(mailAddress: string, password: string): Promise { - // パスワードの長さが8文字未満、英数字が含まれていない場合、エラーを投げる。 - if (password.length < 8 || !password.match(/^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i)) { - throw new Error("パスワードは8文字以上の英数字で設定してください。"); - } - // メールアドレスとパスワードでサインアップする。 const endpoint = "v1/accounts:signUp"; const queries = { diff --git a/app/libraries/authentication/sign-up-validator.ts b/app/libraries/authentication/sign-up-validator.ts new file mode 100644 index 0000000..d562aea --- /dev/null +++ b/app/libraries/authentication/sign-up-validator.ts @@ -0,0 +1,22 @@ +import systemMessages from "../../messages/system-messages"; + +/** + * サインアップのバリデーションを行うクラス。 + */ +export default class SignUpValidator { + /** + * サインアップのバリデーションを行う。 + * @param password パスワード。 + * @param confirmPassword 再確認パスワード。 + * @returns バリデーション結果。 + * @throws バリデーションに失敗した場合、エラーを投げる。 + */ + public static validate(password: string, confirmPassword: string): boolean { + // パスワードの長さが8文字未満、英数字が含まれていない場合、エラーを投げる。 + if (password.length < 8 || !password.match(/^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i)) throw new Error(systemMessages.error.invalidPasswordOnSetting); + + // パスワードと再確認パスワードが一致しない場合、エラーを返す。 + if (password !== confirmPassword) throw new Error(systemMessages.error.passwordMismatch); + return true; + } +} \ No newline at end of file diff --git a/app/libraries/authentication/user-account-manager.ts b/app/libraries/authentication/user-account-manager.ts index 34f2f1b..4eff29d 100644 --- a/app/libraries/authentication/user-account-manager.ts +++ b/app/libraries/authentication/user-account-manager.ts @@ -1,7 +1,8 @@ +import systemMessages from "../../messages/system-messages"; import SignUpResponse from "../../models/authentication/signup-response"; import IAuthenticationClient from "./i-authentication-client"; import SignInWithEmailPasswordResponse from "../../models/authentication/signin-with-email-password-response"; - +import SignUpValidator from "./sign-up-validator"; /** * ユーザー管理を行うクラス。 @@ -20,21 +21,37 @@ export default class UserAccountManager { * ユーザーを登録する。 * @param mailAddress メールアドレス。 * @param password パスワード。 + * @param confirmPassword 再確認パスワード。 * @returns サインアップのレスポンス。 */ - public async register(mailAddress: string, password: string): Promise { - const response = await this.authenticationClient.signUp(mailAddress, password); - return response; + public async register(mailAddress: string, password: string, confirmPassword: string): Promise { + try { + // ユーザー登録のバリデーションを行う。 + SignUpValidator.validate(password, confirmPassword); + + // ユーザーを登録する。 + const response = await this.authenticationClient.signUp(mailAddress, password); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.signUpFailed); + } } /** * ユーザーを削除する。 * @param token トークン。 - * @returns 削除に成功したかどうか。 */ - public async delete(token: string): Promise { - const response = await this.authenticationClient.deleteUser(token); - return response; + public async delete(token: string): Promise { + try { + const response = await this.authenticationClient.deleteUser(token); + if (!response) throw new Error(systemMessages.error.authenticationUserDeletionFailed); + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.authenticationUserDeletionFailed); + } } /** @@ -44,16 +61,26 @@ export default class UserAccountManager { * @returns メールアドレスとパスワードでサインインのレスポンス。 */ public async login(mailAddress: string, password: string): Promise { - const response = await this.authenticationClient.signInWithEmailPassword(mailAddress, password); - return response; + try { + const response = await this.authenticationClient.signInWithEmailPassword(mailAddress, password); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.invalidMailAddressOrPassword); + } } /** * ログアウトする。 * @param token トークン。 - * @returns ログアウトに成功したかどうか。 */ - public async logout(token: string): Promise { - return true; + public async logout(token: string): Promise { + try { + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.logoutFailed); + } } } \ No newline at end of file diff --git a/app/libraries/post/post-interactor.ts b/app/libraries/post/post-interactor.ts index 092e202..cbbdfbb 100644 --- a/app/libraries/post/post-interactor.ts +++ b/app/libraries/post/post-interactor.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import IPostContentRepository from '../../repositories/post/i-post-content-repository'; /** @@ -21,7 +22,13 @@ export default class PostInteractor { * @returns 投稿ID。 */ public async post(posterId: number, releaseInformationId: number, content: string): Promise { - const postId = await this.postContentRepository.create(posterId, releaseInformationId, content); - return postId; + try { + const postId = await this.postContentRepository.create(posterId, releaseInformationId, content); + return postId; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.postFailed); + } } } \ No newline at end of file diff --git a/app/libraries/post/posts-fetcher.ts b/app/libraries/post/posts-fetcher.ts index d920c78..c17e2b6 100644 --- a/app/libraries/post/posts-fetcher.ts +++ b/app/libraries/post/posts-fetcher.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import PostContent from '../../models/post/post-content'; import IPostContentRepository from '../../repositories/post/i-post-content-repository'; @@ -18,16 +19,24 @@ export default class PostsFetcher { * ユーザーが設定したリリースバージョン以下の最新の投稿を指定された数取得する。 * @param profileId プロフィールID。 * @param numberOfPosts 取得する投稿数。 + * @returns 取得した投稿。 */ public async fetchLatestPosts(profileId: string, numberOfPosts: number): Promise { - const posts = await this.postContentRepository.getLatestLimited(profileId, numberOfPosts); - return posts; + try { + const posts = await this.postContentRepository.getLatestLimited(profileId, numberOfPosts); + return posts; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.postRetrievalFailed); + } } /** * ユーザーが設定したリリースバージョン以下の指定された投稿ID以前の投稿を指定された数取得する。 * @param postId 投稿ID。 * @param numberOfPosts 取得する投稿数。 + * @returns 取得した投稿。 */ public async fetchPostsById(postId: number, numberOfPosts: number): Promise { throw new Error('Method not implemented.'); diff --git a/app/libraries/post/release-information-getter.ts b/app/libraries/post/release-information-getter.ts index a28282e..8b67828 100644 --- a/app/libraries/post/release-information-getter.ts +++ b/app/libraries/post/release-information-getter.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import ReleaseInformation from "../../models/post/release-information"; import IReleaseInformationRepository from "../../repositories/post/i-release-information-repository"; @@ -16,18 +17,32 @@ export default class ReleaseInformationGetter { /** * リリース情報を取得する。 * @param releaseInformationId リリース情報ID。 + * @returns リリース情報。 */ public async getReleaseInformation(releaseInformationId: number): Promise { - const response = await this.releaseInformationRepository.get(releaseInformationId); - return response; + try { + const response = await this.releaseInformationRepository.get(releaseInformationId); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.releaseInformationRetrievalFailed); + } } /** * リリース情報を全件取得する。 + * @returns 全てのリリース情報。 */ public async getAllReleaseInformation(): Promise { - const response = await this.releaseInformationRepository.getAll(); - return response; + try { + const response = await this.releaseInformationRepository.getAll(); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.releaseInformationRetrievalFailed); + } } /** @@ -36,7 +51,13 @@ export default class ReleaseInformationGetter { * @returns ユーザーが設定したリリースバージョン以下のリリース情報。 */ public async getReleaseInformationBelowUserSetting(profileId: string): Promise { - const response = await this.releaseInformationRepository.getBelowUserSetting(profileId); - return response; + try { + const response = await this.releaseInformationRepository.getBelowUserSetting(profileId); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.releaseInformationRetrievalFailed); + } } } \ No newline at end of file diff --git a/app/libraries/user/authenticated-user-provider.ts b/app/libraries/user/authenticated-user-provider.ts index 5e00733..9bb2fc2 100644 --- a/app/libraries/user/authenticated-user-provider.ts +++ b/app/libraries/user/authenticated-user-provider.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import AuthenticatedUser from "../../models/user/authenticated-user"; import IAuthenticationClient from "../authentication/i-authentication-client"; import IUserRepository from "../../repositories/user/i-user-repository"; @@ -24,48 +25,60 @@ export default class AuthenticatedUserProvider { * ユーザーが存在しない場合、nullを返す。 */ public async getUserByToken(token: string): Promise { - // ユーザー情報を取得する。 - const clientResponse = await this.authenticationClient.getUserInformation(token); - const repositoryResponse = await this.userRepository.findByAuthenticationProviderId(clientResponse.users[0].localId); + try { + // ユーザー情報を取得する。 + const clientResponse = await this.authenticationClient.getUserInformation(token); + const repositoryResponse = await this.userRepository.findByAuthenticationProviderId(clientResponse.users[0].localId); - // ユーザーが存在しない場合、nullを返す。 - if (repositoryResponse === null) return null; + // ユーザー情報が存在しない場合、nullを返す。 + if (repositoryResponse === null) return null; - // ユーザー情報を生成する。 - const authenticatedUser = { - id: repositoryResponse.id, - profileId: repositoryResponse.profileId, - authenticationProviderId: repositoryResponse.authenticationProviderId, - userName: repositoryResponse.userName, - currentReleaseVersion: repositoryResponse.currentReleaseVersion, - currentReleaseName: repositoryResponse.currentReleaseName, - createdAt: new Date(Number(repositoryResponse.createdAt)), - }; - return authenticatedUser; + // 認証済みユーザーを生成する。 + const authenticatedUser = { + id: repositoryResponse.id, + profileId: repositoryResponse.profileId, + authenticationProviderId: repositoryResponse.authenticationProviderId, + userName: repositoryResponse.userName, + currentReleaseVersion: repositoryResponse.currentReleaseVersion, + currentReleaseName: repositoryResponse.currentReleaseName, + createdAt: new Date(Number(repositoryResponse.createdAt)), + }; + return authenticatedUser; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userInformationRetrievalFailed); + } } /** * プロフィールIDで認証済みユーザーを取得する。 * @param profileId プロフィールID。 */ - public async getUserByProfileId(profileId: string): Promise { - // ユーザー情報を取得する。 - const repositoryResponse = await this.userRepository.findByProfileId(profileId); + public async getUserByProfileId(profileId: string): Promise { + try { + // ユーザー情報を取得する。 + const repositoryResponse = await this.userRepository.findByProfileId(profileId); - // ユーザーが存在しない場合、nullを返す。 - if (repositoryResponse === null) return null; + // ユーザー情報が存在しない場合、nullを返す。 + if (repositoryResponse === null) throw new Error(systemMessages.error.userNotExists); - // ユーザー情報を生成する。 - const authenticatedUser = { - id: repositoryResponse.id, - profileId: repositoryResponse.profileId, - authenticationProviderId: repositoryResponse.authenticationProviderId, - userName: repositoryResponse.userName, - currentReleaseVersion: repositoryResponse.currentReleaseVersion, - currentReleaseName: repositoryResponse.currentReleaseName, - createdAt: new Date(Number(repositoryResponse.createdAt)), - }; - return authenticatedUser; + // 認証済みユーザーを生成する。 + const authenticatedUser = { + id: repositoryResponse.id, + profileId: repositoryResponse.profileId, + authenticationProviderId: repositoryResponse.authenticationProviderId, + userName: repositoryResponse.userName, + currentReleaseVersion: repositoryResponse.currentReleaseVersion, + currentReleaseName: repositoryResponse.currentReleaseName, + createdAt: new Date(Number(repositoryResponse.createdAt)), + }; + return authenticatedUser; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userInformationRetrievalFailed); + } } /** @@ -74,11 +87,17 @@ export default class AuthenticatedUserProvider { * @returns 認証プロバイダID。 */ public async getAuthenticationProviderId(token: string): Promise { - // ユーザー情報を取得する。 - const clientResponse = await this.authenticationClient.getUserInformation(token); + try { + // ユーザー情報を取得する。 + const clientResponse = await this.authenticationClient.getUserInformation(token); - // 認証プロバイダIDを取得する。 - const authenticationProviderId = clientResponse.users[0].localId; - return authenticationProviderId; + // 認証プロバイダIDを取得する。 + const authenticationProviderId = clientResponse.users[0].localId; + return authenticationProviderId; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.authenticationFailed); + } } } \ No newline at end of file diff --git a/app/libraries/user/user-profile-manager.ts b/app/libraries/user/user-profile-manager.ts index 2f22828..83bebd9 100644 --- a/app/libraries/user/user-profile-manager.ts +++ b/app/libraries/user/user-profile-manager.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import IUserRepository from "../../repositories/user/i-user-repository"; import UserRegistrationValidator from "./user-registration-validator"; import ProfileIdCreator from "./profile-id-creator"; @@ -21,36 +22,51 @@ export default class UserProfileManager { * @param authenticationProviderId 認証プロバイダID。 * @param userName ユーザー名。 * @param currentReleaseInformationId 現在のリリース情報ID。 - * @returns 登録に成功したかどうか。 */ - public async register(authenticationProviderId: string, userName: string, currentReleaseInformationId: number): Promise { - // ユーザー登録バリデーションを行う。 - UserRegistrationValidator.validate(authenticationProviderId, userName); + public async register(authenticationProviderId: string, userName: string, currentReleaseInformationId: number): Promise { + try { + // ユーザー登録のバリデーションを行う。 + UserRegistrationValidator.validate(authenticationProviderId, userName); - // ユーザーを登録する。 - const profileId = ProfileIdCreator.create(userName); - const response = await this.userRepository.create(profileId, authenticationProviderId, userName, currentReleaseInformationId); - return response; + // ユーザーを登録する。 + const profileId = ProfileIdCreator.create(userName); + const response = await this.userRepository.create(profileId, authenticationProviderId, userName, currentReleaseInformationId); + if (!response) throw new Error(systemMessages.error.userRegistrationFailed); + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userRegistrationFailed); + } } /** * ユーザー設定を更新する。 * @param userSetting ユーザー設定。 - * @returns 更新に成功したかどうか。 */ - public async editUserSetting(userSetting: UserSetting): Promise { - const response = await this.userRepository.update(userSetting); - return response; + public async editUserSetting(userSetting: UserSetting): Promise { + try { + const response = await this.userRepository.update(userSetting); + if (!response) throw new Error(systemMessages.error.userSettingEditFailed); + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userSettingEditFailed); + } } /** * ユーザーを削除する。 * @param id ユーザーID。 - * @returns 削除に成功したかどうか。 */ - public async delete(id: number): Promise { - const response = await this.userRepository.delete(id); - return response; + public async delete(id: number): Promise { + try { + const response = await this.userRepository.delete(id); + if (!response) throw new Error(systemMessages.error.userDeletionFailed); + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userDeletionFailed); + } } /** @@ -59,8 +75,14 @@ export default class UserProfileManager { * @returns ユーザー設定。 */ public async fetchUserSettingByProfileId(profileId: string): Promise { - const response = await this.userRepository.findUserSettingByProfileId(profileId); - if (response == null) throw new Error("ユーザー設定が存在しません。"); - return response; + try { + const response = await this.userRepository.findUserSettingByProfileId(profileId); + if (response == null) throw new Error(systemMessages.error.userSettingRetrievalFailed); + return response; + } catch (error) { + console.error(error); + if (error instanceof TypeError) throw new Error(systemMessages.error.networkError); + throw new Error(systemMessages.error.userSettingRetrievalFailed); + } } } \ No newline at end of file diff --git a/app/libraries/user/user-registration-validator.ts b/app/libraries/user/user-registration-validator.ts index c852fbc..0ad87f2 100644 --- a/app/libraries/user/user-registration-validator.ts +++ b/app/libraries/user/user-registration-validator.ts @@ -1,3 +1,5 @@ +import systemMessages from "../../messages/system-messages"; + /** * ユーザー登録のバリデーター。 */ @@ -11,11 +13,11 @@ export default class UserRegistrationValidator { */ public static validate(authenticationProviderId: string, userName: string): boolean { // 認証プロバイダIDが不正な場合、エラーを投げる。 - if (!authenticationProviderId) throw new Error("認証プロバイダIDは必須です。"); + if (!authenticationProviderId) throw new Error(systemMessages.error.authenticationFailed); // ユーザー名が不正な場合、エラーを投げる。 const regex = /^[a-zA-Z0-9]*@{1}[a-zA-Z0-9]*$/; - if (!regex.test(userName)) throw new Error("ユーザー名は「username@world」で入力してください。"); + if (!regex.test(userName)) throw new Error(systemMessages.error.invalidUserName); // バリデーションを通過した場合、trueを返す。 return true; diff --git a/app/loaders/user/authenticated-user-loader.ts b/app/loaders/user/authenticated-user-loader.ts index 9b6ac0a..e8458e0 100644 --- a/app/loaders/user/authenticated-user-loader.ts +++ b/app/loaders/user/authenticated-user-loader.ts @@ -30,7 +30,7 @@ export default class AuthenticatedUserLoader { * @param profileId プロフィールID。 * @returns 認証済みユーザー。 */ - public async getUserByProfileId(profileId: string): Promise { + public async getUserByProfileId(profileId: string): Promise { const authenticatedUser = await this.authenticatedUserProvider.getUserByProfileId(profileId); return authenticatedUser; } diff --git a/app/messages/messages.ts b/app/messages/messages.ts new file mode 100644 index 0000000..5df906b --- /dev/null +++ b/app/messages/messages.ts @@ -0,0 +1,18 @@ +/** + * メッセージを保持するインターフェース。 + */ +export default interface Messages { + /** + * エラーメッセージ。 + */ + error: { + [key: string]: string; + }; + + /** + * 成功メッセージ。 + */ + success: { + [key: string]: string; + }; +} \ No newline at end of file diff --git a/app/messages/system-messages.ts b/app/messages/system-messages.ts new file mode 100644 index 0000000..46df1bd --- /dev/null +++ b/app/messages/system-messages.ts @@ -0,0 +1,47 @@ +import Messages from './messages'; + +/** + * システムメッセージ。 + */ +const systemMessages: Messages = { + error: { + unknownError: "不明なエラーが発生しました。", + networkError: "ネットワークエラーが発生しました。", + authenticationProviderError: "認証プロバイダが利用できません。", + authenticationProviderEnvironmentVariableError: "認証プロバイダの環境変数が設定されていません。", + authenticationProviderTestEnvironmentVariableError: "認証プロバイダのテスト環境変数が設定されていません。", + databaseError: "データベースエラーが発生しました。", + databaseEnvironmentVariableError: "データベースの環境変数が設定されていません。", + databaseTestEnvironmentVariableError: "データベースのテスト環境変数が設定されていません。", + databasePoolError: "データベースプールでエラーが発生しました。", + databaseClientError: "データベースクライアントでエラーが発生しました。", + invalidMailAddress: "メールアドレスが不正です。", + invalidPasswordOnSetting: "パスワードは8文字以上の英数字で設定してください。", + passwordMismatch: "パスワードが一致していません。", + signUpFailed: "サインアップに失敗しました。", + authenticationUserDeletionFailed: "認証ユーザーの削除に失敗しました。", + invalidMailAddressOrPassword: "メールアドレスまたはパスワードが間違っています。", + authenticationFailed: "認証に失敗しました。ログインし直してください。", + authenticationProviderIdRetrievalFailed: "認証プロバイダIDの取得に失敗しました。", + logoutFailed: "ログアウトに失敗しました。", + invalidUserName: "ユーザー名は「username@world」で入力してください。", + userRegistrationFailed: "ユーザー登録に失敗しました。", + userNotExists: "ユーザーが存在しません。", + userDeletionFailed: "ユーザーの削除に失敗しました。", + userInformationRetrievalFailed: "ユーザー情報の取得に失敗しました。", + userSettingRegistrationFailed: "ユーザー設定の登録に失敗しました。", + userSettingEditFailed: "ユーザー設定の編集に失敗しました。", + userSettingDeletionFailed: "ユーザー設定の削除に失敗しました。", + userSettingRetrievalFailed: "ユーザー設定の取得に失敗しました。", + postContentNotInputted: "投稿内容が入力されていません。", + postFailed: "投稿に失敗しました。", + postRetrievalFailed: "投稿の取得に失敗しました。", + postDeletionFailed: "投稿の削除に失敗しました。", + releaseInformationRetrievalFailed: "リリース情報の取得に失敗しました。", + releaseInformationNotExists: "リリース情報が存在しません。", + }, + success: { + userSettingSaved: "ユーザー設定を保存しました。", + }, +} +export default systemMessages; \ No newline at end of file diff --git a/app/messages/user/user-registration-input-errors.ts b/app/messages/user/user-registration-input-errors.ts new file mode 100644 index 0000000..ed4358c --- /dev/null +++ b/app/messages/user/user-registration-input-errors.ts @@ -0,0 +1,9 @@ +/** + * 入力エラーのメッセージを保持するインターフェース。 + */ +export default interface UserRegistrationInputErrors { + /** + * ユーザー名に関するエラーメッセージ。 + */ + userName: string[]; +} \ No newline at end of file diff --git a/app/repositories/common/postgres-client-provider.ts b/app/repositories/common/postgres-client-provider.ts index e7f6e0e..d7396b5 100644 --- a/app/repositories/common/postgres-client-provider.ts +++ b/app/repositories/common/postgres-client-provider.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import pg from "pg"; /** @@ -15,19 +16,19 @@ export default class PostgresClientProvider { constructor() { if (process.env.RUN_INFRA_TESTS) { if (!process.env.TEST_DATABASE_URL) { - throw new Error("TEST_DATABASE_URLが設定されていません。"); + throw new Error(systemMessages.error.databaseTestEnvironmentVariableError); } process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; } if (!process.env.DATABASE_URL) { - throw new Error("DATABASE_URLが設定されていません。"); + throw new Error(systemMessages.error.databaseEnvironmentVariableError); } this.pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, ssl: true, }); this.pool.on("error", (error, client) => { - console.error("Postgres pool error", error); + console.error(systemMessages.error.databasePoolError, error); client.release(); }); } @@ -39,7 +40,7 @@ export default class PostgresClientProvider { public async get(): Promise { const client = await this.pool.connect(); client.on("error", (error) => { - console.error("Postgres client error", error); + console.error(systemMessages.error.databaseClientError, error); }); return client; } diff --git a/app/repositories/post/postgres-post-content-repository.ts b/app/repositories/post/postgres-post-content-repository.ts index bde03ed..11e7115 100644 --- a/app/repositories/post/postgres-post-content-repository.ts +++ b/app/repositories/post/postgres-post-content-repository.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import PostgresClientProvider from "../common/postgres-client-provider"; import PostContent from "../../models/post/post-content"; import IPostContentRepository from "./i-post-content-repository"; @@ -36,7 +37,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi const postInsertResult = await client.query(postInsertQuery, postInsertValues); // 結果がない場合、エラーを投げる。 - if (postInsertResult.rowCount === 0) throw new Error("投稿に失敗しました。"); + if (postInsertResult.rowCount === 0) throw new Error(systemMessages.error.postFailed); // 投稿IDを取得する。 const postId = postInsertResult.rows[0].id; @@ -56,7 +57,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi const releaseInformationAssociationResult = await client.query(releaseInformationAssociationInsertQuery, releaseInformationAssociationInsertValues); // 結果がない場合、エラーを投げる。 - if (releaseInformationAssociationResult.rowCount === 0) throw new Error("投稿に失敗しました。"); + if (releaseInformationAssociationResult.rowCount === 0) throw new Error(systemMessages.error.postFailed); // コミットする。 await client.query("COMMIT"); @@ -64,6 +65,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi // 投稿IDを返す。 return postId; } catch (error) { + console.error(error); await client.query("ROLLBACK"); throw error; } finally { @@ -85,6 +87,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi // 投稿結果がない場合、falseを返す。 if (resultDeleteForReleaseInformationAssociation.rowCount === 0) { + console.error(systemMessages.error.postDeletionFailed); await client.query("ROLLBACK"); return false; } @@ -98,6 +101,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi // 投稿結果がない場合、falseを返す。 if (resultForPosts.rowCount === 0) { + console.error(systemMessages.error.postDeletionFailed); await client.query("ROLLBACK"); return false; } @@ -106,6 +110,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi await client.query("COMMIT"); return true; } catch (error) { + console.error(error); await client.query("ROLLBACK"); throw error; } finally { @@ -173,6 +178,7 @@ export default class PostgresPostContentRepository implements IPostContentReposi })); return latestPosts; } catch (error) { + console.error(error); throw error; } finally { client.release(); diff --git a/app/repositories/post/postgres-release-information-repository.ts b/app/repositories/post/postgres-release-information-repository.ts index 9daab27..b6e0420 100644 --- a/app/repositories/post/postgres-release-information-repository.ts +++ b/app/repositories/post/postgres-release-information-repository.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import PostgresClientProvider from "../common/postgres-client-provider"; import ReleaseInformation from "../../models/post/release-information"; import IReleaseInformationRepository from "./i-release-information-repository"; @@ -26,7 +27,7 @@ export default class PostgresReleaseInformationRepository implements IReleaseInf const result = await client.query(query, values); // リリース情報が存在しない場合、エラーを投げる。 - if (result.rows.length === 0) throw new Error(`リリース情報が存在しません。releaseInformationId=${releaseInformationId}`); + if (result.rows.length === 0) throw new Error(`${systemMessages.error.releaseInformationRetrievalFailed}releaseInformationId=${releaseInformationId}`); // リリース情報を生成する。 const releaseInformation = { @@ -37,6 +38,7 @@ export default class PostgresReleaseInformationRepository implements IReleaseInf }; return releaseInformation; } catch (error) { + console.error(error); throw error; } finally { client.release(); @@ -53,7 +55,7 @@ export default class PostgresReleaseInformationRepository implements IReleaseInf const result = await client.query(query); // リリース情報が存在しない場合、エラーを投げる。 - if (result.rows.length === 0) throw new Error(`リリース情報が存在しません。`); + if (result.rows.length === 0) throw new Error(systemMessages.error.releaseInformationNotExists); // リリース情報を生成する。 const allReleaseInformation = result.rows.map((releaseInformation) => { @@ -66,6 +68,7 @@ export default class PostgresReleaseInformationRepository implements IReleaseInf }); return allReleaseInformation; } catch (error) { + console.error(error); throw error; } finally { client.release(); @@ -115,6 +118,7 @@ export default class PostgresReleaseInformationRepository implements IReleaseInf })); return releaseInformationBelowUserSetting; } catch (error) { + console.error(error); throw error; } finally { client.release(); diff --git a/app/repositories/user/postgres-user-repository.ts b/app/repositories/user/postgres-user-repository.ts index 5126e04..3375c0e 100644 --- a/app/repositories/user/postgres-user-repository.ts +++ b/app/repositories/user/postgres-user-repository.ts @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import PostgresClientProvider from "../common/postgres-client-provider"; import User from "../../models/user/user"; import UserSetting from "../../models/user/user-setting"; @@ -40,11 +41,12 @@ export default class PostgresUserRepository implements IUserRepository { // 結果がない場合、falseを返す。 if (userInsertResult.rowCount === 0) { + console.error(systemMessages.error.userRegistrationFailed); await client.query("ROLLBACK"); return false; } - // リリースバージョンフィルターの設定を行う。 + // リリースバージョンフィルターの設定を作成する。 const userId = userInsertResult.rows[0].id; const releaseVersionFilterSettingInsertQuery = ` INSERT INTO release_version_filter_settings ( @@ -61,6 +63,7 @@ export default class PostgresUserRepository implements IUserRepository { // 結果がない場合、falseを返す。 if (releaseVersionFilterSettingInsertResult.rowCount === 0) { + console.error(systemMessages.error.userSettingRegistrationFailed); await client.query("ROLLBACK"); return false; } @@ -69,6 +72,7 @@ export default class PostgresUserRepository implements IUserRepository { await client.query("COMMIT"); return true; } catch (error) { + console.error(error); await client.query("ROLLBACK"); throw error; } finally { @@ -90,7 +94,7 @@ export default class PostgresUserRepository implements IUserRepository { const getUserResult = await client.query(getUserIdQuery, getUserIdValues); // ユーザーが存在しない場合、エラーを投げる。 - if (getUserResult.rowCount === 0) throw new Error("ユーザーが存在しません。"); + if (getUserResult.rowCount === 0) throw new Error(systemMessages.error.userNotExists); // リリースバージョンフィルターの設定が存在するかを確認する。 const userId = getUserResult.rows[0].id; @@ -126,6 +130,7 @@ export default class PostgresUserRepository implements IUserRepository { // 結果がない場合、falseを返す。 if (result.rowCount === 0) { + console.error(systemMessages.error.userSettingEditFailed); await client.query("ROLLBACK"); return false; } @@ -134,6 +139,7 @@ export default class PostgresUserRepository implements IUserRepository { await client.query("COMMIT"); return true; } catch (error) { + console.error(error); await client.query("ROLLBACK"); throw error; } finally { @@ -155,6 +161,7 @@ export default class PostgresUserRepository implements IUserRepository { // 結果がない場合、falseを返す。 if (deleteSettingsResult.rowCount === 0) { + console.error(systemMessages.error.userSettingDeletionFailed); await client.query("ROLLBACK"); return false; } @@ -168,6 +175,7 @@ export default class PostgresUserRepository implements IUserRepository { // 結果がない場合、falseを返す。 if (deleteUserResult.rowCount === 0) { + console.error(systemMessages.error.userDeletionFailed); await client.query("ROLLBACK"); return false; } @@ -176,6 +184,7 @@ export default class PostgresUserRepository implements IUserRepository { await client.query("COMMIT"); return true; } catch (error) { + console.error(error); await client.query("ROLLBACK"); throw error; } finally { @@ -225,6 +234,7 @@ export default class PostgresUserRepository implements IUserRepository { }; return user; } catch (error) { + console.error(error); throw error; } finally { client.release(); @@ -269,6 +279,7 @@ export default class PostgresUserRepository implements IUserRepository { }; return user; } catch (error) { + console.error(error); throw error; } finally { client.release(); @@ -304,6 +315,7 @@ export default class PostgresUserRepository implements IUserRepository { }; return userSetting; } catch (error) { + console.error(error); throw error; } finally { client.release(); diff --git a/app/root.tsx b/app/root.tsx index 698a2b5..97bb176 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -9,6 +9,7 @@ import { Scripts, ScrollRestoration, } from "@remix-run/react"; +import SystemMessageProvider from "./contexts/system-message/system-message-provider"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [ @@ -27,10 +28,12 @@ export default function App() { - - - - + + + + + + ); diff --git a/app/routes/app._index/route.tsx b/app/routes/app._index/route.tsx index 2c88f8e..11f6061 100644 --- a/app/routes/app._index/route.tsx +++ b/app/routes/app._index/route.tsx @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import { LoaderFunctionArgs, json } from "@remix-run/node"; import PostEntry from "./components/post-entry"; import LatestPostTimeLine from "./components/latest-post-time-line"; @@ -6,8 +7,9 @@ import { newlyPostedPostCookie } from "../../cookies.server"; import { useFetcher, useLoaderData } from "@remix-run/react"; import PostContent from "../../models/post/post-content"; import InfiniteScroll from "../components/infinite-scroll"; -import { useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { appLoadContext as context } from "../../dependency-injector/get-load-context"; +import SystemMessageContext from "../../contexts/system-message/system-message-context"; import styles from "./route.module.css"; /** @@ -20,27 +22,33 @@ import styles from "./route.module.css"; export const loader = async ({ request, }: LoaderFunctionArgs) => { - // プロフィールIDを取得する。 - const cookieHeader = request.headers.get("Cookie"); - const session = await getSession(cookieHeader); - const profileId = session.get("userId") as string; + try { + // プロフィールIDを取得する。 + const cookieHeader = request.headers.get("Cookie"); + const session = await getSession(cookieHeader); + const profileId = session.get("userId") as string; - // ユーザーが投稿した直後の場合、ユーザーの投稿が最初に含まれる投稿を返す。 - const cookie = (await newlyPostedPostCookie.parse(cookieHeader)) || {}; - if (cookie.postId) { + // ユーザーが投稿した直後の場合、ユーザーの投稿が最初に含まれる投稿を返す。 + const cookie = (await newlyPostedPostCookie.parse(cookieHeader)) || {}; + if (cookie.postId) { + const latestPostsLoader = context.latestPostsLoader; + const postContents: PostContent[] = await latestPostsLoader.getLatestPosts(profileId); + return json(postContents, { + headers: { + "Set-Cookie": await newlyPostedPostCookie.serialize({}), + }, + }); + } + + // 最新の投稿を取得する。 const latestPostsLoader = context.latestPostsLoader; const postContents: PostContent[] = await latestPostsLoader.getLatestPosts(profileId); - return json(postContents, { - headers: { - "Set-Cookie": await newlyPostedPostCookie.serialize({}), - }, - }); + return json(postContents); + } catch (error) { + console.error(error); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); } - - // 最新の投稿を取得する。 - const latestPostsLoader = context.latestPostsLoader; - const postContents: PostContent[] = await latestPostsLoader.getLatestPosts(profileId); - return json(postContents); } /** @@ -48,8 +56,19 @@ export const loader = async ({ * @returns トップページのインデックス。 */ export default function TopIndex() { + // システムメッセージを取得する。 + const loaderData = useLoaderData(); + const loaderErrorMessage = "errorMessage" in loaderData ? loaderData.errorMessage : ""; + + // システムメッセージを表示する。 + const { showSystemMessage } = useContext(SystemMessageContext); + useEffect(() => { + showSystemMessage("error", loaderErrorMessage); + }, [loaderData]); + + // 最新の投稿を取得する。 const fetcher = useFetcher(); - const initialLatestPostContents: PostContent[] = useLoaderData().map((postContent) => ({ + const initialLatestPostContents: PostContent[] = "errorMessage" in loaderData ? [] : loaderData.map((postContent) => ({ ...postContent, createdAt: new Date(postContent.createdAt), })); diff --git a/app/routes/app.latest-posts.$id/route.tsx b/app/routes/app.latest-posts.$id/route.tsx index 138b321..8281357 100644 --- a/app/routes/app.latest-posts.$id/route.tsx +++ b/app/routes/app.latest-posts.$id/route.tsx @@ -1,3 +1,4 @@ +import systemMessages from "../../messages/system-messages"; import { LoaderFunctionArgs, json } from "@remix-run/node"; import PostContent from "../../models/post/post-content"; import { appLoadContext as context } from "../../dependency-injector/get-load-context"; @@ -15,10 +16,16 @@ export const loader = async ({ request, params, }: LoaderFunctionArgs) => { - if (params.id === undefined || !params.id) return json([]); - const cookieHeader = request.headers.get("Cookie"); - const session = await getSession(cookieHeader); - const profileId = session.get("userId") as string; - const postContents: PostContent[] = await context.latestPostsLoader.getLatestPosts(profileId); - return json(postContents); + try { + if (params.id === undefined || !params.id) return json([]); + const cookieHeader = request.headers.get("Cookie"); + const session = await getSession(cookieHeader); + const profileId = session.get("userId") as string; + const postContents: PostContent[] = await context.latestPostsLoader.getLatestPosts(profileId); + return json(postContents); + } catch (error) { + console.error(error); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); + } } \ No newline at end of file diff --git a/app/routes/app.post-message/route.tsx b/app/routes/app.post-message/route.tsx index f64efc0..6e922d8 100644 --- a/app/routes/app.post-message/route.tsx +++ b/app/routes/app.post-message/route.tsx @@ -1,8 +1,11 @@ +import systemMessages from "../../messages/system-messages"; import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, json, redirect } from "@remix-run/node"; -import { Form, useLoaderData } from "@remix-run/react"; +import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { getSession } from "../../sessions"; import { newlyPostedPostCookie } from "../../cookies.server"; import { appLoadContext as context } from "../../dependency-injector/get-load-context"; +import { useContext, useEffect } from "react"; +import SystemMessageContext from "../../contexts/system-message/system-message-context"; /** * メッセージ投稿ページのメタ情報を設定する。 @@ -23,12 +26,18 @@ export const meta: MetaFunction = () => { export const loader = async ({ request, }: LoaderFunctionArgs) => { - const cookieHeader = request.headers.get("Cookie"); - const session = await getSession(cookieHeader); - const profileId = session.get("userId") as string; - const releaseInformationLoader = context.releaseInformationLoader; - const allReleaseInformation = await releaseInformationLoader.getReleaseInformationBelowUserSetting(profileId); - return json(allReleaseInformation); + try { + const cookieHeader = request.headers.get("Cookie"); + const session = await getSession(cookieHeader); + const profileId = session.get("userId") as string; + const releaseInformationLoader = context.releaseInformationLoader; + const allReleaseInformation = await releaseInformationLoader.getReleaseInformationBelowUserSetting(profileId); + return json(allReleaseInformation); + } catch (error) { + console.error(error); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); + } } /** @@ -48,9 +57,6 @@ export const action = async ({ const authenticatedUserLoader = context.authenticatedUserLoader; const authenticatedUser = await authenticatedUserLoader.getUserByProfileId(profileId); - // 認証済みユーザーが取得できない場合、エラーを返す。 - if (!authenticatedUser) return json({ error: "メッセージの投稿に失敗しました。" }); - // ユーザーIDを取得する。 const userId = authenticatedUser.id; @@ -74,7 +80,8 @@ export const action = async ({ }); } catch (error) { console.error(error); - return json({ error: "メッセージの投稿に失敗しました。" }); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); } } @@ -83,7 +90,23 @@ export const action = async ({ * @returns メッセージ投稿ページ。 */ export default function PostMessage() { - const allReleaseInformation = useLoaderData(); + // システムメッセージを取得する。 + const loaderData = useLoaderData(); + const loaderErrorMessage = "errorMessage" in loaderData ? loaderData.errorMessage : ""; + const actionData = useActionData(); + const actionErrorMessage = actionData ? actionData.errorMessage : ""; + + // システムメッセージを表示する。 + const { showSystemMessage } = useContext(SystemMessageContext); + useEffect(() => { + showSystemMessage("error", loaderErrorMessage); + }, [loaderData]); + useEffect(() => { + showSystemMessage("error", actionErrorMessage); + }, [actionData]); + + // リリース情報を取得する。 + const allReleaseInformation = "errorMessage" in loaderData ? [] : loaderData; const getReleaseVersionOptions = () => { return {allReleaseInformation.map((releaseInformation) => { diff --git a/app/routes/auth.signup/route.tsx b/app/routes/auth.signup/route.tsx index d01d9f9..67ffdea 100644 --- a/app/routes/auth.signup/route.tsx +++ b/app/routes/auth.signup/route.tsx @@ -1,7 +1,10 @@ +import systemMessages from "../../messages/system-messages"; import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, json, redirect } from "@remix-run/node"; -import { Form } from "@remix-run/react"; +import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { commitSession, getSession } from "../../sessions"; import { appLoadContext as context } from "../../dependency-injector/get-load-context"; +import { useContext, useEffect } from "react"; +import SystemMessageContext from "../../contexts/system-message/system-message-context"; /** * サインアップページのメタ情報を設定する。 @@ -23,17 +26,23 @@ export const meta: MetaFunction = () => { export const loader = async ({ request, }: LoaderFunctionArgs) => { - // ログインしている場合、トップページにリダイレクトする。 - const cookieHeader = request.headers.get("Cookie"); - const session = await getSession(cookieHeader); - if (session.has("userId")) { - return redirect("/app", { - headers: { - "Set-Cookie": await commitSession(session), - }, - }); + try { + // ログインしている場合、トップページにリダイレクトする。 + const cookieHeader = request.headers.get("Cookie"); + const session = await getSession(cookieHeader); + if (session.has("userId")) { + return redirect("/app", { + headers: { + "Set-Cookie": await commitSession(session), + }, + }); + } + return null; + } catch (error) { + console.error(error); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); } - return null; } /** @@ -52,12 +61,9 @@ export const action = async ({ const password = formData.get("password") as string; const confirmPassword = formData.get("confirmPassword") as string; - // パスワードとパスワード再確認が一致しない場合、エラーを返す。 - if (password !== confirmPassword) return json({ error: "パスワードが一致しません。" }); - // ユーザーを登録する。 const userRegistrationAction = context.userRegistrationAction; - const response = await userRegistrationAction.register(mailAddress, password); + const response = await userRegistrationAction.register(mailAddress, password, confirmPassword); // IDトークンとリフレッシュトークンをセッションに保存する。 const idToken = response.idToken; @@ -75,7 +81,8 @@ export const action = async ({ }); } catch (error) { console.error(error); - return json({ error: "サインアップに失敗しました。" }); + if (error instanceof TypeError || error instanceof Error) return json({ errorMessage: error.message }); + return json({ errorMessage: systemMessages.error.unknownError }); } } @@ -84,6 +91,21 @@ export const action = async ({ * @returns サインアップページ。 */ export default function Signup() { + // システムメッセージを取得する。 + const loaderData = useLoaderData(); + const loaderErrorMessage = loaderData && "errorMessage" in loaderData ? loaderData.errorMessage : ""; + const actionData = useActionData(); + const actionErrorMessage = actionData ? actionData.errorMessage : ""; + + // システムメッセージを表示する。 + const { showSystemMessage } = useContext(SystemMessageContext); + useEffect(() => { + showSystemMessage("error", actionErrorMessage); + }, [loaderData]); + useEffect(() => { + showSystemMessage("error", actionErrorMessage); + }, [actionData]); + return (