-
Notifications
You must be signed in to change notification settings - Fork 3
認証を実装しました。 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
認証を実装しました。 #13
Changes from 3 commits
3d969c0
b59a95c
b9e4e5d
e8a3aeb
1dbdd30
914a4ec
5c70347
484374c
cc3e85d
98a4fa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 }>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,12 +17,50 @@ const usersRoutes = app | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
14
to
21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid exposing full user list to any authenticated user. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.
| 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| EXPO_PUBLIC_SUPABASE_URL="" | ||
| EXPO_PUBLIC_SUPABASE_KEY="" |
There was a problem hiding this comment.
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"
🤖 Prompt for AI Agents