diff --git a/.env.example b/.env.example
index c80b40a..8b6154c 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,6 @@
# frontend
VITE_API_URL=
+VITE_REDIRECT_URL=
# backend
POSTGRES_USER=
@@ -11,3 +12,5 @@ BETTER_AUTH_URL=
BETTER_AUTH_SECRET=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
diff --git a/.github/workflows/deploy-frontend.yaml b/.github/workflows/deploy-frontend.yaml
index a1e02c8..293066d 100644
--- a/.github/workflows/deploy-frontend.yaml
+++ b/.github/workflows/deploy-frontend.yaml
@@ -24,6 +24,7 @@ jobs:
- name: Build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
+ VITE_CLIENT_URL: ${{ secrets.VITE_CLIENT_URL }}
VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }}
run: nix develop --command bash -c "pnpm i && pnpm run build"
# Deploy Frontend to Cloudflare Workers
diff --git a/.github/workflows/preview-frontend.yaml b/.github/workflows/preview-frontend.yaml
index 13b8e5e..84c2c80 100644
--- a/.github/workflows/preview-frontend.yaml
+++ b/.github/workflows/preview-frontend.yaml
@@ -26,6 +26,7 @@ jobs:
id: build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
+ VITE_CLIENT_URL: ${{ secrets.VITE_CLIENT_URL }}
VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }}
run: nix develop --command bash -c "pnpm i && pnpm run frontend:build"
# Deploy Preview to Cloudflare Workers
diff --git a/assets/er.mmd b/assets/er.mmd
index 9f9a9aa..882ff85 100644
--- a/assets/er.mmd
+++ b/assets/er.mmd
@@ -7,15 +7,10 @@ erDiagram
text image
created_at timestamp
updated_at timestamp
- }
- UserProfile {
- text user_id "PK FK"
text display_name
text avatar_url
text bio
- created_at timestamp
- updated_at timestamp
}
Session {
@@ -84,8 +79,6 @@ erDiagram
User ||--|{ Session : "id<->user_id"
User ||--|{ Account : "id<->user_id"
- User ||--o| UserProfile : "id<->user_id"
-
User ||--o{ Group : "id<->created_by"
Group ||--o{ GroupMembership : "id<->group_id"
diff --git a/assets/er.png b/assets/er.png
index b07ad4b..eea88dd 100644
Binary files a/assets/er.png and b/assets/er.png differ
diff --git a/flake.lock b/flake.lock
index 8af9108..58e912d 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1764642553,
- "narHash": "sha256-mvbFFzVBhVK1FjyPHZGMAKpNiqkr7k++xIwy+p/NQvA=",
+ "lastModified": 1767966113,
+ "narHash": "sha256-mSTsvXa4WveSRJexsmCbm9dY17B1fKp7NLpJxllpQw4=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "f720de59066162ee879adcc8c79e15c51fe6bfb4",
+ "rev": "5f02c91314c8ba4afe83b256b023756412218535",
"type": "github"
},
"original": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7d34c85..262766e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -145,8 +145,8 @@ catalogs:
specifier: ^17.2.3
version: 17.2.3
dotenv-caster:
- specifier: ^2.1.23
- version: 2.1.23
+ specifier: ^2.2.1
+ version: 2.2.1
importers:
@@ -205,7 +205,7 @@ importers:
version: 17.2.3
dotenv-caster:
specifier: catalog:setup
- version: 2.1.23(dotenv@17.2.3)
+ version: 2.2.1(dotenv@17.2.3)
drizzle-kit:
specifier: catalog:backend
version: 0.31.6
@@ -481,7 +481,7 @@ importers:
version: 17.2.3
dotenv-caster:
specifier: catalog:setup
- version: 2.1.23(dotenv@17.2.3)
+ version: 2.2.1(dotenv@17.2.3)
tsx:
specifier: catalog:node
version: 4.20.6
@@ -2346,8 +2346,8 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
- dotenv-caster@2.1.23:
- resolution: {integrity: sha512-L276rkMu8t9BEqciqhE6JxtShJ2zBLhZ3drcK3+0WGNkJobpTiRJ9eFRmlMaz0Ewnl4m5BqD6ZaGFL6GQMtFaA==}
+ dotenv-caster@2.2.1:
+ resolution: {integrity: sha512-B4ESLIOIB7hL1KiGKMw7Y4R5NP/faRliC47fsR4/axZwWmaDeHdYamA6dRFJU5k2AzkyEWUjzIFHlGfctoRVNg==}
peerDependencies:
dotenv: ^17.2.3
@@ -5916,7 +5916,7 @@ snapshots:
dlv@1.1.3: {}
- dotenv-caster@2.1.23(dotenv@17.2.3):
+ dotenv-caster@2.2.1(dotenv@17.2.3):
dependencies:
dotenv: 17.2.3
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index edce2b7..ebe4608 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -56,7 +56,7 @@ catalogs:
turbo: ^2.5.8
setup:
dotenv: ^17.2.3
- dotenv-caster: ^2.1.23
+ dotenv-caster: ^2.2.1
ignoredBuiltDependencies:
- '@swc/core'
diff --git a/products/backend/.env.example b/products/backend/.env.example
index 037faab..8e61426 100644
--- a/products/backend/.env.example
+++ b/products/backend/.env.example
@@ -1,5 +1,8 @@
DATABASE_URL=
+DATABASE_CREDENTIALS_SSL_REJECT_UNAUTHORIZED=
BETTER_AUTH_URL=
BETTER_AUTH_SECRET=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
diff --git a/products/backend/README.md b/products/backend/README.md
index f6ab4d9..495ad95 100644
--- a/products/backend/README.md
+++ b/products/backend/README.md
@@ -36,6 +36,10 @@ better-authが実行時に参照する設定ファイル。
pnpm run backend:better-auth:generate
```
+`auto-auth-schema.ts`が更新される。
+
+このファイルを読み、適宜`src/db/auth-schema.ts`を手動で更新する。
+
3. `pnpm run backend:drizzle:generate`を実行する。
```sh
@@ -100,6 +104,21 @@ pnpm run backend:deploy
※ HyperdriveのIDが漏洩すると、他者にデータベースを操作される可能性があるため、デプロイ完了後は必ず元に戻してからステージング (`git add`) をすること。
+## drizzleのスキーマファイルの構造
+
+```mermaid
+flowchart LR
+ auth-schema["src/db/auth-schema.ts (better-auth関係のテーブル定義用)"]
+ pay-crew2["src/db/pay-crew2-schema.ts (pay-crew2関係のテーブル定義用)"]
+ relation["src/db/relation.ts (リレーション定義用)"]
+ schema["src/db/schema.ts (エクスポート用)"]
+ auth-schema --> relation
+ auth-schema --> schema
+ relation --> schema
+ pay-crew2 --> relation
+ pay-crew2 --> schema
+```
+
## 構造
この構造は、レイヤードアーキテクチャから着想を得て定義している。
@@ -116,6 +135,7 @@ pnpm run backend:deploy
- hono
- @hono/zod-openapi
- zod-openapi-share
+ - better-auth
#### presentation/share
@@ -128,6 +148,7 @@ presentation層で共通して使用するエンティティを定義する。
- 外部依存
- drizzle
+ - better-auth
### db
@@ -146,6 +167,7 @@ openapi.jsonの生成を行う処理を定義している。
- hono
- @hono/zod-openapi
- zod-openapi-share
+ - better-auth
## 依存関係
diff --git a/products/backend/drizzle.config.ts b/products/backend/drizzle.config.ts
index a2ea9b5..fb5f4f0 100644
--- a/products/backend/drizzle.config.ts
+++ b/products/backend/drizzle.config.ts
@@ -16,7 +16,7 @@ const databaseCredentialsSslRejectUnauthorized = caster.castBoolean(
export default {
dialect: 'postgresql',
- schema: './src/db/schema.ts',
+ schema: ['./src/db/auth-schema.ts', './src/db/pay-crew2-schema.ts', './src/db/relation.ts'],
out: './drizzle',
dbCredentials: {
url: databaseUrl,
diff --git a/products/backend/src/db/auth-schema.ts b/products/backend/src/db/auth-schema.ts
index 9949897..5f9fb35 100644
--- a/products/backend/src/db/auth-schema.ts
+++ b/products/backend/src/db/auth-schema.ts
@@ -1,36 +1,26 @@
import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';
-export const user = pgTable('user', {
- id: text('id').primaryKey(),
- name: text('name').notNull(),
- email: text('email').notNull().unique(),
- emailVerified: boolean('email_verified').default(false).notNull(),
- image: text('image'),
- createdAt: timestamp('created_at').defaultNow().notNull(),
- updatedAt: timestamp('updated_at')
- .defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
- .notNull(),
-});
-
-export const userProfile = pgTable(
- 'user_profile',
+export const user = pgTable(
+ 'user',
{
- userId: text('user_id')
- .primaryKey()
- .references(() => user.id, { onDelete: 'cascade' }),
- displayName: text('display_name'),
- avatarUrl: text('avatar_url'),
- bio: text('bio'),
+ id: text('id').primaryKey(),
+ name: text('name').notNull(),
+ email: text('email').notNull().unique(),
+ emailVerified: boolean('email_verified').default(false).notNull(),
+ image: text('image'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
+ // 追加のプロフィール情報
+ displayName: text('display_name'),
+ avatarUrl: text('avatar_url'),
+ bio: text('bio'),
},
(table) => [
- // displayName に対する二次索引を作成
- index('user_profile_display_name_idx').on(table.displayName),
+ // display_name に対する二次索引を作成
+ index('user_display_name_idx').on(table.displayName),
]
);
diff --git a/products/backend/src/db/relation.ts b/products/backend/src/db/relation.ts
index fee3630..1fb7a54 100644
--- a/products/backend/src/db/relation.ts
+++ b/products/backend/src/db/relation.ts
@@ -1,17 +1,11 @@
// drizzle
import { relations } from 'drizzle-orm';
-import { account, session, user, userProfile } from './auth-schema';
-import { debt, group, groupMembership } from './pay-crew2-schema';
// tables
-export { user, userProfile, session, account, verification } from './auth-schema';
-export { group, groupMembership, debt } from './pay-crew2-schema';
+import { account, session, user } from './auth-schema';
+import { debt, group, groupMembership } from './pay-crew2-schema';
// auth-schema
-export const userRelations = relations(user, ({ one, many }) => ({
- userProfile: one(userProfile, {
- fields: [user.id],
- references: [userProfile.userId],
- }),
+export const userRelations = relations(user, ({ many }) => ({
sessions: many(session, {
relationName: 'user__session_userId',
}),
@@ -35,13 +29,6 @@ export const userRelations = relations(user, ({ one, many }) => ({
}),
}));
-export const userProfileRelations = relations(userProfile, ({ one }) => ({
- user: one(user, {
- fields: [userProfile.userId],
- references: [user.id],
- }),
-}));
-
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
diff --git a/products/backend/src/db/schema.ts b/products/backend/src/db/schema.ts
index 895b6a6..0a3fc8f 100644
--- a/products/backend/src/db/schema.ts
+++ b/products/backend/src/db/schema.ts
@@ -1 +1,3 @@
+export * from './auth-schema';
+export * from './pay-crew2-schema';
export * from './relation';
diff --git a/products/backend/src/index.ts b/products/backend/src/index.ts
index 4fc0e9d..48635be 100644
--- a/products/backend/src/index.ts
+++ b/products/backend/src/index.ts
@@ -20,7 +20,7 @@ app.use(
cors({
origin: ['https://pay-crew2.yukiosada.work', 'http://localhost:5173'],
allowHeaders: ['Content-Type', 'Authorization', 'baggage', 'sentry-trace'],
- allowMethods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS'],
+ allowMethods: ['POST', 'PUT', 'PATCH', 'GET', 'DELETE', 'OPTIONS'],
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true,
diff --git a/products/backend/src/presentation/factory/hono.ts b/products/backend/src/presentation/factory/hono.ts
index 399a708..3d02f8d 100644
--- a/products/backend/src/presentation/factory/hono.ts
+++ b/products/backend/src/presentation/factory/hono.ts
@@ -14,7 +14,7 @@ const honoFactory = () => {
if (!result.success) {
console.error(result.error);
throw new HTTPException(400, {
- message: 'Zod Validation Error',
+ message: 'Bad Request',
});
}
},
diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts
index 4d5ac4a..99f4209 100644
--- a/products/backend/src/presentation/routes/group.ts
+++ b/products/backend/src/presentation/routes/group.ts
@@ -21,8 +21,6 @@ import {
registerGroupDebtResponseSchema,
type RegisterGroupDebtResponseSchemaType,
deleteGroupDebtRequestSchema,
- deleteGroupDebtResponseSchema,
- type DeleteGroupDebtResponseSchemaType,
} from 'validator';
// error schema
import { route } from '../share/error';
@@ -30,7 +28,7 @@ import { route } from '../share/error';
import { drizzle } from 'drizzle-orm/node-postgres';
import { and, eq, isNull } from 'drizzle-orm';
import { group, debt, groupMembership } from '../../db/pay-crew2-schema';
-import { user, userProfile } from '../../db/auth-schema';
+import { user } from '../../db/auth-schema';
const hono = honoFactory();
@@ -39,7 +37,7 @@ const createGroupSchema = route.createSchema(
{
path: '/api/group/create',
method: 'post',
- description: 'create a new group',
+ description: 'グループを新規作成するエンドポイント',
security: [{ SessionCookie: [] }],
request: {
body: {
@@ -62,55 +60,44 @@ const createGroupSchema = route.createSchema(
},
},
},
- [400, 401, 500] as const
+ [401, 500] as const
);
hono.openapi(createGroupSchema, async (c) => {
- c.status(201);
+ const loginUser = c.get('user');
const body = c.req.valid('json');
- const user = c.get('user');
- //validation
- if (!body || !body.name) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- // set group name
- const groupName = body.name;
-
- // create invited_id
- let uuid1 = crypto.randomUUID();
- let uuid2 = crypto.randomUUID();
- // invite_id format: -
- const invite_id = `${uuid1}-${uuid2}`;
+ // 現在時刻の取得
+ const now = new Date();
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- //* insert group data *//
+ //* グループ情報を挿入 *//
const result = await db
.insert(group)
.values({
id: crypto.randomUUID(),
- name: groupName,
- invite_id: invite_id,
- createdBy: user.id,
- createdAt: new Date(),
- updatedAt: new Date(),
+ name: body.group_name,
+ invite_id: `${crypto.randomUUID()}-${crypto.randomUUID()}`,
+ createdBy: loginUser.id,
+ createdAt: now,
+ updatedAt: now,
})
- .returning();
+ .returning({
+ id: group.id,
+ invite_id: group.invite_id,
+ });
- //* insert created_by user to groupMembership *//
- await db
- .insert(groupMembership)
- .values({
- id: crypto.randomUUID(),
- groupId: result[0].id,
- userId: user.id,
- joinedAt: new Date(),
- })
- .returning();
+ //* グループ作成者をgroupMembershipに挿入 *//
+ await db.insert(groupMembership).values({
+ id: crypto.randomUUID(),
+ groupId: result[0].id,
+ userId: loginUser.id,
+ joinedAt: now,
+ });
- // return response
+ // レスポンス
return c.json(
{
group_id: result[0].id,
@@ -125,7 +112,7 @@ const joinGroupSchema = route.createSchema(
{
path: '/api/group/join',
method: 'post',
- description: 'join an existing group',
+ description: '招待IDを使ってグループに参加するエンドポイント',
security: [{ SessionCookie: [] }],
request: {
body: {
@@ -152,39 +139,62 @@ const joinGroupSchema = route.createSchema(
);
hono.openapi(joinGroupSchema, async (c) => {
- c.status(201);
const body = c.req.valid('json');
const user = c.get('user');
- //validation
- if (!body || !body.invite_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- const inviteId = body.invite_id;
+ // 現在時刻の取得
+ const now = new Date();
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // find group by invite_id
- const groupData = await db.select().from(group).where(eq(group.invite_id, inviteId)).limit(1);
+ // invite_id から group を取得
+ const groupData = await db
+ .select({
+ id: group.id,
+ })
+ .from(group)
+ .where(eq(group.invite_id, body.invite_id))
+ .limit(1);
+
+ // invite_id が不正な場合はエラー
if (groupData.length === 0) {
- // validate invite_id
- throw new HTTPException(401, { message: 'Invalid invite_id' });
+ throw new HTTPException(400, { message: 'Bad Request' });
}
- // add user to the group
- const groupId = groupData[0].id;
+ // すでに参加している場合は即座に返スす
+ const existingMembership = await db
+ .select({
+ groupMembershipId: groupMembership.id,
+ })
+ .from(groupMembership)
+ .where(and(eq(groupMembership.groupId, groupData[0].id), eq(groupMembership.userId, user.id)))
+ .limit(1);
+
+ if (existingMembership.length > 0) {
+ // レスポンス
+ return c.json(
+ {
+ group_id: groupData[0].id,
+ } satisfies JoinGroupResponseSchemaType,
+ 201
+ );
+ }
+
+ // loginUser を invite_id の group に参加させる
const result = await db
.insert(groupMembership)
.values({
id: crypto.randomUUID(),
- groupId: groupId,
+ groupId: groupData[0].id,
userId: user.id,
- joinedAt: new Date(),
+ joinedAt: now,
})
- .returning();
+ .returning({
+ groupId: groupMembership.groupId,
+ });
- // return response
+ // レスポンス
return c.json(
{
group_id: result[0].groupId,
@@ -211,8 +221,8 @@ const getGroupInfoSchema = route.createSchema(
},
},
responses: {
- 200: {
- description: 'OK',
+ 201: {
+ description: 'Created',
content: {
'application/json': {
schema: getGroupInfoResponseSchema,
@@ -225,106 +235,98 @@ const getGroupInfoSchema = route.createSchema(
);
hono.openapi(getGroupInfoSchema, async (c) => {
- c.status(201);
const body = c.req.valid('json');
const loginUser = c.get('user');
- // NOTE: このvalidationは、groupのhistoryの取得のエンドポイントでも全く同じものを使うので、共通化を検討すること
- // NOTE: --- 共通化開始 ---
- // validation
- if (!body || !body.group_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- const groupId = body.group_id;
-
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // find me from the group (validation)
+ // NOTE: --- 共通化開始 ---
+ // loginUser が body.group_id のグループのメンバーであることを確認
const me = await db
- .select()
+ .select({
+ groupMembershipId: groupMembership.id,
+ })
.from(groupMembership)
- .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id)))
+ .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id)))
.limit(1);
+
+ // body.group_id のグループのメンバーでない場合はエラー
if (me.length === 0) {
- // validate membership
- throw new HTTPException(401, { message: 'User is not a member of the group' });
+ throw new HTTPException(400, { message: 'Bad Request' });
}
// NOTE: --- 共通化終了 ---
- //* get group info *//
- const groupData = await db.select().from(group).where(eq(group.id, groupId)).limit(1);
- if (groupData.length === 0) {
- // validate group_id
- throw new HTTPException(401, { message: 'Invalid group_id' });
- }
+ //* body.group_id のグループ情報を取得 *//
+ const groupData = await db
+ .select({
+ name: group.name,
+ createdBy: group.createdBy,
+ })
+ .from(group)
+ .where(eq(group.id, body.group_id))
+ .limit(1);
- //* get created_by member's name *//
- const createdByUserId = groupData[0].createdBy;
- let createdByUserName = '';
- // get user name from user table
- const createdByUserInfo = await db.select().from(user).where(eq(user.id, createdByUserId)).limit(1);
- if (createdByUserInfo.length === 0) {
- throw new HTTPException(500, { message: 'The creator of the group was not found' });
- }
- createdByUserName = createdByUserInfo[0].name;
- // get display name from user_profile table
- const createdByDisplayNameInfo = await db
- .select()
- .from(userProfile)
- .where(eq(userProfile.userId, createdByUserId))
+ //* body.group_id のグループ作成者情報を取得 *//
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const createdByUserNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, groupData[0].createdBy))
.limit(1);
- if (
- createdByDisplayNameInfo.length > 0 &&
- typeof createdByDisplayNameInfo[0].displayName === 'string' &&
- createdByDisplayNameInfo[0].displayName.length > 0
- ) {
- createdByUserName = createdByDisplayNameInfo[0].displayName;
- }
- //* get members *//
- // get all member userIds from groupMembership table
- const membersData = await db.select().from(groupMembership).where(eq(groupMembership.groupId, groupId));
- const memberUserIds = membersData.map((member) => member.userId);
- // get user name or display name for each member
+ // NOTE: 共通化できそう (info.ts と group.ts で共通化)
+ // NOTE: --- 共通化開始 ---
+ //* body.group_id のメンバー情報を取得 *//
+ // グループメンバーのユーザーIDを取得 (group_membership table)
+ const memberUserIds = await db
+ .select({
+ userId: groupMembership.userId,
+ })
+ .from(groupMembership)
+ .where(eq(groupMembership.groupId, body.group_id));
+
+ // メンバー情報を格納する配列
const members: GetGroupInfoResponseMemberElementSchemaType[] = [];
+
for (const memberUserId of memberUserIds) {
- // initialize user name
- let userName = '';
-
- // get user name from user table
- const userNameInfo = await db.select().from(user).where(eq(user.id, memberUserId)).limit(1);
- if (userNameInfo.length === 0) {
- throw new HTTPException(500, { message: 'A group member was not found' });
- }
- //set user name
- userName = userNameInfo[0].name;
-
- // get display name from user_profile table
- const displayNameInfo = await db.select().from(userProfile).where(eq(userProfile.userId, memberUserId)).limit(1);
- if (
- displayNameInfo.length > 0 &&
- typeof displayNameInfo[0].displayName === 'string' &&
- displayNameInfo[0].displayName.length > 0
- ) {
- userName = displayNameInfo[0].displayName;
- }
-
- // push to members array
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const memberUserNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, memberUserId.userId))
+ .limit(1);
+
+ // メンバー情報を配列に追加
members.push({
- user_id: memberUserId,
- user_name: userName,
+ user_id: memberUserId.userId,
+ user_name:
+ memberUserNameInfo[0].displayName !== null && memberUserNameInfo[0].displayName.length > 0
+ ? memberUserNameInfo[0].displayName
+ : memberUserNameInfo[0].name,
});
}
+ // NOTE: --- 共通化終了 ---
// return response
return c.json(
{
group_name: groupData[0].name,
- created_by: createdByUserName,
+ created_by:
+ createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0
+ ? createdByUserNameInfo[0].displayName
+ : createdByUserNameInfo[0].name,
members: members,
} satisfies GetGroupInfoResponseSchemaType,
- 200
+ 201
);
});
@@ -346,8 +348,8 @@ const getGroupDebtHistorySchema = route.createSchema(
},
},
responses: {
- 200: {
- description: 'OK',
+ 201: {
+ description: 'Created',
content: {
'application/json': {
schema: getGroupDebtHistoryResponseSchema,
@@ -360,97 +362,91 @@ const getGroupDebtHistorySchema = route.createSchema(
);
hono.openapi(getGroupDebtHistorySchema, async (c) => {
- c.status(201);
- const body = c.req.valid('json');
const loginUser = c.get('user');
+ const body = c.req.valid('json');
- // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること
- // NOTE: --- 共通化開始 ---
- // validation
- if (!body || !body.group_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- const groupId = body.group_id;
-
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // find me from the group (validation)
+ // NOTE: --- 共通化開始 ---
+ // loginUser が body.group_id のグループのメンバーであることを確認
const me = await db
- .select()
+ .select({
+ groupMembershipId: groupMembership.id,
+ })
.from(groupMembership)
- .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id)))
+ .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id)))
.limit(1);
+
+ // body.group_id のグループのメンバーでない場合はエラー
if (me.length === 0) {
- // validate membership
- throw new HTTPException(401, { message: 'User is not a member of the group' });
+ throw new HTTPException(400, { message: 'Bad Request' });
}
// NOTE: --- 共通化終了 ---
- //* get each group debt info *//
+ //* グループの貸し借り履歴を取得 *//
+ // 貸し借り履歴を格納する配列
let debtData: GetGroupDebtHistoryResponseElementSchemaType[] = [];
+
+ // body.group_id の貸し借り履歴を取得 (debt table)
const rawDebtData = await db
- .select()
+ .select({
+ id: debt.id,
+ debtorId: debt.debtorId,
+ creditorId: debt.creditorId,
+ amount: debt.amount,
+ })
.from(debt)
- .where(and(eq(debt.groupId, groupId), isNull(debt.deletedAt)));
+ .where(and(eq(debt.groupId, body.group_id), isNull(debt.deletedAt)));
+
for (const debtEntry of rawDebtData) {
- // get debtor name from user table
- const debtorInfo = await db.select().from(user).where(eq(user.id, debtEntry.debtorId)).limit(1);
- if (debtorInfo.length === 0) {
- throw new HTTPException(500, { message: 'A debtor user was not found' });
- }
- let debtorName = debtorInfo[0].name;
- // get display name from user_profile table
- const debtorDisplayNameInfo = await db
- .select()
- .from(userProfile)
- .where(eq(userProfile.userId, debtEntry.debtorId))
+ // debt_name を取得
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const DebtorNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, debtEntry.debtorId))
.limit(1);
- if (
- debtorDisplayNameInfo.length > 0 &&
- typeof debtorDisplayNameInfo[0].displayName === 'string' &&
- debtorDisplayNameInfo[0].displayName.length > 0
- ) {
- debtorName = debtorDisplayNameInfo[0].displayName;
- }
-
- // get creditor name from user table
- const creditorInfo = await db.select().from(user).where(eq(user.id, debtEntry.creditorId)).limit(1);
- if (creditorInfo.length === 0) {
- throw new HTTPException(500, { message: 'A creditor user was not found' });
- }
- let creditorName = creditorInfo[0].name;
- // get display name from user_profile table
- const creditorDisplayNameInfo = await db
- .select()
- .from(userProfile)
- .where(eq(userProfile.userId, debtEntry.creditorId))
+
+ // creditor_name を取得
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const CreditorNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, debtEntry.creditorId))
.limit(1);
- if (
- creditorDisplayNameInfo.length > 0 &&
- typeof creditorDisplayNameInfo[0].displayName === 'string' &&
- creditorDisplayNameInfo[0].displayName.length > 0
- ) {
- creditorName = creditorDisplayNameInfo[0].displayName;
- }
-
- // push to debtData array
+
+ // 貸し借り履歴データを配列に追加
debtData.push({
debt_id: debtEntry.id,
debtor_id: debtEntry.debtorId,
- debtor_name: debtorName,
+ debtor_name:
+ DebtorNameInfo[0].displayName !== null && DebtorNameInfo[0].displayName.length > 0
+ ? DebtorNameInfo[0].displayName
+ : DebtorNameInfo[0].name,
creditor_id: debtEntry.creditorId,
- creditor_name: creditorName,
+ creditor_name:
+ CreditorNameInfo[0].displayName !== null && CreditorNameInfo[0].displayName.length > 0
+ ? CreditorNameInfo[0].displayName
+ : CreditorNameInfo[0].name,
amount: debtEntry.amount,
});
}
- // return response
+ // レスポンス
return c.json(
{
debts: debtData,
} satisfies GetGroupDebtHistoryResponseSchemaType,
- 200
+ 201
);
});
@@ -486,49 +482,51 @@ const registerGroupDebtSchema = route.createSchema(
);
hono.openapi(registerGroupDebtSchema, async (c) => {
- c.status(201);
- const body = c.req.valid('json');
const loginUser = c.get('user');
+ const body = c.req.valid('json');
- // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること
- // NOTE: --- 共通化開始 ---
- // validation
- if (!body || !body.group_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- const groupId = body.group_id;
+ // 現在時刻の取得
+ const now = new Date();
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // find me from the group (validation)
+ // NOTE: --- 共通化開始 ---
+ // loginUser が body.group_id のグループのメンバーであることを確認
const me = await db
- .select()
+ .select({
+ groupMembershipId: groupMembership.id,
+ })
.from(groupMembership)
- .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id)))
+ .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id)))
.limit(1);
+
+ // body.group_id のグループのメンバーでない場合はエラー
if (me.length === 0) {
- // validate membership
- throw new HTTPException(401, { message: 'User is not a member of the group' });
+ throw new HTTPException(400, { message: 'Bad Request' });
}
// NOTE: --- 共通化終了 ---
- // register group debt entry
+ // body.group_id に貸し借り履歴を追加 (debt table)
const result = await db
.insert(debt)
.values({
id: crypto.randomUUID(),
- groupId: groupId,
+ groupId: body.group_id,
creditorId: body.creditor_id,
debtorId: body.debtor_id,
amount: body.amount,
- description: body.description ? body.description : null,
- occurredAt: body.occurred_at ? new Date(body.occurred_at) : new Date(),
- createdAt: new Date(),
- updatedAt: new Date(),
- deletedBy: null,
+ description: typeof body.description === 'undefined' ? null : body.description,
+ occurredAt: body.occurred_at ? new Date(body.occurred_at) : now,
+ createdAt: now,
+ updatedAt: now,
})
- .returning();
+ .returning({
+ creditorId: debt.creditorId,
+ debtorId: debt.debtorId,
+ amount: debt.amount,
+ occurredAt: debt.occurredAt,
+ });
return c.json(
{
@@ -559,13 +557,8 @@ const deleteGroupDebtSchema = route.createSchema(
},
},
responses: {
- 200: {
- description: 'Created',
- content: {
- 'application/json': {
- schema: deleteGroupDebtResponseSchema,
- },
- },
+ 204: {
+ description: 'No Content',
},
},
},
@@ -573,51 +566,43 @@ const deleteGroupDebtSchema = route.createSchema(
);
hono.openapi(deleteGroupDebtSchema, async (c) => {
- c.status(200);
- const body = c.req.valid('json');
const loginUser = c.get('user');
+ const body = c.req.valid('json');
- // NOTE: このvalidationは、各グループの情報を取得するエンドポイントでも全く同じものを使うので、共通化を検討すること
- // NOTE: --- 共通化開始 ---
- // validation
- if (!body || !body.group_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
- const groupId = body.group_id;
+ // 現在時刻の取得
+ const now = new Date();
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // find me from the group (validation)
+ // NOTE: --- 共通化開始 ---
+ // loginUser が body.group_id のグループのメンバーであることを確認
const me = await db
- .select()
+ .select({
+ groupMembershipId: groupMembership.id,
+ })
.from(groupMembership)
- .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUser.id)))
+ .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id)))
.limit(1);
+
+ // body.group_id のグループのメンバーでない場合はエラー
if (me.length === 0) {
- // validate membership
- throw new HTTPException(401, { message: 'User is not a member of the group' });
+ throw new HTTPException(400, { message: 'Bad Request' });
}
// NOTE: --- 共通化終了 ---
- const now = new Date();
- // delete group debt entry (soft delete)
- const result = await db
+ //* body.debt_id の貸し借りの履歴の削除 (論理削除) *//
+ await db
.update(debt)
.set({
deletedBy: loginUser.id,
deletedAt: now,
updatedAt: now,
})
- .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, groupId)))
- .returning();
+ .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, body.group_id), isNull(debt.deletedAt)));
- return c.json(
- {
- debt_id: result[0].id,
- } satisfies DeleteGroupDebtResponseSchemaType,
- 200
- );
+ // レスポンス
+ return c.body(null, 204);
});
export default hono;
diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts
index 0634a47..6a7452c 100644
--- a/products/backend/src/presentation/routes/info.ts
+++ b/products/backend/src/presentation/routes/info.ts
@@ -1,5 +1,4 @@
// hono instance
-import { HTTPException } from 'hono/http-exception';
import honoFactory from '../factory/hono';
// validator
import {
@@ -11,8 +10,6 @@ import {
type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType,
type InfoAboutGroupsTheUserBelongsToResponseSchemaType,
deleteInfoAboutUserRepaymentRequestSchema,
- deleteInfoAboutUserRepaymentResponseSchema,
- DeleteInfoAboutUserRepaymentResponseSchemaType,
} from 'validator';
// error schema
import { route } from '../share/error';
@@ -20,7 +17,9 @@ import { route } from '../share/error';
import { drizzle } from 'drizzle-orm/node-postgres';
import { and, or, eq, inArray, isNull } from 'drizzle-orm';
import { group, debt, groupMembership } from '../../db/pay-crew2-schema';
-import { user, userProfile } from '../../db/auth-schema';
+import { user } from '../../db/auth-schema';
+// types
+import { TransactionType } from '../utils/types';
const hono = honoFactory();
@@ -29,7 +28,7 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema(
{
path: '/api/info/group',
method: 'get',
- description: 'get info about groups the user belongs to',
+ description: 'ログインユーザーが参加しているグループ一覧を取得するエンドポイント',
security: [{ SessionCookie: [] }],
request: {},
responses: {
@@ -49,11 +48,13 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema(
hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => {
const loginUser = c.get('user');
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- //* get group info that belongs to the user *//
+ // グループ情報格納用配列
let groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = [];
+
+ //* ユーザが参加しているグループ情報を取得 (group table) *//
const groupsData = await db
.select({
id: group.id,
@@ -72,50 +73,68 @@ hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => {
);
for (const groupData of groupsData) {
- //* get members *//
- // get all member userIds from groupMembership table
- const membersData = await db.select().from(groupMembership).where(eq(groupMembership.groupId, groupData.id));
- const memberUserIds = membersData.map((member) => member.userId);
- // get user name or display name for each member
+ //* body.group_id のグループ作成者情報を取得 *//
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const createdByUserNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, groupData.createdBy))
+ .limit(1);
+
+ // NOTE: --- 共通化開始 ---
+ //* groupData.id のメンバー情報を取得 *//
+ // グループメンバーのユーザーIDを取得 (group_membership table)
+ const memberUserIds = await db
+ .select({
+ userId: groupMembership.userId,
+ })
+ .from(groupMembership)
+ .where(eq(groupMembership.groupId, groupData.id));
+
+ // メンバー情報を格納する配列
const members: InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] = [];
+
for (const memberUserId of memberUserIds) {
- // initialize user name
- let userName = '';
-
- // get user name from user table
- const userNameInfo = await db.select().from(user).where(eq(user.id, memberUserId)).limit(1);
- if (userNameInfo.length === 0) {
- throw new HTTPException(500, { message: 'A group member was not found' });
- }
- //set user name
- userName = userNameInfo[0].name;
-
- // get display name from user_profile table
- const displayNameInfo = await db.select().from(userProfile).where(eq(userProfile.userId, memberUserId)).limit(1);
- if (
- displayNameInfo.length > 0 &&
- typeof displayNameInfo[0].displayName === 'string' &&
- displayNameInfo[0].displayName.length > 0
- ) {
- userName = displayNameInfo[0].displayName;
- }
-
- // push to members array
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const userNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, memberUserId.userId))
+ .limit(1);
+
+ // メンバー情報を配列に追加
members.push({
- user_id: memberUserId,
- user_name: userName,
+ user_id: memberUserId.userId,
+ user_name:
+ userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0
+ ? userNameInfo[0].displayName
+ : userNameInfo[0].name,
});
}
- // push to groupInfo array
+ // NOTE: --- 共通化終了 ---
+
+ // グループ情報を配列に追加
groupInfo.push({
group_id: groupData.id,
group_name: groupData.name,
- created_by: groupData.createdBy,
+ created_by_id: groupData.createdBy,
+ created_by_name:
+ createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0
+ ? createdByUserNameInfo[0].displayName
+ : createdByUserNameInfo[0].name,
members: members,
});
}
- // return response
+ // レスポンス
return c.json(
{
groups: groupInfo,
@@ -129,7 +148,7 @@ const infoAboutUserTransactionsSchema = route.createSchema(
{
path: '/api/info/transaction',
method: 'get',
- description: 'get info about user transactions',
+ description: 'ログインユーザーの貸し借りの履歴を取得するエンドポイント',
security: [{ SessionCookie: [] }],
request: {},
responses: {
@@ -146,39 +165,40 @@ const infoAboutUserTransactionsSchema = route.createSchema(
[401, 500] as const
);
-type TransactionType = {
- user_id: string; // 取引相手のユーザID
- lent_amount: number; // 貸した金額
- borrowed_amount: number; // 借りた金額
-};
-
hono.openapi(infoAboutUserTransactionsSchema, async (c) => {
const loginUser = c.get('user');
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- // loginUserが貸している取引履歴を取得
+ //* loginUserが貸している取引履歴を取得 *//
// マイナス n 円
const creditorTransactions = await db
- .select()
+ .select({
+ debtorId: debt.debtorId,
+ amount: debt.amount,
+ })
.from(debt)
.where(and(eq(debt.creditorId, loginUser.id), isNull(debt.deletedAt)));
- // loginUserが借りている取引履歴を取得
+ //* loginUserが借りている取引履歴を取得 *//
// プラス n 円
const debtorTransactions = await db
- .select()
+ .select({
+ creditorId: debt.creditorId,
+ amount: debt.amount,
+ })
.from(debt)
.where(and(eq(debt.debtorId, loginUser.id), isNull(debt.deletedAt)));
- // 取引相手ごとに集計
+ //* 取引相手ごとに集計 *//
+ // 集計結果を格納するMap
const transactions: Map = new Map();
// 貸している取引を集計
- for (const transaction of creditorTransactions) {
- const userId = transaction.debtorId;
- const amount = transaction.amount;
+ for (const creditorTransaction of creditorTransactions) {
+ const userId = creditorTransaction.debtorId;
+ const amount = creditorTransaction.amount;
if (!transactions.has(userId)) {
transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 });
}
@@ -187,9 +207,9 @@ hono.openapi(infoAboutUserTransactionsSchema, async (c) => {
}
// 借りている取引を集計
- for (const transaction of debtorTransactions) {
- const userId = transaction.creditorId;
- const amount = transaction.amount;
+ for (const debtorTransaction of debtorTransactions) {
+ const userId = debtorTransaction.creditorId;
+ const amount = debtorTransaction.amount;
if (!transactions.has(userId)) {
transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 });
}
@@ -197,38 +217,33 @@ hono.openapi(infoAboutUserTransactionsSchema, async (c) => {
existing.borrowed_amount += amount;
}
- // netAmount の計算
+ // 集計結果を格納する配列
const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = [];
+
+ // 貸し借りの合算
for (const transaction of transactions.values()) {
- // calculate net amount: netAmount = borrowed_amount - lent_amount
+ // 合算結果: netAmount = borrowed_amount - lent_amount
const netAmount = transaction.borrowed_amount - transaction.lent_amount;
- let userName = '';
- // get user name from user table
- const userInfo = await db.select().from(user).where(eq(user.id, transaction.user_id)).limit(1);
- if (userInfo.length === 0) {
- throw new HTTPException(500, { message: 'A transaction counterparty was not found' });
- }
- userName = userInfo[0].name;
- // get display name from user_profile table
- const displayNameInfo = await db
- .select()
- .from(userProfile)
- .where(eq(userProfile.userId, transaction.user_id))
+ // NOTE: 共通化できそう
+ // ユーザ名を取得 (user table)
+ const userNameInfo = await db
+ .select({
+ name: user.name,
+ displayName: user.displayName,
+ })
+ .from(user)
+ .where(eq(user.id, transaction.user_id))
.limit(1);
- if (
- displayNameInfo.length > 0 &&
- typeof displayNameInfo[0].displayName === 'string' &&
- displayNameInfo[0].displayName.length > 0
- ) {
- userName = displayNameInfo[0].displayName;
- }
- // push to aggregatedTransactions array if netAmount is not zero
+ // netAmount が 0 でない場合のみ配列に追加
if (netAmount !== 0) {
aggregatedTransactions.push({
counterparty_id: transaction.user_id,
- counterparty_name: userName,
+ counterparty_name:
+ userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0
+ ? userNameInfo[0].displayName
+ : userNameInfo[0].name,
amount: netAmount,
});
}
@@ -248,7 +263,7 @@ const infoUserRepaymentSchema = route.createSchema(
{
path: '/api/info/transaction',
method: 'delete',
- description: 'do user repayment',
+ description: 'ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント',
security: [{ SessionCookie: [] }],
request: {
body: {
@@ -261,34 +276,24 @@ const infoUserRepaymentSchema = route.createSchema(
},
},
responses: {
- 200: {
- description: 'Created',
- content: {
- 'application/json': {
- schema: deleteInfoAboutUserRepaymentResponseSchema,
- },
- },
+ 204: {
+ description: 'No Content',
},
},
},
- [400, 401, 500] as const
+ [401, 500] as const
);
hono.openapi(infoUserRepaymentSchema, async (c) => {
- const body = c.req.valid('json');
const loginUser = c.get('user');
+ const body = c.req.valid('json');
- // validation
- if (!body || !body.counterparty_id) {
- throw new HTTPException(400, { message: 'Bad Request' });
- }
-
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
const now = new Date();
// loginUser <-> counterparty_id 間の取引履歴を削除(deletedAt, deletedBy をセット)
- const creditorTransactions = await db
+ await db
.update(debt)
.set({
deletedBy: loginUser.id,
@@ -300,16 +305,10 @@ hono.openapi(infoUserRepaymentSchema, async (c) => {
and(eq(debt.creditorId, loginUser.id), eq(debt.debtorId, body.counterparty_id), isNull(debt.deletedAt)),
and(eq(debt.debtorId, loginUser.id), eq(debt.creditorId, body.counterparty_id), isNull(debt.deletedAt))
)
- )
- .returning();
+ );
- // return response
- return c.json(
- {
- counterparty_id: body.counterparty_id,
- } satisfies DeleteInfoAboutUserRepaymentResponseSchemaType,
- 200
- );
+ // レスポンス
+ return c.body(null, 204);
});
export default hono;
diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts
index 5fa3170..912d9ae 100644
--- a/products/backend/src/presentation/routes/userProfile.ts
+++ b/products/backend/src/presentation/routes/userProfile.ts
@@ -1,20 +1,17 @@
// hono instance
-import { HTTPException } from 'hono/http-exception';
import honoFactory from '../factory/hono';
// validator
import {
getUserProfileResponseSchema,
type GetUserProfileResponseSchemaType,
updateUserProfileRequestSchema,
- updateUserProfileResponseSchema,
- type UpdateUserProfileResponseSchemaType,
} from 'validator';
// error schema
import { route } from '../share/error';
// drizzle
import { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
-import { userProfile } from '../../db/auth-schema';
+import { user } from '../../db/auth-schema';
const hono = honoFactory();
@@ -23,7 +20,7 @@ const getUserProfile = route.createSchema(
{
path: '/api/profile',
method: 'get',
- description: 'get user profile',
+ description: 'ログインユーザーのプロフィール情報を取得するエンドポイント',
security: [{ SessionCookie: [] }],
request: {},
responses: {
@@ -37,39 +34,28 @@ const getUserProfile = route.createSchema(
},
},
},
- [400, 401, 500] as const
+ [401, 500] as const
);
hono.openapi(getUserProfile, async (c) => {
- const user = c.get('user');
+ const loginUser = c.get('user');
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- //* get user profile *//
- const userProfileData = await db
- .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio })
- .from(userProfile)
- .where(eq(userProfile.userId, user.id))
+ //* ユーザ情報を取得 (user table) *//
+ const userData = await db
+ .select({ display_name: user.displayName, avatar_url: user.avatarUrl, bio: user.bio })
+ .from(user)
+ .where(eq(user.id, loginUser.id))
.limit(1);
- if (userProfileData.length === 0) {
- return c.json(
- {
- display_name: '',
- avatar_url: '',
- bio: '',
- } satisfies GetUserProfileResponseSchemaType,
- 200
- );
- }
-
- // return response
+ // レスポンス
return c.json(
{
- display_name: userProfileData[0].display_name === null ? '' : userProfileData[0].display_name,
- avatar_url: userProfileData[0].avatar_url === null ? '' : userProfileData[0].avatar_url,
- bio: userProfileData[0].bio === null ? '' : userProfileData[0].bio,
+ display_name: userData[0].display_name === null ? '' : userData[0].display_name,
+ avatar_url: userData[0].avatar_url === null ? '' : userData[0].avatar_url,
+ bio: userData[0].bio === null ? '' : userData[0].bio,
} satisfies GetUserProfileResponseSchemaType,
200
);
@@ -79,8 +65,8 @@ hono.openapi(getUserProfile, async (c) => {
const updateUserProfile = route.createSchema(
{
path: '/api/profile',
- method: 'put',
- description: 'update user profile',
+ method: 'patch',
+ description: 'ログインユーザーのプロフィール情報を更新するエンドポイント',
security: [{ SessionCookie: [] }],
request: {
body: {
@@ -93,78 +79,33 @@ const updateUserProfile = route.createSchema(
},
},
responses: {
- 200: {
- description: 'OK',
- content: {
- 'application/json': {
- schema: updateUserProfileResponseSchema,
- },
- },
+ 204: {
+ description: 'No Content',
},
},
},
- [400, 401, 500] as const
+ [401, 500] as const
);
hono.openapi(updateUserProfile, async (c) => {
- const user = c.get('user');
+ const loginUser = c.get('user');
const body = c.req.valid('json');
- // connect to db
+ // データベース接続
const db = drizzle({ connection: c.env.HYPERDRIVE });
- //* check user profile exists *//
- const isUserProfileData = await db
- .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio })
- .from(userProfile)
- .where(eq(userProfile.userId, user.id))
- .limit(1);
-
- if (isUserProfileData.length === 0) {
- //* insert empty profile *//
- const result = await db.insert(userProfile).values({
- userId: user.id,
+ //* ユーザ情報を更新 (user table) *//
+ await db
+ .update(user)
+ .set({
displayName: typeof body.display_name === 'undefined' ? null : body.display_name,
avatarUrl: typeof body.avatar_url === 'undefined' ? null : body.avatar_url,
bio: typeof body.bio === 'undefined' ? null : body.bio,
- });
+ })
+ .where(eq(user.id, loginUser.id));
- console.log('Update Result:', result);
- } else {
- //* update user profile *//
- const result = await db
- .update(userProfile)
- .set({
- displayName: typeof body.display_name === 'undefined' ? null : body.display_name,
- avatarUrl: typeof body.avatar_url === 'undefined' ? null : body.avatar_url,
- bio: typeof body.bio === 'undefined' ? null : body.bio,
- })
- .where(eq(userProfile.userId, user.id));
-
- console.log('Update Result:', result);
- }
-
- //* get updated user profile *//
- const userProfileData = await db
- .select({ display_name: userProfile.displayName, avatar_url: userProfile.avatarUrl, bio: userProfile.bio })
- .from(userProfile)
- .where(eq(userProfile.userId, user.id))
- .limit(1);
-
- // validation
- if (userProfileData.length === 0) {
- throw new HTTPException(500, { message: 'Failed to retrieve updated user profile' });
- }
-
- // return response
- return c.json(
- {
- display_name: userProfileData[0].display_name === null ? '' : userProfileData[0].display_name,
- avatar_url: userProfileData[0].avatar_url === null ? '' : userProfileData[0].avatar_url,
- bio: userProfileData[0].bio === null ? '' : userProfileData[0].bio,
- } satisfies UpdateUserProfileResponseSchemaType,
- 200
- );
+ // レスポンス
+ return c.body(null, 204);
});
export default hono;
diff --git a/products/backend/src/presentation/utils/types.ts b/products/backend/src/presentation/utils/types.ts
new file mode 100644
index 0000000..b853b82
--- /dev/null
+++ b/products/backend/src/presentation/utils/types.ts
@@ -0,0 +1,5 @@
+export type TransactionType = {
+ user_id: string; // 取引相手のユーザID
+ lent_amount: number; // 貸した金額
+ borrowed_amount: number; // 借りた金額
+};
diff --git a/products/frontend/.env.example b/products/frontend/.env.example
index 59a578e..0419fb2 100644
--- a/products/frontend/.env.example
+++ b/products/frontend/.env.example
@@ -1,2 +1,3 @@
VITE_API_URL=
+VITE_CLIENT_URL=
VITE_REDIRECT_URL=
diff --git a/products/frontend/openapi.json b/products/frontend/openapi.json
index 0789491..f8b95d0 100644
--- a/products/frontend/openapi.json
+++ b/products/frontend/openapi.json
@@ -1,10 +1,5 @@
{
"openapi": "3.1.0",
- "security": [
- {
- "SessionCookie": []
- }
- ],
"info": {
"title": "Pay Crew2 API",
"version": "0.0.1",
@@ -24,7 +19,7 @@
"paths": {
"/api/info/group": {
"get": {
- "description": "get info about groups the user belongs to",
+ "description": "ログインユーザーが参加しているグループ一覧を取得するエンドポイント",
"security": [
{
"SessionCookie": []
@@ -51,7 +46,11 @@
"type": "string",
"minLength": 1
},
- "created_by": {
+ "created_by_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "created_by_name": {
"type": "string",
"minLength": 1
},
@@ -79,7 +78,8 @@
"required": [
"group_id",
"group_name",
- "created_by",
+ "created_by_id",
+ "created_by_name",
"members"
]
}
@@ -873,7 +873,7 @@
},
"/api/info/transaction": {
"get": {
- "description": "get info about user transactions",
+ "description": "ログインユーザーの貸し借りの履歴を取得するエンドポイント",
"security": [
{
"SessionCookie": []
@@ -1699,7 +1699,7 @@
}
},
"delete": {
- "description": "do user repayment",
+ "description": "ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント",
"security": [
{
"SessionCookie": []
@@ -1725,27 +1725,11 @@
}
},
"responses": {
- "200": {
- "description": "Created",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "counterparty_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "counterparty_id"
- ]
- }
- }
- }
+ "204": {
+ "description": "No Content"
},
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -2132,8 +2116,8 @@
}
}
},
- "401": {
- "description": "Unauthorized",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -2519,9 +2503,64 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/group/create": {
+ "post": {
+ "description": "グループを新規作成するエンドポイント",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_name": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "group_name"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "invite_id": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "group_id",
+ "invite_id"
+ ]
+ }
+ }
+ }
},
- "500": {
- "description": "Internal Server Error",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -2907,64 +2946,9 @@
}
}
}
- }
- }
- }
- },
- "/api/group/create": {
- "post": {
- "description": "create a new group",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "name"
- ]
- }
- }
- }
- },
- "responses": {
- "201": {
- "description": "Created",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "format": "uuid"
- },
- "invite_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "group_id",
- "invite_id"
- ]
- }
- }
- }
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -3350,9 +3334,59 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/group/join": {
+ "post": {
+ "description": "招待IDを使ってグループに参加するエンドポイント",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "invite_id": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "invite_id"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "group_id"
+ ]
+ }
+ }
+ }
},
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"content": {
"application/json": {
"schema": {
@@ -3739,8 +3773,8 @@
}
}
},
- "500": {
- "description": "Internal Server Error",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -4126,59 +4160,9 @@
}
}
}
- }
- }
- }
- },
- "/api/group/join": {
- "post": {
- "description": "join an existing group",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "invite_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "invite_id"
- ]
- }
- }
- }
- },
- "responses": {
- "201": {
- "description": "Created",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "format": "uuid"
- }
- },
- "required": [
- "group_id"
- ]
- }
- }
- }
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -4564,12 +4548,88 @@
}
}
}
- },
- "401": {
- "description": "Unauthorized",
- "content": {
- "application/json": {
- "schema": {
+ }
+ }
+ }
+ },
+ "/api/group/info": {
+ "post": {
+ "description": "get group information",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "group_id"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_name": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "created_by": {
+ "type": "string",
+ "minLength": 1
+ },
+ "members": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "user_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "user_name": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "user_id",
+ "user_name"
+ ]
+ }
+ }
+ },
+ "required": [
+ "group_name",
+ "created_by",
+ "members"
+ ]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
"type": "object",
"properties": {
"status": {
@@ -4953,8 +5013,8 @@
}
}
},
- "500": {
- "description": "Internal Server Error",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -5340,85 +5400,9 @@
}
}
}
- }
- }
- }
- },
- "/api/group/info": {
- "post": {
- "description": "get group information",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "group_id"
- ]
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_name": {
- "type": "string",
- "format": "uuid"
- },
- "created_by": {
- "type": "string",
- "minLength": 1
- },
- "members": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "user_id": {
- "type": "string",
- "minLength": 1
- },
- "user_name": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "user_id",
- "user_name"
- ]
- }
- }
- },
- "required": [
- "group_name",
- "created_by",
- "members"
- ]
- }
- }
- }
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -5804,9 +5788,95 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/group/debt/history": {
+ "post": {
+ "description": "get group debt history",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "group_id"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "debts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "debt_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "debtor_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "debtor_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "creditor_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "creditor_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "amount": {
+ "type": "number",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "debt_id",
+ "debtor_id",
+ "debtor_name",
+ "creditor_id",
+ "creditor_name",
+ "amount"
+ ]
+ }
+ }
+ },
+ "required": [
+ "debts"
+ ]
+ }
+ }
+ }
},
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"content": {
"application/json": {
"schema": {
@@ -6193,1732 +6263,8 @@
}
}
},
- "500": {
- "description": "Internal Server Error",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "status": {
- "anyOf": [
- {
- "type": "number",
- "enum": [
- 100
- ]
- },
- {
- "type": "number",
- "enum": [
- 102
- ]
- },
- {
- "type": "number",
- "enum": [
- 103
- ]
- },
- {
- "type": "number",
- "enum": [
- 200
- ]
- },
- {
- "type": "number",
- "enum": [
- 201
- ]
- },
- {
- "type": "number",
- "enum": [
- 202
- ]
- },
- {
- "type": "number",
- "enum": [
- 203
- ]
- },
- {
- "type": "number",
- "enum": [
- 206
- ]
- },
- {
- "type": "number",
- "enum": [
- 207
- ]
- },
- {
- "type": "number",
- "enum": [
- 208
- ]
- },
- {
- "type": "number",
- "enum": [
- 226
- ]
- },
- {
- "type": "number",
- "enum": [
- 300
- ]
- },
- {
- "type": "number",
- "enum": [
- 301
- ]
- },
- {
- "type": "number",
- "enum": [
- 302
- ]
- },
- {
- "type": "number",
- "enum": [
- 303
- ]
- },
- {
- "type": "number",
- "enum": [
- 305
- ]
- },
- {
- "type": "number",
- "enum": [
- 306
- ]
- },
- {
- "type": "number",
- "enum": [
- 307
- ]
- },
- {
- "type": "number",
- "enum": [
- 308
- ]
- },
- {
- "type": "number",
- "enum": [
- 400
- ]
- },
- {
- "type": "number",
- "enum": [
- 401
- ]
- },
- {
- "type": "number",
- "enum": [
- 402
- ]
- },
- {
- "type": "number",
- "enum": [
- 403
- ]
- },
- {
- "type": "number",
- "enum": [
- 404
- ]
- },
- {
- "type": "number",
- "enum": [
- 405
- ]
- },
- {
- "type": "number",
- "enum": [
- 406
- ]
- },
- {
- "type": "number",
- "enum": [
- 407
- ]
- },
- {
- "type": "number",
- "enum": [
- 408
- ]
- },
- {
- "type": "number",
- "enum": [
- 409
- ]
- },
- {
- "type": "number",
- "enum": [
- 410
- ]
- },
- {
- "type": "number",
- "enum": [
- 411
- ]
- },
- {
- "type": "number",
- "enum": [
- 412
- ]
- },
- {
- "type": "number",
- "enum": [
- 413
- ]
- },
- {
- "type": "number",
- "enum": [
- 414
- ]
- },
- {
- "type": "number",
- "enum": [
- 415
- ]
- },
- {
- "type": "number",
- "enum": [
- 416
- ]
- },
- {
- "type": "number",
- "enum": [
- 417
- ]
- },
- {
- "type": "number",
- "enum": [
- 418
- ]
- },
- {
- "type": "number",
- "enum": [
- 421
- ]
- },
- {
- "type": "number",
- "enum": [
- 422
- ]
- },
- {
- "type": "number",
- "enum": [
- 423
- ]
- },
- {
- "type": "number",
- "enum": [
- 424
- ]
- },
- {
- "type": "number",
- "enum": [
- 425
- ]
- },
- {
- "type": "number",
- "enum": [
- 426
- ]
- },
- {
- "type": "number",
- "enum": [
- 428
- ]
- },
- {
- "type": "number",
- "enum": [
- 429
- ]
- },
- {
- "type": "number",
- "enum": [
- 431
- ]
- },
- {
- "type": "number",
- "enum": [
- 451
- ]
- },
- {
- "type": "number",
- "enum": [
- 500
- ]
- },
- {
- "type": "number",
- "enum": [
- 501
- ]
- },
- {
- "type": "number",
- "enum": [
- 502
- ]
- },
- {
- "type": "number",
- "enum": [
- 503
- ]
- },
- {
- "type": "number",
- "enum": [
- 504
- ]
- },
- {
- "type": "number",
- "enum": [
- 505
- ]
- },
- {
- "type": "number",
- "enum": [
- 506
- ]
- },
- {
- "type": "number",
- "enum": [
- 507
- ]
- },
- {
- "type": "number",
- "enum": [
- 508
- ]
- },
- {
- "type": "number",
- "enum": [
- 510
- ]
- },
- {
- "type": "number",
- "enum": [
- 511
- ]
- },
- {
- "type": "number",
- "enum": [
- -1
- ]
- }
- ],
- "example": 400,
- "description": "HTTPステータスコード"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "example": "Bad Request",
- "description": "エラーメッセージ"
- }
- },
- "required": [
- "status",
- "message"
- ]
- }
- }
- }
- }
- }
- }
- },
- "/api/group/debt/history": {
- "post": {
- "description": "get group debt history",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "group_id"
- ]
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "debts": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "debt_id": {
- "type": "string",
- "minLength": 1
- },
- "debtor_id": {
- "type": "string",
- "minLength": 1
- },
- "debtor_name": {
- "type": "string",
- "minLength": 1
- },
- "creditor_id": {
- "type": "string",
- "minLength": 1
- },
- "creditor_name": {
- "type": "string",
- "minLength": 1
- },
- "amount": {
- "type": "number",
- "minimum": 0
- }
- },
- "required": [
- "debt_id",
- "debtor_id",
- "debtor_name",
- "creditor_id",
- "creditor_name",
- "amount"
- ]
- }
- }
- },
- "required": [
- "debts"
- ]
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "status": {
- "anyOf": [
- {
- "type": "number",
- "enum": [
- 100
- ]
- },
- {
- "type": "number",
- "enum": [
- 102
- ]
- },
- {
- "type": "number",
- "enum": [
- 103
- ]
- },
- {
- "type": "number",
- "enum": [
- 200
- ]
- },
- {
- "type": "number",
- "enum": [
- 201
- ]
- },
- {
- "type": "number",
- "enum": [
- 202
- ]
- },
- {
- "type": "number",
- "enum": [
- 203
- ]
- },
- {
- "type": "number",
- "enum": [
- 206
- ]
- },
- {
- "type": "number",
- "enum": [
- 207
- ]
- },
- {
- "type": "number",
- "enum": [
- 208
- ]
- },
- {
- "type": "number",
- "enum": [
- 226
- ]
- },
- {
- "type": "number",
- "enum": [
- 300
- ]
- },
- {
- "type": "number",
- "enum": [
- 301
- ]
- },
- {
- "type": "number",
- "enum": [
- 302
- ]
- },
- {
- "type": "number",
- "enum": [
- 303
- ]
- },
- {
- "type": "number",
- "enum": [
- 305
- ]
- },
- {
- "type": "number",
- "enum": [
- 306
- ]
- },
- {
- "type": "number",
- "enum": [
- 307
- ]
- },
- {
- "type": "number",
- "enum": [
- 308
- ]
- },
- {
- "type": "number",
- "enum": [
- 400
- ]
- },
- {
- "type": "number",
- "enum": [
- 401
- ]
- },
- {
- "type": "number",
- "enum": [
- 402
- ]
- },
- {
- "type": "number",
- "enum": [
- 403
- ]
- },
- {
- "type": "number",
- "enum": [
- 404
- ]
- },
- {
- "type": "number",
- "enum": [
- 405
- ]
- },
- {
- "type": "number",
- "enum": [
- 406
- ]
- },
- {
- "type": "number",
- "enum": [
- 407
- ]
- },
- {
- "type": "number",
- "enum": [
- 408
- ]
- },
- {
- "type": "number",
- "enum": [
- 409
- ]
- },
- {
- "type": "number",
- "enum": [
- 410
- ]
- },
- {
- "type": "number",
- "enum": [
- 411
- ]
- },
- {
- "type": "number",
- "enum": [
- 412
- ]
- },
- {
- "type": "number",
- "enum": [
- 413
- ]
- },
- {
- "type": "number",
- "enum": [
- 414
- ]
- },
- {
- "type": "number",
- "enum": [
- 415
- ]
- },
- {
- "type": "number",
- "enum": [
- 416
- ]
- },
- {
- "type": "number",
- "enum": [
- 417
- ]
- },
- {
- "type": "number",
- "enum": [
- 418
- ]
- },
- {
- "type": "number",
- "enum": [
- 421
- ]
- },
- {
- "type": "number",
- "enum": [
- 422
- ]
- },
- {
- "type": "number",
- "enum": [
- 423
- ]
- },
- {
- "type": "number",
- "enum": [
- 424
- ]
- },
- {
- "type": "number",
- "enum": [
- 425
- ]
- },
- {
- "type": "number",
- "enum": [
- 426
- ]
- },
- {
- "type": "number",
- "enum": [
- 428
- ]
- },
- {
- "type": "number",
- "enum": [
- 429
- ]
- },
- {
- "type": "number",
- "enum": [
- 431
- ]
- },
- {
- "type": "number",
- "enum": [
- 451
- ]
- },
- {
- "type": "number",
- "enum": [
- 500
- ]
- },
- {
- "type": "number",
- "enum": [
- 501
- ]
- },
- {
- "type": "number",
- "enum": [
- 502
- ]
- },
- {
- "type": "number",
- "enum": [
- 503
- ]
- },
- {
- "type": "number",
- "enum": [
- 504
- ]
- },
- {
- "type": "number",
- "enum": [
- 505
- ]
- },
- {
- "type": "number",
- "enum": [
- 506
- ]
- },
- {
- "type": "number",
- "enum": [
- 507
- ]
- },
- {
- "type": "number",
- "enum": [
- 508
- ]
- },
- {
- "type": "number",
- "enum": [
- 510
- ]
- },
- {
- "type": "number",
- "enum": [
- 511
- ]
- },
- {
- "type": "number",
- "enum": [
- -1
- ]
- }
- ],
- "example": 400,
- "description": "HTTPステータスコード"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "example": "Bad Request",
- "description": "エラーメッセージ"
- }
- },
- "required": [
- "status",
- "message"
- ]
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "status": {
- "anyOf": [
- {
- "type": "number",
- "enum": [
- 100
- ]
- },
- {
- "type": "number",
- "enum": [
- 102
- ]
- },
- {
- "type": "number",
- "enum": [
- 103
- ]
- },
- {
- "type": "number",
- "enum": [
- 200
- ]
- },
- {
- "type": "number",
- "enum": [
- 201
- ]
- },
- {
- "type": "number",
- "enum": [
- 202
- ]
- },
- {
- "type": "number",
- "enum": [
- 203
- ]
- },
- {
- "type": "number",
- "enum": [
- 206
- ]
- },
- {
- "type": "number",
- "enum": [
- 207
- ]
- },
- {
- "type": "number",
- "enum": [
- 208
- ]
- },
- {
- "type": "number",
- "enum": [
- 226
- ]
- },
- {
- "type": "number",
- "enum": [
- 300
- ]
- },
- {
- "type": "number",
- "enum": [
- 301
- ]
- },
- {
- "type": "number",
- "enum": [
- 302
- ]
- },
- {
- "type": "number",
- "enum": [
- 303
- ]
- },
- {
- "type": "number",
- "enum": [
- 305
- ]
- },
- {
- "type": "number",
- "enum": [
- 306
- ]
- },
- {
- "type": "number",
- "enum": [
- 307
- ]
- },
- {
- "type": "number",
- "enum": [
- 308
- ]
- },
- {
- "type": "number",
- "enum": [
- 400
- ]
- },
- {
- "type": "number",
- "enum": [
- 401
- ]
- },
- {
- "type": "number",
- "enum": [
- 402
- ]
- },
- {
- "type": "number",
- "enum": [
- 403
- ]
- },
- {
- "type": "number",
- "enum": [
- 404
- ]
- },
- {
- "type": "number",
- "enum": [
- 405
- ]
- },
- {
- "type": "number",
- "enum": [
- 406
- ]
- },
- {
- "type": "number",
- "enum": [
- 407
- ]
- },
- {
- "type": "number",
- "enum": [
- 408
- ]
- },
- {
- "type": "number",
- "enum": [
- 409
- ]
- },
- {
- "type": "number",
- "enum": [
- 410
- ]
- },
- {
- "type": "number",
- "enum": [
- 411
- ]
- },
- {
- "type": "number",
- "enum": [
- 412
- ]
- },
- {
- "type": "number",
- "enum": [
- 413
- ]
- },
- {
- "type": "number",
- "enum": [
- 414
- ]
- },
- {
- "type": "number",
- "enum": [
- 415
- ]
- },
- {
- "type": "number",
- "enum": [
- 416
- ]
- },
- {
- "type": "number",
- "enum": [
- 417
- ]
- },
- {
- "type": "number",
- "enum": [
- 418
- ]
- },
- {
- "type": "number",
- "enum": [
- 421
- ]
- },
- {
- "type": "number",
- "enum": [
- 422
- ]
- },
- {
- "type": "number",
- "enum": [
- 423
- ]
- },
- {
- "type": "number",
- "enum": [
- 424
- ]
- },
- {
- "type": "number",
- "enum": [
- 425
- ]
- },
- {
- "type": "number",
- "enum": [
- 426
- ]
- },
- {
- "type": "number",
- "enum": [
- 428
- ]
- },
- {
- "type": "number",
- "enum": [
- 429
- ]
- },
- {
- "type": "number",
- "enum": [
- 431
- ]
- },
- {
- "type": "number",
- "enum": [
- 451
- ]
- },
- {
- "type": "number",
- "enum": [
- 500
- ]
- },
- {
- "type": "number",
- "enum": [
- 501
- ]
- },
- {
- "type": "number",
- "enum": [
- 502
- ]
- },
- {
- "type": "number",
- "enum": [
- 503
- ]
- },
- {
- "type": "number",
- "enum": [
- 504
- ]
- },
- {
- "type": "number",
- "enum": [
- 505
- ]
- },
- {
- "type": "number",
- "enum": [
- 506
- ]
- },
- {
- "type": "number",
- "enum": [
- 507
- ]
- },
- {
- "type": "number",
- "enum": [
- 508
- ]
- },
- {
- "type": "number",
- "enum": [
- 510
- ]
- },
- {
- "type": "number",
- "enum": [
- 511
- ]
- },
- {
- "type": "number",
- "enum": [
- -1
- ]
- }
- ],
- "example": 400,
- "description": "HTTPステータスコード"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "example": "Bad Request",
- "description": "エラーメッセージ"
- }
- },
- "required": [
- "status",
- "message"
- ]
- }
- }
- }
- },
- "500": {
- "description": "Internal Server Error",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "status": {
- "anyOf": [
- {
- "type": "number",
- "enum": [
- 100
- ]
- },
- {
- "type": "number",
- "enum": [
- 102
- ]
- },
- {
- "type": "number",
- "enum": [
- 103
- ]
- },
- {
- "type": "number",
- "enum": [
- 200
- ]
- },
- {
- "type": "number",
- "enum": [
- 201
- ]
- },
- {
- "type": "number",
- "enum": [
- 202
- ]
- },
- {
- "type": "number",
- "enum": [
- 203
- ]
- },
- {
- "type": "number",
- "enum": [
- 206
- ]
- },
- {
- "type": "number",
- "enum": [
- 207
- ]
- },
- {
- "type": "number",
- "enum": [
- 208
- ]
- },
- {
- "type": "number",
- "enum": [
- 226
- ]
- },
- {
- "type": "number",
- "enum": [
- 300
- ]
- },
- {
- "type": "number",
- "enum": [
- 301
- ]
- },
- {
- "type": "number",
- "enum": [
- 302
- ]
- },
- {
- "type": "number",
- "enum": [
- 303
- ]
- },
- {
- "type": "number",
- "enum": [
- 305
- ]
- },
- {
- "type": "number",
- "enum": [
- 306
- ]
- },
- {
- "type": "number",
- "enum": [
- 307
- ]
- },
- {
- "type": "number",
- "enum": [
- 308
- ]
- },
- {
- "type": "number",
- "enum": [
- 400
- ]
- },
- {
- "type": "number",
- "enum": [
- 401
- ]
- },
- {
- "type": "number",
- "enum": [
- 402
- ]
- },
- {
- "type": "number",
- "enum": [
- 403
- ]
- },
- {
- "type": "number",
- "enum": [
- 404
- ]
- },
- {
- "type": "number",
- "enum": [
- 405
- ]
- },
- {
- "type": "number",
- "enum": [
- 406
- ]
- },
- {
- "type": "number",
- "enum": [
- 407
- ]
- },
- {
- "type": "number",
- "enum": [
- 408
- ]
- },
- {
- "type": "number",
- "enum": [
- 409
- ]
- },
- {
- "type": "number",
- "enum": [
- 410
- ]
- },
- {
- "type": "number",
- "enum": [
- 411
- ]
- },
- {
- "type": "number",
- "enum": [
- 412
- ]
- },
- {
- "type": "number",
- "enum": [
- 413
- ]
- },
- {
- "type": "number",
- "enum": [
- 414
- ]
- },
- {
- "type": "number",
- "enum": [
- 415
- ]
- },
- {
- "type": "number",
- "enum": [
- 416
- ]
- },
- {
- "type": "number",
- "enum": [
- 417
- ]
- },
- {
- "type": "number",
- "enum": [
- 418
- ]
- },
- {
- "type": "number",
- "enum": [
- 421
- ]
- },
- {
- "type": "number",
- "enum": [
- 422
- ]
- },
- {
- "type": "number",
- "enum": [
- 423
- ]
- },
- {
- "type": "number",
- "enum": [
- 424
- ]
- },
- {
- "type": "number",
- "enum": [
- 425
- ]
- },
- {
- "type": "number",
- "enum": [
- 426
- ]
- },
- {
- "type": "number",
- "enum": [
- 428
- ]
- },
- {
- "type": "number",
- "enum": [
- 429
- ]
- },
- {
- "type": "number",
- "enum": [
- 431
- ]
- },
- {
- "type": "number",
- "enum": [
- 451
- ]
- },
- {
- "type": "number",
- "enum": [
- 500
- ]
- },
- {
- "type": "number",
- "enum": [
- 501
- ]
- },
- {
- "type": "number",
- "enum": [
- 502
- ]
- },
- {
- "type": "number",
- "enum": [
- 503
- ]
- },
- {
- "type": "number",
- "enum": [
- 504
- ]
- },
- {
- "type": "number",
- "enum": [
- 505
- ]
- },
- {
- "type": "number",
- "enum": [
- 506
- ]
- },
- {
- "type": "number",
- "enum": [
- 507
- ]
- },
- {
- "type": "number",
- "enum": [
- 508
- ]
- },
- {
- "type": "number",
- "enum": [
- 510
- ]
- },
- {
- "type": "number",
- "enum": [
- 511
- ]
- },
- {
- "type": "number",
- "enum": [
- -1
- ]
- }
- ],
- "example": 400,
- "description": "HTTPステータスコード"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "example": "Bad Request",
- "description": "エラーメッセージ"
- }
- },
- "required": [
- "status",
- "message"
- ]
- }
- }
- }
- }
- }
- }
- },
- "/api/group/debt/register": {
- "post": {
- "description": "register group debt entry",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "minLength": 1
- },
- "creditor_id": {
- "type": "string",
- "minLength": 1
- },
- "debtor_id": {
- "type": "string",
- "minLength": 1
- },
- "amount": {
- "type": "number",
- "minimum": 0
- },
- "description": {
- "type": "string"
- },
- "occurred_at": {
- "type": "string"
- }
- },
- "required": [
- "group_id",
- "creditor_id",
- "debtor_id",
- "amount"
- ]
- }
- }
- }
- },
- "responses": {
- "201": {
- "description": "Created",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "creditorId": {
- "type": "string",
- "minLength": 1
- },
- "debtorId": {
- "type": "string",
- "minLength": 1
- },
- "amount": {
- "type": "number",
- "minimum": 0
- },
- "occurredAt": {
- "type": "string",
- "format": "date"
- }
- },
- "required": [
- "creditorId",
- "debtorId",
- "amount",
- "occurredAt"
- ]
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -8305,8 +6651,8 @@
}
}
},
- "401": {
- "description": "Unauthorized",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -8692,9 +7038,95 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/group/debt/register": {
+ "post": {
+ "description": "register group debt entry",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "creditor_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "debtor_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "amount": {
+ "type": "number",
+ "minimum": 0
+ },
+ "description": {
+ "type": "string"
+ },
+ "occurred_at": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "group_id",
+ "creditor_id",
+ "debtor_id",
+ "amount"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "creditorId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "debtorId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "amount": {
+ "type": "number",
+ "minimum": 0
+ },
+ "occurredAt": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ "required": [
+ "creditorId",
+ "debtorId",
+ "amount",
+ "occurredAt"
+ ]
+ }
+ }
+ }
},
- "500": {
- "description": "Internal Server Error",
+ "400": {
+ "description": "Bad Request",
"content": {
"application/json": {
"schema": {
@@ -9049,95 +7481,40 @@
"enum": [
510
]
- },
- {
- "type": "number",
- "enum": [
- 511
- ]
- },
- {
- "type": "number",
- "enum": [
- -1
- ]
- }
- ],
- "example": 400,
- "description": "HTTPステータスコード"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "example": "Bad Request",
- "description": "エラーメッセージ"
- }
- },
- "required": [
- "status",
- "message"
- ]
- }
- }
- }
- }
- }
- }
- },
- "/api/group/debt/delete": {
- "delete": {
- "description": "delete group debt entry",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "group_id": {
- "type": "string",
- "minLength": 1
- },
- "debt_id": {
- "type": "string",
- "minLength": 1
- }
- },
- "required": [
- "group_id",
- "debt_id"
- ]
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Created",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "debt_id": {
+ },
+ {
+ "type": "number",
+ "enum": [
+ 511
+ ]
+ },
+ {
+ "type": "number",
+ "enum": [
+ -1
+ ]
+ }
+ ],
+ "example": 400,
+ "description": "HTTPステータスコード"
+ },
+ "message": {
"type": "string",
- "minLength": 1
+ "minLength": 1,
+ "example": "Bad Request",
+ "description": "エラーメッセージ"
}
},
"required": [
- "debt_id"
+ "status",
+ "message"
]
}
}
}
},
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -9524,8 +7901,8 @@
}
}
},
- "401": {
- "description": "Unauthorized",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -9911,9 +8288,48 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/group/debt/delete": {
+ "delete": {
+ "description": "delete group debt entry",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "debt_id": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": [
+ "group_id",
+ "debt_id"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "No Content"
},
- "500": {
- "description": "Internal Server Error",
+ "400": {
+ "description": "Bad Request",
"content": {
"application/json": {
"schema": {
@@ -10299,49 +8715,9 @@
}
}
}
- }
- }
- }
- },
- "/api/profile": {
- "get": {
- "description": "get user profile",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "display_name": {
- "type": "string",
- "minLength": 1
- },
- "avatar_url": {
- "type": "string",
- "format": "uri"
- },
- "bio": {
- "type": "string"
- }
- },
- "required": [
- "display_name",
- "avatar_url",
- "bio"
- ]
- }
- }
- }
},
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -10728,8 +9104,8 @@
}
}
},
- "401": {
- "description": "Unauthorized",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -11115,9 +9491,49 @@
}
}
}
+ }
+ }
+ }
+ },
+ "/api/profile": {
+ "get": {
+ "description": "ログインユーザーのプロフィール情報を取得するエンドポイント",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "display_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "bio": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "display_name",
+ "avatar_url",
+ "bio"
+ ]
+ }
+ }
+ }
},
- "500": {
- "description": "Internal Server Error",
+ "401": {
+ "description": "Unauthorized",
"content": {
"application/json": {
"schema": {
@@ -11503,69 +9919,9 @@
}
}
}
- }
- }
- },
- "put": {
- "description": "update user profile",
- "security": [
- {
- "SessionCookie": []
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "display_name": {
- "type": "string"
- },
- "avatar_url": {
- "type": "string",
- "format": "uri"
- },
- "bio": {
- "type": "string"
- }
- }
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "display_name": {
- "type": "string",
- "minLength": 1
- },
- "avatar_url": {
- "type": "string",
- "format": "uri"
- },
- "bio": {
- "type": "string"
- }
- },
- "required": [
- "display_name",
- "avatar_url",
- "bio"
- ]
- }
- }
- }
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
@@ -11951,6 +10307,41 @@
}
}
}
+ }
+ }
+ },
+ "patch": {
+ "description": "ログインユーザーのプロフィール情報を更新するエンドポイント",
+ "security": [
+ {
+ "SessionCookie": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "display_name": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "bio": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "No Content"
},
"401": {
"description": "Unauthorized",
diff --git a/products/frontend/src/api/openapi.d.ts b/products/frontend/src/api/openapi.d.ts
index d65d222..8993b57 100644
--- a/products/frontend/src/api/openapi.d.ts
+++ b/products/frontend/src/api/openapi.d.ts
@@ -4,85 +4,6 @@
*/
export interface paths {
- "/api/session": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** @description sample endpoint to get session info */
- get: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description OK */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- id: string;
- email: string;
- };
- };
- };
- /** @description Unauthorized */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
- };
- /** @description Internal Server Error */
- 500: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
- };
- };
- };
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
"/api/info/group": {
parameters: {
query?: never;
@@ -90,7 +11,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- /** @description get info about groups the user belongs to */
+ /** @description ログインユーザーが参加しているグループ一覧を取得するエンドポイント */
get: {
parameters: {
query?: never;
@@ -111,7 +32,8 @@ export interface paths {
/** Format: uuid */
group_id: string;
group_name: string;
- created_by: string;
+ created_by_id: string;
+ created_by_name: string;
members: {
user_id: string;
user_name: string;
@@ -177,7 +99,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- /** @description get info about user transactions */
+ /** @description ログインユーザーの貸し借りの履歴を取得するエンドポイント */
get: {
parameters: {
query?: never;
@@ -246,7 +168,7 @@ export interface paths {
};
put?: never;
post?: never;
- /** @description do user repayment */
+ /** @description ログインユーザーと指定された取引相手間の取引履歴を削除する (完済する) エンドポイント */
delete: {
parameters: {
query?: never;
@@ -262,36 +184,12 @@ export interface paths {
};
};
responses: {
- /** @description Created */
- 200: {
+ /** @description No Content */
+ 204: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- counterparty_id: string;
- };
- };
- };
- /** @description Bad Request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
+ content?: never;
};
/** @description Unauthorized */
401: {
@@ -349,7 +247,7 @@ export interface paths {
};
get?: never;
put?: never;
- /** @description create a new group */
+ /** @description グループを新規作成するエンドポイント */
post: {
parameters: {
query?: never;
@@ -360,7 +258,7 @@ export interface paths {
requestBody: {
content: {
"application/json": {
- name: string;
+ group_name: string;
};
};
};
@@ -378,26 +276,6 @@ export interface paths {
};
};
};
- /** @description Bad Request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
- };
/** @description Unauthorized */
401: {
headers: {
@@ -455,7 +333,7 @@ export interface paths {
};
get?: never;
put?: never;
- /** @description join an existing group */
+ /** @description 招待IDを使ってグループに参加するエンドポイント */
post: {
parameters: {
query?: never;
@@ -576,8 +454,8 @@ export interface paths {
};
};
responses: {
- /** @description OK */
- 200: {
+ /** @description Created */
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -686,8 +564,8 @@ export interface paths {
};
};
responses: {
- /** @description OK */
- 200: {
+ /** @description Created */
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -912,16 +790,12 @@ export interface paths {
};
};
responses: {
- /** @description Created */
- 200: {
+ /** @description No Content */
+ 204: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- debt_id: string;
- };
- };
+ content?: never;
};
/** @description Bad Request */
400: {
@@ -997,7 +871,7 @@ export interface paths {
path?: never;
cookie?: never;
};
- /** @description get user profile */
+ /** @description ログインユーザーのプロフィール情報を取得するエンドポイント */
get: {
parameters: {
query?: never;
@@ -1021,26 +895,6 @@ export interface paths {
};
};
};
- /** @description Bad Request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
- };
/** @description Unauthorized */
401: {
headers: {
@@ -1083,8 +937,13 @@ export interface paths {
};
};
};
- /** @description update user profile */
- put: {
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /** @description ログインユーザーのプロフィール情報を更新するエンドポイント */
+ patch: {
parameters: {
query?: never;
header?: never;
@@ -1102,39 +961,12 @@ export interface paths {
};
};
responses: {
- /** @description OK */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- display_name: string;
- /** Format: uri */
- avatar_url: string;
- bio: string;
- };
- };
- };
- /** @description Bad Request */
- 400: {
+ /** @description No Content */
+ 204: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- /**
- * @description HTTPステータスコード
- * @example 400
- */
- status: 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1;
- /**
- * @description エラーメッセージ
- * @example Bad Request
- */
- message: string;
- };
- };
+ content?: never;
};
/** @description Unauthorized */
401: {
@@ -1178,11 +1010,6 @@ export interface paths {
};
};
};
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
trace?: never;
};
}
diff --git a/products/frontend/src/routes/GenerateGroup/index.tsx b/products/frontend/src/routes/GenerateGroup/index.tsx
index ddb1360..8da7cc9 100644
--- a/products/frontend/src/routes/GenerateGroup/index.tsx
+++ b/products/frontend/src/routes/GenerateGroup/index.tsx
@@ -1,6 +1,5 @@
import { useState, type FC } from 'react';
// @tanstack/react-query
-// import { useQueryClient } from '@tanstack/react-query';
import { $api } from '../../api/fetchClient';
// react-hook-form
import { ErrorMessage } from '@hookform/error-message';
@@ -12,24 +11,28 @@ import {
type CreateGroupResponseSchemaType,
createGroupRequestSchema,
} from 'validator';
+// react-router
+import { Link } from 'react-router';
const GenerateGroup: FC = () => {
- // const queryClient = useQueryClient();
- const [isResultError, setIsResultError] = useState(false);
+ // コピー状態管理
+ const [copyStatus, setCopyStatus] = useState<'idle' | 'copying' | 'success' | 'error'>('idle');
+ const [inviteUrl, setInviteUrl] = useState(null);
+
+ // グループ作成処理
const [result, setResult] = useState(null);
const { mutate, isSuccess, isPending, isError, error } = $api.useMutation('post', '/api/group/create', {
onSuccess: (data) => {
- if (!data?.group_id || !data?.invite_id) {
- setIsResultError(true);
- } else {
- setResult({
- group_id: data?.group_id,
- invite_id: data?.invite_id,
- });
- }
+ setResult({
+ group_id: data?.group_id,
+ invite_id: data?.invite_id,
+ });
+ setInviteUrl(`${import.meta.env.VITE_CLIENT_URL}/invite/${data?.invite_id}`);
+ setCopyStatus('idle');
},
});
+ // react-hook-formの設定
const {
register,
handleSubmit,
@@ -37,22 +40,35 @@ const GenerateGroup: FC = () => {
} = useForm({
resolver: zodResolver(createGroupRequestSchema),
defaultValues: {
- name: '',
+ group_name: '',
},
});
+ // グループ作成ハンドラ
const onSubmit: SubmitHandler = async (formData) => {
mutate({ body: formData, credentials: 'include' });
};
+ // 招待URLハンドラ
+ const inviteUrlHandler = async (url: string) => {
+ try {
+ setCopyStatus('copying');
+ await navigator.clipboard.writeText(url);
+ setCopyStatus('success');
+ } catch (e) {
+ console.error(e);
+ setCopyStatus('error');
+ }
+ };
+
return (
<>
グループの作成
- {isSuccess && !isResultError && result ? (
+ {isPending && グループの作成中...
}
+ {isError && (
<>
グループ作成結果
- グループID: {result.group_id}
- 招待ID: {result.invite_id}
+ グループの作成に失敗しました。再度お試しください。
>
- ) : isResultError ? (
+ )}
+ {isSuccess && result && (
<>
グループ作成結果
- グループの作成に失敗しました。再度お試しください。
+
+ {inviteUrl && (
+
+
+
+ {copyStatus === 'success' &&
コピーしました
}
+ {copyStatus === 'error' &&
コピーに失敗しました
}
+
+ )}
+ グループページへ移動
>
- ) : null}
+ )}
>
);
};
diff --git a/products/frontend/src/routes/GroupDetail/index.tsx b/products/frontend/src/routes/GroupDetail/index.tsx
index 3b39400..d329ea7 100644
--- a/products/frontend/src/routes/GroupDetail/index.tsx
+++ b/products/frontend/src/routes/GroupDetail/index.tsx
@@ -1,13 +1,12 @@
-import { useEffect, useState, type FC } from 'react';
+import { useEffect, useMemo, useState, type FC } from 'react';
import { useParams } from 'react-router';
// @tanstack/react-query
-// import { useQueryClient } from '@tanstack/react-query';
import { $api } from '../../api/fetchClient';
import {
- registerGroupDebtRequestSchema,
type GetGroupDebtHistoryResponseSchemaType,
type GetGroupInfoResponseSchemaType,
- type RegisterGroupDebtRequestSchemaType,
+ RegisterGroupDebtFormSchema,
+ type RegisterGroupDebtFormSchemaType,
} from 'validator';
// react-hook-form
import { ErrorMessage } from '@hookform/error-message';
@@ -15,54 +14,44 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, type SubmitHandler } from 'react-hook-form';
const GroupDetail: FC = () => {
+ // URLパラメータからgroupIdを取得
const { groupId } = useParams<{ groupId: string }>();
+
+ // groupIdが存在しない場合の処理
if (!groupId) return Group ID is not provided.
;
- const [isGroupInfoResultError, setIsGroupInfoResultError] = useState(false);
+ // グループ情報の取得
const [groupInfoResult, setGroupInfoResult] = useState(null);
const groupInfoMutation = $api.useMutation('post', `/api/group/info`, {
onSuccess: (data) => {
- if (!data?.group_name || !data?.created_by || !data?.members) {
- setIsGroupInfoResultError(true);
- } else {
- // convert dat to GetGroupInfoResponseSchemaType
- const groupInfo: GetGroupInfoResponseSchemaType = {
- group_name: data.group_name,
- created_by: data.created_by,
- members: data.members,
- };
- // set group info result
- setGroupInfoResult(groupInfo);
- }
+ // グループ情報をセット
+ setGroupInfoResult(data);
},
});
- const [isDebtHistoryResultError, setIsDebtHistoryResultError] = useState(false);
+ // グループの貸し借り履歴の取得
const [debtHistoryResult, setDebtHistoryResult] = useState(null);
const debtHistoryMutation = $api.useMutation('post', `/api/group/debt/history`, {
onSuccess: (data) => {
- if (!data?.debts) {
- setIsDebtHistoryResultError(true);
- } else {
- // convert dat to GetGroupDebtHistoryResponseSchemaType
- const debtHistory: GetGroupDebtHistoryResponseSchemaType = {
- debts: data.debts,
- };
- // set group info result
- setDebtHistoryResult(debtHistory);
- }
+ // グループの貸し借り履歴をセット
+ setDebtHistoryResult(data);
},
});
+ // コンポーネントマウント時にグループ情報と貸し借り履歴を取得
useEffect(() => {
groupInfoMutation.mutate({ body: { group_id: groupId }, credentials: 'include' });
debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' });
}, []);
- // register debt form
- const RegisterGroupDebtFormSchema = registerGroupDebtRequestSchema.omit({ group_id: true });
- type RegisterGroupDebtFormSchemaType = Omit;
+ // 貸し借り登録処理
+ const debtRegisterMutation = $api.useMutation('post', `/api/group/debt/register`, {
+ onSuccess: () => {
+ debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' });
+ },
+ });
+ //NOTE: こいつは、どうにかする予定
const toDatetimeLocal = (d: Date) => {
const pad = (n: number) => String(n).padStart(2, '0');
const yyyy = d.getFullYear();
@@ -73,15 +62,12 @@ const GroupDetail: FC = () => {
return `${yyyy}-${mm}-${dd}T${hh}:${mi}`; // "YYYY-MM-DDTHH:mm"
};
- const debtRegisterMutation = $api.useMutation('post', `/api/group/debt/register`, {
- onSuccess: () => {
- debtHistoryMutation.mutate({ body: { group_id: groupId }, credentials: 'include' });
- },
- });
-
+ // react-hook-formの設定
const {
register,
handleSubmit,
+ watch,
+ setValue,
formState: { errors },
} = useForm({
resolver: zodResolver(RegisterGroupDebtFormSchema),
@@ -94,15 +80,33 @@ const GroupDetail: FC = () => {
},
});
+ // 同じ人を借り手/貸し手に選べないようにするためにwatch
+ const debtorId = watch('debtor_id');
+ const creditorId = watch('creditor_id');
+
+ // メンバー一覧の取得
+ // membersのnullを除外して