自分のプロジェクトのコードで学ぶ7テーマ | 英語の意味も全部解説
どちらも「サーバー(自分のパソコンじゃなくVercelのコンピュータ)で動くコード」。
endai-system の演題保存コード:
"use server"; // ← この1行で「このファイルはサーバーで動く」と宣言
export async function saveAbstract(data: FormData) {
要するに: ボタンを押す → サーバーで処理 → 結果が画面に反映。これだけ。
line-crm のWebhook受信コード:
// app/api/webhook/line/route.ts
export async function POST(request: Request) {
: 型名 で「この変数はこういう形ですよ」と宣言する
要するに: LINEやStripeなど外部サービスが呼ぶURLを作る仕組み。
自分のアプリの画面から呼ぶ → Server Actions
外部サービスから呼ばれる → API Routes
endai-systemが40個もActions持ってるのは全部「画面のフォーム送信」だから。line-crmがAPI Routesだけなのは外部(LINE)からの呼び出しだから。
RLS = Row Level Security = 「行レベルのセキュリティ」。データベースの行(1件のデータ)ごとに「誰が見ていいか」を制御する仕組み。
endai-system src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
// ↑「サーバー用の接続係を作る関数」を読み込み
import { cookies } from "next/headers";
// ↑ ブラウザが送ってきたcookie(ログイン情報入りの小さなデータ)を読む関数
export async function createClient() {
const cookieStore = await cookies();
// ↑ cookieの保管庫を取得。awaitは「取得完了を待つ」
return createServerClient(
SUPABASE_URL, // データベースの住所
SUPABASE_ANON_KEY, // 一般用の鍵(anon = anonymous = 匿名)
{
cookies: {
getAll() { return cookieStore.getAll(); },
// ↑「今のログイン情報を全部教えて」とSupabaseに渡す
},
}
);
}
ANON_KEY(匿名の鍵)で接続すると、RLSが効く。つまり「ログインしている人に許可されたデータだけ」が返ってくる。
CREATE POLICY "著者は自分の演題だけ読める"
ON abstracts FOR SELECT
USING (author_id = auth.uid());
つまり: Aさんがログインして全件取得しようとしても、Aさんが著者のデータしか返ってこない。ハッカーが全件取得コードを送りつけても同じ。
line-crm にだけある特別な接続方法:
export function createServiceClient() {
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
// ↑ NEXT_PUBLIC_ がついてない = ブラウザには見えない
{ cookies: { getAll: () => [], setAll: () => {} } }
// ↑ cookieは空 = ログインユーザーなしで接続
);
}
endai-system src/lib/auth.ts
"use server";
type UserRole = "admin" | "organizer" | "reviewer" | "author";
// ↑ type = 型の定義。UserRole(ユーザーの役割)は4種類のどれか
// | は「または」。決まった文字列しか入らない
const ROLE_HIERARCHY: Record<UserRole, number> = {
admin: 4, organizer: 3, reviewer: 2, author: 1,
};
// ↑ Record<キーの型, 値の型> = 辞書(キーと値のペア)
// 数字が大きいほど強い権限
export async function requireRole(minimumRole: UserRole): Promise<AuthResult> {
// ↑ minimumRole = 「最低限必要な役割」
// Promise<AuthResult> = 「AuthResultを返すことを約束する」(非同期だから)
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
// ↑ 分割代入(destructuring = 分解して取り出す)
// supabase.auth.getUser() の結果から data の中の user だけ取り出す
// 書き換えると:
// const result = await supabase.auth.getUser();
// const user = result.data.user; と同じ
if (!user) {
throw new Error("認証エラー: ログインが必要です");
// ↑ throw = 「投げる」。エラーを投げて処理を中断する
}
const { data: profile } = await supabase
.from("profiles") // profilesテーブルから
.select("role") // role列だけ取得
.eq("id", user.id) // eq = equals(等しい)。IDが一致する行
.single(); // 1行だけ(複数あったらエラー)
const userRole = (profile?.role as UserRole) || "author";
// ↑ ?. = オプショナルチェイニング。profileがnullでもエラーにならない
// as UserRole = 型の変換(「これはUserRole型だよ」とTSに教える)
// || = 「または」。左がnull/undefinedなら右の値を使う
if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[minimumRole]) {
throw new Error("権限エラー");
}
return { user: { id: user.id, email: user.email }, role: userRole };
}
export async function deleteConference(id: string) {
await requireRole("admin"); // adminじゃないとここでエラー → 下に進めない
// ... 削除処理
}
bento_order_web の認証コード:
useEffect(() => {
// ↑ useEffect = 「副作用」
// コンポーネントが画面に表示された時に1回だけ実行される
// 「副作用」= 画面の描画以外の処理(データ取得、タイマー、監視など)
const unsubscribe = onAuthChange(async (fbUser) => {
// ↑ onAuthChange = 「認証状態が変わったら教えて」とFirebaseに登録
// fbUser = Firebase User(ログイン中の人。ログアウトするとnull)
// unsubscribe = 「購読解除」。監視をやめるための関数が返ってくる
setFirebaseUser(fbUser);
if (fbUser) {
const userData = await getUserData(fbUser.uid);
// ↑ uid = User ID。Firebaseがユーザーごとにつけるユニークなまとめ
setUser(userData);
} else {
setUser(null); // ログアウト状態
}
setLoading(false);
});
return () => unsubscribe();
// ↑ useEffectから関数を返すと「クリーンアップ」になる
// コンポーネントが画面から消える時に実行される
// 監視を解除しないとメモリリーク(メモリを食い続ける)になる
}, []);
// ↑ [] = 依存配列が空 = 「最初の1回だけ実行」
// [userId] と書くと「userIdが変わるたびに再実行」
bento_order_web のメニュー監視:
return onSnapshot(q, (snapshot) => {
// ↑ onSnapshot = 「データが変わるたびに教えて」
// snapshot = その瞬間のデータの写真(スナップショット)
const items = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
// ↑ ... = スプレッド演算子。中身を全部展開してコピーする
// { name: "カレー", price: 500 }
// → id と合わせて { id: "xxx", name: "カレー", price: 500 }
}));
callback(items);
});
onAuthStateChanged と onSnapshot は同じ仕組み:
登録 → 変化があるたびにコールバック関数が呼ばれる → 不要になったら unsubscribe で解除
const [count, setCount] = useState(0);
// [現在の値, 値を変える関数] = useState(初期値)
// count = 0 から始まる
// setCount(5) で count が 5 になり、画面が自動で再描画される
bento_order_web の認証Context:
const AuthContext = createContext(null);
// ↑ createContext = 「共有スペース」を作る
export function AuthProvider({ children }) {
// ↑ children = この中に入る全部の子コンポーネント
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user }}>
{children}
</AuthContext.Provider>
);
// ↑ Provider = 「提供者」
// value に入れたデータが、中の全コンポーネントから使える
}
// 使う側(アプリ内のどこからでも)
const { user } = useContext(AuthContext);
// ↑ useContext = 「共有スペースからデータを取り出す」
visitcare で使用:
import { create } from "zustand";
const usePatientStore = create((set) => ({
// ↑ create = ストア(データの倉庫)を作る
// set = 値を更新する関数
selectedPatient: null,
setPatient: (patient) => set({ selectedPatient: patient }),
// ↑ これを使っているコンポーネント「だけ」が再描画される
// Contextだと全コンポーネントが再描画されてしまう
}));
// 使う側(Providerで包む必要なし)
const patient = usePatientStore(state => state.selectedPatient);
そのコンポーネントだけで使う → useState
ログイン情報のようにめったに変わらない → Context
頻繁に変わる+複数箇所で使う → Zustand
Next.jsでページ表示時にデータ取得 → Server Componentで直接取得(状態管理不要)
line-crm のWebhook署名検証:
export async function verifySignature(body: string, signature: string) {
// body = LINEから送られてきたメッセージの中身
// signature = LINEが「自分が送った証拠」として付けた文字列
const secret = process.env.LINE_CHANNEL_SECRET!;
// ↑ ! = 「絶対にnullじゃない」とTSに宣言
const encoder = new TextEncoder();
// ↑ 文字列をバイト列(コンピュータが処理できる形)に変換する道具
const key = await crypto.subtle.importKey(
'raw', // 鍵の形式(生データ)
encoder.encode(secret), // secretをバイト列に変換
{ name: 'HMAC', hash: 'SHA-256' }, // 暗号方式
false, // 鍵をエクスポートしない
['sign'] // この鍵は署名に使う
);
// ↑ crypto.subtle = 組み込みの暗号ライブラリ
// importKey = 「鍵を読み込む」
const signatureBuffer = await crypto.subtle.sign(
'HMAC', key, encoder.encode(body)
);
// ↑ bodyを秘密鍵で暗号化 = 自分で署名を計算する
const computed = btoa(
String.fromCharCode(...new Uint8Array(signatureBuffer))
);
// ↑ btoa = Binary to ASCII
// バイナリデータをBase64文字列(安全な文字だけ)に変換
return computed === signature;
// 自分で計算した署名 === LINEが送ってきた署名
// 一致 → 本物のLINEからの通知
// 不一致 → 偽物(誰かがLINEのふりをしている)
}
SECRET(秘密の文字列)を使って、送られてきたデータを暗号化する。同じSECRETで同じデータを暗号化すれば、必ず同じ結果になる。結果が一致すれば「同じSECRETを知っている = 本物のLINE」と確認できる。
endai-system のテスト例:
test("ログインできる", async ({ page }) => {
// ↑ page = Playwrightが操作する仮想ブラウザ
await page.goto("/login");
// ↑ goto = 「行く」。ログインページを開く
await page.fill('[name="email"]', "test@example.com");
// ↑ fill = 「埋める」。入力欄にテキストを入力
await page.click('button[type="submit"]');
// ↑ click = 送信ボタンをクリック
await expect(page).toHaveURL("/dashboard");
// ↑ expect = 「期待する」
// toHaveURL = 「このURLを持っているはず」
});
| 事故 | 原因 | ビルドで検出? |
|---|---|---|
| fetchが全部失敗 | CSPのconnect-srcに通信先を書き忘れ | ❌ |
| ボタンが全部無反応 | CSPのscript-srcに'unsafe-inline'がない | ❌ |
| ビルド成功なのに本番で白画面 | CSPがブラウザ実行時にしか効かない | ❌ |
→ 全部Playwrightでブラウザ実行しないと見つからない
| プロジェクト | Unit | E2E | Security |
|---|---|---|---|
| endai-system | ✅ Vitest | ✅ Playwright (30+) | ✅ Semgrep |
| bento_order_web | ❌ | ✅ Playwright | ❌ |
| line-crm | ❌ | ❌ | ❌ |
| visitcare | ❌ | ❌ | ❌ |
| nurse-expense-app | ❌ | ❌ | ❌ |
| # | テーマ | 一言 |
|---|---|---|
| 1 | Server Actions vs API Routes | 画面から→Actions、外部から→Routes |
| 2 | RLS + Service Role | 普通の鍵=自分のだけ、マスターキー=全部(Webhook用) |
| 3 | Middleware認証 | ページの門番 + データ操作前の二重チェック |
| 4 | Firebase Auth | onAuthStateChanged=ログイン監視、onSnapshot=データ監視 |
| 5 | 状態管理 | 小→useState、認証→Context、複雑→Zustand |
| 6 | 署名検証 | 同じ秘密鍵で暗号化→結果一致なら本物 |
| 7 | テスト | ビルド成功≠動く。ブラウザ実行で初めてわかるバグがある |
| 英語 | 読み方 | 意味 |
|---|---|---|
| export | エクスポート | 外に出す(他のファイルから使えるように) |
| import | インポート | 読み込む(他のファイルから持ってくる) |
| async / await | エイシンク / アウェイト | 非同期 / 待つ(DBやAPIの応答を待つ) |
| const | コンスト | 定数(一度入れたら変えられない変数) |
| throw | スロー | 投げる(エラーを投げて処理を中断) |
| callback | コールバック | 折り返し電話。後で呼んでもらう関数 |
| subscribe / unsubscribe | サブスクライブ | 購読 / 購読解除(変化の監視を始める/やめる) |
| destructuring | デストラクチャリング | 分割代入(オブジェクトから必要な部分だけ取り出す) |
| middleware | ミドルウェア | 中間処理。リクエストがページに届く前に動くコード |
| policy | ポリシー | ルール・方針(RLSのアクセスルール) |
| mutation | ミューテーション | 変更。データを書き換えること |
| query | クエリ | 問い合わせ。データを取得する命令 |
| schema | スキーマ | 構造定義。データがどんな形かの設計図 |
| literal | リテラル | 文字通りの値("admin" など固定文字列) |
| optional chaining | オプショナルチェイニング | ?. nullでもエラーにならない安全なアクセス |
| spread operator | スプレッド演算子 | ... 中身を全部展開してコピーする |