Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ web_modules/
.env
.env.*
!.env.example
!.env.sample

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
# Repository Guidelines

## Project Structure & Module Organization

- `backend/`: Hono-based AWS Lambda handler in TypeScript. Primary entry at `backend/src/index.ts`; build artifacts land in `backend/dist`.
- `frontend/`: Expo Router app. Screens live under `frontend/app/` (e.g., `frontend/app/(tabs)/_layout.tsx`), shared UI in `frontend/components/` and `frontend/components/ui/`, assets in `frontend/assets/images/`, and utilities/hooks in `frontend/hooks/` and `frontend/constants/`.
- Use feature-focused folders; keep shared primitives in `components/ui` and avoid duplicating styling logic across screens.

## Build, Test, and Development Commands

- Backend: from `backend/`, `npm install` then `npm run build` (esbuild bundle to `dist/index.js`). `npm run lint` for ESLint. `npm run deploy` runs build → zip → AWS Lambda update (`aws` CLI must be configured).
- Frontend: from `frontend/`, `npm install` then `npm run start` (Expo dev server), or platform targets via `npm run ios` / `npm run android` / `npm run web`. `npm run lint` uses Expo’s ESLint preset. `npm run reset-project` wipes starter code—use cautiously.

## Coding Style & Naming Conventions

- TypeScript first; prefer explicit return types on exported functions and components.
- 2-space indentation, single quotes, and trailing commas per ESLint/Prettier defaults (match existing files).
- React components: PascalCase filenames (`HelloWave.tsx`), hooks start with `use...`, and route files follow Expo Router patterns (`app/modal.tsx`).
- Keep handlers pure; isolate side effects (network, storage) in small utilities under `hooks/` or `constants/` when possible.

## Testing Guidelines

- No automated tests are present yet; add Jest/Expo Testing Library (frontend) or lightweight integration tests (backend) as you extend features.
- Name specs `*.test.ts` / `*.test.tsx` near the code or under `__tests__/`. Include at least one render/assert path for new UI and one success/error path for new backend endpoints.
- Run linting before opening a PR; treat lint clean as a gate until tests exist.

## Commit & Pull Request Guidelines

- Use short, imperative commit messages with scope hints when helpful (e.g., `frontend: add haptic tab`, `backend: tighten hello handler`). Keep related changes in a single commit.
- PRs should include: concise summary, linked issue/ticket, test or lint command results, and screenshots/video for UI changes (mobile + web when relevant).
- Keep PRs small and focused; prefer follow-up PRs over mixing unrelated refactors.

## Environment & Security Notes

- Node 20+ recommended for backend (esbuild targets `node20`); align local runtime with Lambda. Expo SDK 54 is in use; match the documented Node/Java versions for native tooling.
- Do not commit secrets or AWS credentials. Use local env/config files and verify `.gitignore` coverage before pushing.
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ Pass the `CloudflareBindings` as generics when instantiation `Hono`:

```ts
// src/index.ts
const app = new Hono<{ Bindings: CloudflareBindings }>()
const app = new Hono<{ Bindings: CloudflareBindings }>();
```
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@hono/zod-validator": "^0.7.5",
"@supabase/supabase-js": "^2.87.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.0",
"hono": "^4.10.7",
Expand Down
100 changes: 98 additions & 2 deletions backend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions backend/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import {
jsonb,
timestamp,
boolean,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
} from "drizzle-orm/pg-core"

export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(), // Supabase Authと連携するなら .defaultRandom() は外して手動挿入
id: uuid("id").primaryKey(), // Supabase Auth の user.id をそのまま格納
displayName: text("display_name").notNull(),
level: integer("level").default(1).notNull(),
totalXp: integer("total_xp").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
})

export const enemies = pgTable("enemies", {
id: uuid("id").primaryKey(),
Expand All @@ -24,7 +23,7 @@ export const enemies = pgTable("enemies", {
assetUrl: text("asset_url"),
attackPattern: jsonb("attack_pattern").default({}),
isActive: boolean("is_active").default(true),
});
})

export const battleLogs = pgTable("battle_logs", {
id: uuid("id").primaryKey().defaultRandom(),
Expand All @@ -40,4 +39,4 @@ export const battleLogs = pgTable("battle_logs", {
.$type<Array<{ t: number; d: number; c: boolean }>>()
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
})
40 changes: 40 additions & 0 deletions backend/src/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createClient } from "@supabase/supabase-js"
import type { CloudflareBindings } from "../types"

export const createSupabaseAdminClient = (env: CloudflareBindings) => {
const { SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY } = env

if (!SUPABASE_URL) {
throw new Error("SUPABASE_URL is not set")
}

if (!SUPABASE_SERVICE_ROLE_KEY) {
throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set")
}

return createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
})
}
Comment on lines +4 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit return type annotation.

Per coding guidelines, exported functions should have explicit return types. The function should declare its return type as SupabaseClient.

As per coding guidelines: "Use TypeScript; prefer explicit return types on exported functions and components"

+import type { SupabaseClient } from "@supabase/supabase-js"
 import { createClient } from "@supabase/supabase-js"
 import type { CloudflareBindings } from "../types"

-export const createSupabaseAdminClient = (env: CloudflareBindings) => {
+export const createSupabaseAdminClient = (env: CloudflareBindings): SupabaseClient => {
   const { SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY } = env

   if (!SUPABASE_URL) {
     throw new Error("SUPABASE_URL is not set")
   }

   if (!SUPABASE_SERVICE_ROLE_KEY) {
     throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set")
   }

   return createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
     auth: {
       autoRefreshToken: false,
       persistSession: false,
     },
   })
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/src/lib/supabase.ts around lines 4 to 21, the exported
createSupabaseAdminClient function lacks an explicit return type; update its
signature to declare the return type as SupabaseClient and add a corresponding
type import (e.g. import type { SupabaseClient } from '@supabase/supabase-js')
at the top of the file, keeping the existing implementation and ensuring
TypeScript compiles with the explicit return type.


export const verifyAccessToken = async (
env: CloudflareBindings,
accessToken: string
) => {
const supabase = createSupabaseAdminClient(env)
const { data, error } = await supabase.auth.getUser(accessToken)

if (error) {
throw new Error(`アクセストークンの検証に失敗しました: ${error.message}`)
}

if (!data.user) {
throw new Error("アクセストークンに紐づくユーザーが見つかりませんでした")
}

return data.user
}

48 changes: 43 additions & 5 deletions backend/src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { zValidator } from "@hono/zod-validator"
import { z } from "zod"
import { users } from "../db/schema"
import { getDb } from "../db/client"
import { eq } from "drizzle-orm"
import type { CloudflareBindings } from "../types"
import { verifyAccessToken } from "../lib/supabase"

const app = new Hono<{ Bindings: CloudflareBindings }>()

Expand All @@ -17,12 +17,50 @@ const usersRoutes = app
})
Comment on lines 14 to 21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid exposing full user list to any authenticated user.
GET / returns all rows from users; if this isn’t strictly admin-only, it’s a privacy/data-leak footgun. Consider scoping to authUser.id or adding an authorization layer.

🤖 Prompt for AI Agents
In backend/src/routes/users.ts around lines 14-21, the route currently returns
all user rows to any authenticated caller; change it to enforce authorization:
retrieve the authenticated user from the request context (e.g., c.state.user or
whatever authMiddleware sets), then either (A) scope the query to that user
(WHERE id = authUser.id) for a self-profile endpoint, or (B) check the user's
role/permissions and only allow returning the full list when the user is an
admin; update the DB query accordingly and return 403 when unauthorized.

.post(
"/",
zValidator("json", z.object({ displayName: z.string().min(1) })),
zValidator(
"json",
z.object({
displayName: z.string().min(1),
userId: z.string().uuid(),
})
),
async (c) => {
const { displayName } = c.req.valid("json")
const { displayName, userId } = c.req.valid("json")

const authorization = c.req.header("authorization")
const accessToken = authorization?.replace(/^Bearer\s+/i, "")

if (!accessToken) {
return c.json({ message: "認証情報が不足しています" }, 401)
}

let supabaseUser: { id: string }
try {
supabaseUser = await verifyAccessToken(c.env, accessToken)
} catch (error) {
console.error("アクセストークン検証エラー", error)
return c.json({ message: "認証に失敗しました" }, 401)
}

if (supabaseUser.id !== userId) {
return c.json(
{ message: "トークンのユーザーとリクエストのユーザーが一致しません" },
401
)
}

const db = getDb(c.env)
const res = await db.insert(users).values({ displayName }).returning()
return c.json(res[0])
try {
const res = await db
.insert(users)
.values({ id: userId, displayName })
.returning()

return c.json(res[0])
} catch (error) {
console.error("ユーザー作成エラー", error)
return c.json({ message: "ユーザー作成に失敗しました" }, 500)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle duplicate user insertion more specifically.

The error handling catches all database errors generically. If a user with the same id already exists (unique constraint violation), this returns a generic 500 error.

Consider checking for duplicate key errors and returning a more specific response:

  • 409 Conflict for duplicate users
  • 201 Created for successful insertion
  • Different error messages to distinguish between duplicate and other database errors
  const db = getDb(c.env)
  try {
    const res = await db
      .insert(users)
      .values({ id: userId, displayName })
      .returning()

    return c.json(res[0])
  } catch (error) {
-   console.error("ユーザー作成エラー", error)
-   return c.json({ message: "ユーザー作成に失敗しました" }, 500)
+   console.error("ユーザー作成エラー", error)
+   // Check if it's a unique constraint violation (Postgres error code 23505)
+   if (error instanceof Error && error.message.includes('duplicate key')) {
+     return c.json({ message: "ユーザーは既に存在します" }, 409)
+   }
+   return c.json({ message: "ユーザー作成に失敗しました" }, 500)
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const db = getDb(c.env)
const res = await db.insert(users).values({ displayName }).returning()
return c.json(res[0])
try {
const res = await db
.insert(users)
.values({ id: userId, displayName })
.returning()
return c.json(res[0])
} catch (error) {
console.error("ユーザー作成エラー", error)
return c.json({ message: "ユーザー作成に失敗しました" }, 500)
}
const db = getDb(c.env)
try {
const res = await db
.insert(users)
.values({ id: userId, displayName })
.returning()
return c.json(res[0])
} catch (error) {
console.error("ユーザー作成エラー", error)
// Check if it's a unique constraint violation (Postgres error code 23505)
if (error instanceof Error && error.message.includes('duplicate key')) {
return c.json({ message: "ユーザーは既に存在します" }, 409)
}
return c.json({ message: "ユーザー作成に失敗しました" }, 500)
}
🤖 Prompt for AI Agents
In backend/src/routes/users.ts around lines 52 to 63, the catch block treats all
DB errors the same; update error handling to detect unique constraint /
duplicate-key errors (check the DB error properties such as error.code,
error.sqlState or error.constraint depending on the driver) and when detected
return a 409 Conflict with a clear message about duplicate user; on successful
insert explicitly return 201 Created with the created user body; for
non-duplicate DB errors keep logging full error details and return a 500 with a
generic failure message.

}
)

Expand Down
2 changes: 2 additions & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type CloudflareBindings = {
DATABASE_URL: string;
SUPABASE_URL: string;
SUPABASE_SERVICE_ROLE_KEY: string;
};
4 changes: 1 addition & 3 deletions backend/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"name": "backend",
"main": "src/index.ts",
"compatibility_date": "2025-12-08",
"compatibility_flags": [
"nodejs_compat"
]
"compatibility_flags": ["nodejs_compat"],
// "vars": {
// "MY_VAR": "my-variable"
// },
Expand Down
2 changes: 2 additions & 0 deletions frontend/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EXPO_PUBLIC_SUPABASE_URL=""
EXPO_PUBLIC_SUPABASE_KEY=""
Loading
Loading