← back to kuchikomi-farm-admin__test

Function bodies 91 total

All specs Real LLM only Function bodies
requireAdmin function · typescript · L8-L21 (14 LOC)
app/actions/admin.ts
async function requireAdmin() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error("未認証です")

  const { data: profile } = await supabase
    .from("profiles")
    .select("role")
    .eq("id", user.id)
    .single()

  if (!profile || profile.role !== "admin") throw new Error("管理者権限がありません")
  return user
}
getAdminUsers function · typescript · L24-L70 (47 LOC)
app/actions/admin.ts
export async function getAdminUsers() {
  await requireAdmin()

  const adminClient = createAdminClient()

  // profiles + referral stats を取得
  const { data: profiles, error } = await adminClient
    .from("profiles")
    .select("id, display_name, email, status, role, rank, created_at")
    .order("created_at", { ascending: false })

  if (error) return { error: "ユーザー一覧の取得に失敗しました" }

  // 各ユーザーの紹介実績を取得
  const users = await Promise.all(
    (profiles || []).map(async (p) => {
      // クリック数は invite_codes.click_count から集計
      const { data: codes } = await adminClient
        .from("invite_codes")
        .select("click_count")
        .eq("created_by", p.id)

      const clicks = (codes || []).reduce((sum, c) => sum + (c.click_count || 0), 0)

      // 登録数は referrals テーブルから集計
      const { data: referrals } = await adminClient
        .from("referrals")
        .select("id, registered_at")
        .eq("referrer_id", p.id)

      const registrations = referrals?.filter((r) => r.regist
getDashboardStats function · typescript · L73-L131 (59 LOC)
app/actions/admin.ts
export async function getDashboardStats() {
  await requireAdmin()
  const adminClient = createAdminClient()

  // 全プロフィール取得
  const { data: profiles } = await adminClient
    .from("profiles")
    .select("id, status, created_at")

  const allUsers = profiles || []
  const totalUsers = allUsers.length
  const activeUsers = allUsers.filter((p) => p.status === "active").length
  const activeRate = totalUsers > 0 ? Math.round((activeUsers / totalUsers) * 100) : 0

  // 今月の新規登録
  const now = new Date()
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
  const monthlyNewUsers = allUsers.filter((p) => p.created_at >= monthStart).length

  // コンテンツ数
  const { count: contentCount } = await adminClient
    .from("contents")
    .select("id", { count: "exact", head: true })

  // 月別登録推移(直近6ヶ月)
  const growthData = []
  for (let i = 5; i >= 0; i--) {
    const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
    const monthEnd = new Date(d.getFullYear(), d.g
getAdminRewards function · typescript · L140-L207 (68 LOC)
app/actions/admin.ts
export async function getAdminRewards() {
  await requireAdmin()
  const adminClient = createAdminClient()

  // 既存の特典を取得
  let { data: rewards } = await adminClient
    .from("rewards")
    .select("*")
    .order("required_referrals", { ascending: true })

  // 3固定ティアが存在しなければ自動作成
  if (!rewards || rewards.length === 0) {
    for (const tier of DEFAULT_TIERS) {
      await adminClient.from("rewards").insert(tier)
    }
    const result = await adminClient
      .from("rewards")
      .select("*")
      .order("required_referrals", { ascending: true })
    rewards = result.data || []
  }

  // 各ティアの達成者数を計算
  const { data: allReferrals } = await adminClient
    .from("referrals")
    .select("referrer_id, registered_at")

  // referrer_id ごとの登録完了数を集計
  const referralCounts: Record<string, number> = {}
  for (const r of allReferrals || []) {
    if (r.registered_at) {
      referralCounts[r.referrer_id] = (referralCounts[r.referrer_id] || 0) + 1
    }
  }

  const rewardsWithStats = rewa
updateAdminReward function · typescript · L210-L223 (14 LOC)
app/actions/admin.ts
export async function updateAdminReward(rewardId: string, title: string, description: string) {
  await requireAdmin()
  const adminClient = createAdminClient()

  const { error } = await adminClient
    .from("rewards")
    .update({ title, description })
    .eq("id", rewardId)

  if (error) return { error: "特典の更新に失敗しました" }

  revalidatePath("/admin")
  return { success: true }
}
updateUserStatus function · typescript · L226-L240 (15 LOC)
app/actions/admin.ts
export async function updateUserStatus(userId: string, newStatus: "active" | "pending") {
  await requireAdmin()

  const adminClient = createAdminClient()

  const { error } = await adminClient
    .from("profiles")
    .update({ status: newStatus })
    .eq("id", userId)

  if (error) return { error: "ステータスの更新に失敗しました" }

  revalidatePath("/admin")
  return { success: true }
}
verifyInviteCode function · typescript · L20-L32 (13 LOC)
app/actions/auth.ts
export async function verifyInviteCode(code: string) {
  const supabase = await createClient()

  const { data, error } = await supabase.rpc("verify_invite_code", {
    input_code: code,
  })

  if (error) {
    return { valid: false as const, error: "検証中にエラーが発生しました" }
  }

  return data as { valid: boolean; referrer_name?: string }
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
signUp function · typescript · L35-L106 (72 LOC)
app/actions/auth.ts
export async function signUp(input: SignUpInput) {
  // 1. Zod バリデーション (サーバーサイド)
  const parsed = signUpSchema.safeParse(input)
  if (!parsed.success) {
    return {
      error: parsed.error.errors[0].message,
      fieldErrors: parsed.error.flatten().fieldErrors,
    }
  }

  const { lastName, firstName, email, password, question, ref } =
    parsed.data

  // 2. 招待コード再検証 (TOCTOU 対策)
  const codeCheck = await verifyInviteCode(ref)
  if (!codeCheck.valid) {
    return { error: "招待リンクが無効です" }
  }

  // 3. Supabase Auth サインアップ
  const supabase = await createClient()

  // 既存セッションを明示的にクリア(ログイン中ユーザーが別アカウントで登録する際の干渉を防止)
  await supabase.auth.signOut()

  const displayName = `${lastName} ${firstName}`

  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        display_name: displayName,
        screening_answer: question,
        invite_code: ref,
      },
      emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
   
signUpWithAdminFallback function · typescript · L110-L270 (161 LOC)
app/actions/auth.ts
async function signUpWithAdminFallback(
  email: string,
  password: string,
  displayName: string,
  question: string,
  ref: string,
) {
  const admin = createAdminClient()

  // 1. まず既存ユーザーをチェック(前回の失敗で auth.users に残っている可能性)
  const { data: existingUsers } = await admin.auth.admin.listUsers()
  const existingUser = existingUsers?.users?.find(u => u.email === email)

  let userId: string

  if (existingUser) {
    // 既存ユーザーが存在 — プロファイルが無いかチェック
    userId = existingUser.id
    const { data: existingProfile } = await admin
      .from("profiles")
      .select("id")
      .eq("id", userId)
      .maybeSingle()

    if (existingProfile) {
      // プロファイルも存在する → 既に登録済み
      return { error: "このメールアドレスは既に登録されています" }
    }
    console.log("[signUp fallback] Found orphaned auth user, creating profile:", userId)
  } else {
    // 2. admin API でユーザーを作成(トリガーを再度試行)
    //    email_confirm: false で確認メールのトリガーは別途行う
    const { data: newUser, error: createError } = await admin.auth.admin.createUser(
signIn function · typescript · L273-L337 (65 LOC)
app/actions/auth.ts
export async function signIn(input: SignInInput) {
  const parsed = signInSchema.safeParse(input)
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }

  const supabase = await createClient()

  // 既存セッションを明示的にクリア(別アカウントへの切替時にセッション競合を防止)
  await supabase.auth.signOut()

  // Supabase Auth ログイン
  const { data, error } = await supabase.auth.signInWithPassword({
    email: parsed.data.email,
    password: parsed.data.password,
  })

  if (error) {
    if (error.status === 429) {
      return {
        error:
          "ログイン試行回数が上限に達しました。しばらく待ってからお試しください",
      }
    }
    return { error: "メールアドレスまたはパスワードが正しくありません" }
  }

  // メール確認チェック
  if (!data.user.email_confirmed_at) {
    await supabase.auth.signOut()
    return {
      error:
        "メールアドレスの確認が完了していません。受信メールをご確認ください",
    }
  }

  // プロフィールステータスチェック
  const { data: profile } = await supabase
    .from("profiles")
    .select("status, role")
    .eq("id", data.user.id)
    .single()

  if (!profile ||
signOut function · typescript · L340-L344 (5 LOC)
app/actions/auth.ts
export async function signOut() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect("/")
}
requestPasswordReset function · typescript · L347-L375 (29 LOC)
app/actions/auth.ts
export async function requestPasswordReset(email: string) {
  const parsed = emailSchema.safeParse(email)
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }

  // アカウント存在チェック(admin client で RLS を迂回)
  const admin = createAdminClient()
  const { count } = await admin
    .from("profiles")
    .select("id", { count: "exact", head: true })
    .eq("email", parsed.data)

  if (!count || count === 0) {
    return { error: "このメールアドレスで登録されたアカウントが見つかりません" }
  }

  const supabase = await createClient()

  const { error } = await supabase.auth.resetPasswordForEmail(parsed.data, {
    redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/reset-callback`,
  })

  if (error) {
    return { error: "リセットメールの送信に失敗しました。しばらく経ってからお試しください" }
  }

  return { success: true }
}
submitScreeningAnswer function · typescript · L378-L405 (28 LOC)
app/actions/auth.ts
export async function submitScreeningAnswer(answer: string) {
  if (!answer || answer.trim().length === 0) {
    return { error: "審査質問への回答を入力してください" }
  }

  const supabase = await createClient()

  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (!user) {
    return { error: "認証されていません" }
  }

  const { error } = await supabase
    .from("profiles")
    .update({ screening_answer: answer.trim() })
    .eq("id", user.id)

  if (error) {
    return { error: "送信中にエラーが発生しました" }
  }

  await supabase.auth.signOut()

  return { success: true }
}
resetPassword function · typescript · L408-L436 (29 LOC)
app/actions/auth.ts
export async function resetPassword(newPassword: string) {
  const parsed = passwordSchema.safeParse(newPassword)
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }

  const supabase = await createClient()

  // リセットリンクで認証済みセッションが必要
  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (!user?.email) {
    return { error: "セッションが無効です。リセットリンクを再度お試しください" }
  }

  const { error } = await supabase.auth.updateUser({
    password: parsed.data,
  })

  if (error) {
    return { error: "パスワードの更新に失敗しました" }
  }

  await supabase.auth.signOut()

  return { success: true }
}
changePassword function · typescript · L439-L474 (36 LOC)
app/actions/auth.ts
export async function changePassword(input: ChangePasswordInput) {
  const parsed = changePasswordSchema.safeParse(input)
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }

  const supabase = await createClient()

  // 現在のパスワードで再認証
  const {
    data: { user },
  } = await supabase.auth.getUser()
  if (!user?.email) {
    return { error: "ユーザー情報を取得できません" }
  }

  const { error: verifyError } = await supabase.auth.signInWithPassword({
    email: user.email,
    password: parsed.data.currentPassword,
  })

  if (verifyError) {
    return { error: "現在のパスワードが正しくありません" }
  }

  // パスワード更新
  const { error } = await supabase.auth.updateUser({
    password: parsed.data.newPassword,
  })

  if (error) {
    return { error: "パスワードの更新に失敗しました" }
  }

  return { success: true }
}
All rows above produced by Repobility · https://repobility.com
getPublishedContents function · typescript · L8-L25 (18 LOC)
app/actions/content.ts
export async function getPublishedContents() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  // admin client を使い RLS に依存しない(Server Action 内で auth.uid() が
  // 正しく解決されないケースを回避)
  const adminClient = createAdminClient()

  const { data, error } = await adminClient
    .from("contents")
    .select("*")
    .eq("status", "published")
    .order("publish_date", { ascending: false })

  if (error) return { error: "コンテンツの取得に失敗しました" }
  return { data: data || [] }
}
getAllContents function · typescript · L28-L50 (23 LOC)
app/actions/content.ts
export async function getAllContents() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const adminClient = createAdminClient()

  const { data: profile } = await adminClient
    .from("profiles")
    .select("role")
    .eq("id", user.id)
    .single()

  if (!profile || profile.role !== "admin") return { error: "管理者権限がありません" }

  const { data, error } = await adminClient
    .from("contents")
    .select("*")
    .order("created_at", { ascending: false })

  if (error) return { error: "コンテンツの取得に失敗しました" }
  return { data: data || [] }
}
createContent function · typescript · L53-L96 (44 LOC)
app/actions/content.ts
export async function createContent(input: {
  type: string
  title: string
  authorName: string
  body?: string
  url?: string
  thumbnailUrl?: string
  status: string
  publishDate?: string
  premium: boolean
  requiredRank: string
  duration?: string
}) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const adminClient = createAdminClient()

  const { data, error } = await adminClient
    .from("contents")
    .insert({
      type: input.type as "article" | "video" | "external",
      title: input.title,
      author_name: input.authorName,
      author_id: user.id,
      body: input.body || null,
      url: input.url || null,
      thumbnail_url: input.thumbnailUrl || null,
      status: input.status as "draft" | "scheduled" | "published",
      publish_date: input.publishDate || (input.status === "published" ? new Date().toISOString() : null),
      premium: input.premium,
      required_
deleteContent function · typescript · L99-L116 (18 LOC)
app/actions/content.ts
export async function deleteContent(id: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const adminClient = createAdminClient()

  const { error } = await adminClient
    .from("contents")
    .delete()
    .eq("id", id)

  if (error) return { error: "コンテンツの削除に失敗しました" }

  revalidatePath("/feed")
  revalidatePath("/admin")
  return { success: true }
}
getContentById function · typescript · L119-L134 (16 LOC)
app/actions/content.ts
export async function getContentById(id: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const adminClient = createAdminClient()

  const { data, error } = await adminClient
    .from("contents")
    .select("*")
    .eq("id", id)
    .single()

  if (error) return { error: "コンテンツが見つかりません" }
  return { data }
}
uploadThumbnail function · typescript · L140-L182 (43 LOC)
app/actions/content.ts
export async function uploadThumbnail(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const file = formData.get("file") as File | null
  if (!file) return { error: "ファイルが選択されていません" }

  // バリデーション
  if (!ALLOWED_TYPES.includes(file.type)) {
    return { error: "対応していない画像形式です(JPEG, PNG, WebP, GIF のみ)" }
  }

  if (file.size > MAX_FILE_SIZE) {
    return { error: "ファイルサイズが5MBを超えています" }
  }

  // ファイル名生成
  const ext = file.name.split(".").pop() || "jpg"
  const fileName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`
  const filePath = fileName

  // admin client で Storage RLS をバイパス
  const adminClient = createAdminClient()

  const { error: uploadError } = await adminClient.storage
    .from("thumbnails")
    .upload(filePath, file, {
      contentType: file.type,
      upsert: false,
    })

  if (uploadError) {
    return { error: `アップロードに失敗しました: ${uploa
updateContentThumbnail function · typescript · L185-L199 (15 LOC)
app/actions/content.ts
export async function updateContentThumbnail(contentId: string, thumbnailUrl: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const adminClient = createAdminClient()

  const { error } = await adminClient
    .from("contents")
    .update({ thumbnail_url: thumbnailUrl })
    .eq("id", contentId)

  if (error) return { error: "サムネイルの更新に失敗しました" }
  return { success: true }
}
uploadVideo function · typescript · L205-L247 (43 LOC)
app/actions/content.ts
export async function uploadVideo(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const file = formData.get("file") as File | null
  if (!file) return { error: "ファイルが選択されていません" }

  // バリデーション
  if (!ALLOWED_VIDEO_TYPES.includes(file.type)) {
    return { error: "対応していない動画形式です(MP4, WebM, MOV, AVI のみ)" }
  }

  if (file.size > VIDEO_MAX_FILE_SIZE) {
    return { error: "ファイルサイズが100MBを超えています" }
  }

  // ファイル名生成
  const ext = file.name.split(".").pop() || "mp4"
  const fileName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`
  const filePath = `videos/${fileName}`

  // admin client で Storage RLS をバイパス
  const adminClient = createAdminClient()

  const { error: uploadError } = await adminClient.storage
    .from("content-media")
    .upload(filePath, file, {
      contentType: file.type,
      upsert: false,
    })

  if (uploadError) {
    return { error:
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
toggleLike function · typescript · L7-L32 (26 LOC)
app/actions/interactions.ts
export async function toggleLike(contentId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data: existing } = await supabase
    .from("content_interactions")
    .select("id")
    .eq("user_id", user.id)
    .eq("content_id", contentId)
    .eq("type", "like")
    .maybeSingle()

  if (existing) {
    await supabase
      .from("content_interactions")
      .delete()
      .eq("id", existing.id)
    return { liked: false }
  } else {
    await supabase
      .from("content_interactions")
      .insert({ user_id: user.id, content_id: contentId, type: "like" })
    return { liked: true }
  }
}
toggleBookmark function · typescript · L35-L60 (26 LOC)
app/actions/interactions.ts
export async function toggleBookmark(contentId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data: existing } = await supabase
    .from("content_interactions")
    .select("id")
    .eq("user_id", user.id)
    .eq("content_id", contentId)
    .eq("type", "bookmark")
    .maybeSingle()

  if (existing) {
    await supabase
      .from("content_interactions")
      .delete()
      .eq("id", existing.id)
    return { bookmarked: false }
  } else {
    await supabase
      .from("content_interactions")
      .insert({ user_id: user.id, content_id: contentId, type: "bookmark" })
    return { bookmarked: true }
  }
}
getUserInteractions function · typescript · L63-L79 (17 LOC)
app/actions/interactions.ts
export async function getUserInteractions(contentId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { liked: false, bookmarked: false }

  const { data } = await supabase
    .from("content_interactions")
    .select("type")
    .eq("user_id", user.id)
    .eq("content_id", contentId)

  const types = (data || []).map(d => d.type)
  return {
    liked: types.includes("like"),
    bookmarked: types.includes("bookmark"),
  }
}
getLikedContents function · typescript · L82-L96 (15 LOC)
app/actions/interactions.ts
export async function getLikedContents() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("content_interactions")
    .select("content_id, created_at, contents(*)")
    .eq("user_id", user.id)
    .eq("type", "like")
    .order("created_at", { ascending: false })

  if (error) return { error: "いいね一覧の取得に失敗しました" }
  return { data }
}
getBookmarkedContents function · typescript · L99-L113 (15 LOC)
app/actions/interactions.ts
export async function getBookmarkedContents() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("content_interactions")
    .select("content_id, created_at, contents(*)")
    .eq("user_id", user.id)
    .eq("type", "bookmark")
    .order("created_at", { ascending: false })

  if (error) return { error: "保存済み一覧の取得に失敗しました" }
  return { data }
}
getRecommendedContents function · typescript · L116-L135 (20 LOC)
app/actions/interactions.ts
export async function getRecommendedContents(limit: number = 3) {
  const supabase = await createClient()

  const { data, error } = await supabase
    .from("contents")
    .select("*, content_interactions(type)")
    .eq("status", "published")

  if (error || !data) return { data: [] }

  const scored = data.map(content => {
    const interactions = (content.content_interactions || []) as { type: string }[]
    const likeCount = interactions.filter(i => i.type === "like").length
    const bookmarkCount = interactions.filter(i => i.type === "bookmark").length
    return { ...content, score: likeCount + bookmarkCount }
  })

  scored.sort((a, b) => b.score - a.score)
  return { data: scored.slice(0, limit) }
}
getProfile function · typescript · L12-L25 (14 LOC)
app/actions/profile.ts
export async function getProfile() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("profiles")
    .select("*")
    .eq("id", user.id)
    .single()

  if (error) return { error: "プロフィールの取得に失敗しました" }
  return { data }
}
updateProfile function · typescript · L28-L53 (26 LOC)
app/actions/profile.ts
export async function updateProfile(input: UpdateProfileInput) {
  const parsed = updateProfileSchema.safeParse(input)
  if (!parsed.success) {
    return {
      error: parsed.error.errors[0].message,
      fieldErrors: parsed.error.flatten().fieldErrors,
    }
  }

  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { error } = await supabase
    .from("profiles")
    .update({
      display_name: parsed.data.displayName,
      bio: parsed.data.bio || null,
    })
    .eq("id", user.id)

  if (error) return { error: "プロフィールの更新に失敗しました" }

  revalidatePath("/settings")
  return { success: true }
}
Repobility · code-quality intelligence platform · https://repobility.com
getNotificationPreferences function · typescript · L56-L69 (14 LOC)
app/actions/profile.ts
export async function getNotificationPreferences() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("notification_preferences")
    .select("*")
    .eq("user_id", user.id)
    .single()

  if (error) return { error: "通知設定の取得に失敗しました" }
  return { data }
}
updateNotificationPreferences function · typescript · L72-L100 (29 LOC)
app/actions/profile.ts
export async function updateNotificationPreferences(prefs: {
  emailNewContent: boolean
  emailNewsletter: boolean
  emailInviteUpdate: boolean
  lineNewContent: boolean
  lineReward: boolean
  pushBrowser: boolean
}) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { error } = await supabase
    .from("notification_preferences")
    .update({
      email_new_content: prefs.emailNewContent,
      email_newsletter: prefs.emailNewsletter,
      email_invite_update: prefs.emailInviteUpdate,
      line_new_content: prefs.lineNewContent,
      line_reward: prefs.lineReward,
      push_browser: prefs.pushBrowser,
    })
    .eq("user_id", user.id)

  if (error) return { error: "通知設定の更新に失敗しました" }

  revalidatePath("/settings")
  return { success: true }
}
getMyInviteCode function · typescript · L103-L147 (45 LOC)
app/actions/profile.ts
export async function getMyInviteCode() {
  const supabase = await createClient()
  const { data: { user }, error: authError } = await supabase.auth.getUser()
  if (authError) {
    console.error("[getMyInviteCode] Auth error:", authError.message)
    return { error: "認証情報の取得に失敗しました" }
  }
  if (!user) return { error: "未認証です" }

  // RPC で既存コード返却 or 新規生成(DB 内でアトミックに処理)
  const { data, error } = await supabase.rpc("generate_invite_code")

  if (error) {
    console.error("[getMyInviteCode] RPC error:", error)
    // RPC が失敗した場合、admin client でフォールバック
    const admin = createAdminClient()
    const { data: existing, error: selectError } = await admin
      .from("invite_codes")
      .select("code")
      .eq("created_by", user.id)
      .order("created_at", { ascending: false })
      .limit(1)
      .maybeSingle()

    if (selectError) {
      console.error("[getMyInviteCode] Fallback select error:", selectError.message)
    }

    if (existing?.code) {
      const appUrl = process.env.
getReferralStats function · typescript · L150-L178 (29 LOC)
app/actions/profile.ts
export async function getReferralStats() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  // クリック数は invite_codes.click_count から集計
  const { data: codes } = await supabase
    .from("invite_codes")
    .select("click_count")
    .eq("created_by", user.id)

  const totalClicks = (codes || []).reduce((sum, c) => sum + (c.click_count || 0), 0)

  // 登録数は referrals テーブルから集計
  const { data: referrals, error } = await supabase
    .from("referrals")
    .select("id, registered_at")
    .eq("referrer_id", user.id)

  if (error) return { error: "紹介実績の取得に失敗しました" }

  const registrations = referrals?.filter(r => r.registered_at !== null).length || 0

  return {
    referralCount: registrations,
    clickCount: totalClicks,
    conversionRate: totalClicks > 0 ? ((registrations / totalClicks) * 100).toFixed(1) : "0",
  }
}
generateInviteCode function · typescript · L181-L204 (24 LOC)
app/actions/profile.ts
export async function generateInviteCode() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase.rpc("generate_invite_code")

  if (error) {
    console.error("[generateInviteCode] RPC error:", error)
    return { error: `招待リンクの取得に失敗しました: ${error.message}` }
  }

  const result = data as { code?: string; error?: string } | null
  if (!result || result.error) {
    return { error: result?.error || "招待リンクの取得に失敗しました" }
  }

  const code = result.code
  const inviteUrl = code
    ? `${process.env.NEXT_PUBLIC_APP_URL}/signup?ref=${code}`
    : null

  return { code, inviteUrl }
}
getInviteSlots function · typescript · L207-L224 (18 LOC)
app/actions/profile.ts
export async function getInviteSlots() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("invite_slots")
    .select("initial_slots, bonus_slots, used_slots")
    .eq("user_id", user.id)
    .single()

  if (error) return { error: "招待枠の取得に失敗しました" }
  return {
    initialSlots: data.initial_slots,
    bonusSlots: data.bonus_slots,
    usedSlots: data.used_slots,
  }
}
getSlotUnlockConditions function · typescript · L234-L252 (19 LOC)
app/actions/profile.ts
export async function getSlotUnlockConditions() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("slot_unlock_conditions")
    .select("condition, completed")
    .eq("user_id", user.id)

  if (error) return { error: "解放条件の取得に失敗しました" }
  return {
    conditions: (data || []).map((row) => ({
      key: row.condition,
      label: conditionLabels[row.condition] || row.condition,
      done: row.completed,
    })),
  }
}
getRewardMilestones function · typescript · L255-L276 (22 LOC)
app/actions/profile.ts
export async function getRewardMilestones() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data: rewards, error } = await supabase
    .from("rewards")
    .select("required_referrals, title, description, icon")
    .eq("status", "active")
    .order("required_referrals", { ascending: true })

  if (error) return { error: "報酬情報の取得に失敗しました" }

  return {
    milestones: (rewards || []).map((r) => ({
      target: r.required_referrals,
      label: `${r.required_referrals}人達成`,
      reward: r.title,
      icon: r.icon,
    })),
  }
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
getLoginHistory function · typescript · L279-L293 (15 LOC)
app/actions/profile.ts
export async function getLoginHistory() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: "未認証です" }

  const { data, error } = await supabase
    .from("login_history")
    .select("*")
    .eq("user_id", user.id)
    .order("logged_in_at", { ascending: false })
    .limit(10)

  if (error) return { error: "ログイン履歴の取得に失敗しました" }
  return { data }
}
AdminPage function · typescript · L23-L73 (51 LOC)
app/admin/page.tsx
export default function AdminPage() {
  const [activeTab, setActiveTab] = useState<TabId>("overview")

  return (
    <div className="min-h-screen bg-[#F8F9FA]">
      <AppHeader isAdmin />

      <div className="mx-auto max-w-[1400px] px-4 md:px-8 lg:px-12 py-8">
        {/* Header */}
        <div className="mb-8">
          <p className="text-xs text-[#1B3022]/40 tracking-wider uppercase">{"Admin Dashboard"}</p>
          <h1 className="font-serif text-2xl text-[#1B3022] mt-1">{"管理者ダッシュボード"}</h1>
        </div>

        {/* Tabs */}
        <div className="flex items-center gap-1 mb-8 overflow-x-auto pb-1 -mx-4 px-4 md:mx-0 md:px-0">
          {tabs.map((tab) => {
            const Icon = tab.icon
            return (
              <button
                key={tab.id}
                type="button"
                onClick={() => !tab.disabled && setActiveTab(tab.id)}
                disabled={tab.disabled}
                className={cn(
                  "flex items-center gap-2 px-4
formatDate function · typescript · L38-L43 (6 LOC)
app/article/[id]/page.tsx
function formatDate(dateStr: string): string {
  if (!dateStr) return ""
  const d = new Date(dateStr)
  if (isNaN(d.getTime())) return dateStr
  return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
}
mapDbContent function · typescript · L45-L64 (20 LOC)
app/article/[id]/page.tsx
function mapDbContent(c: Record<string, unknown>): Content {
  return {
    id: c.id as string,
    type: c.type as ContentType,
    title: c.title as string,
    description: (c.description as string) || "",
    body: (c.body as string) || "",
    status: c.status as ContentStatus,
    publishDate: formatDate((c.publish_date as string) || ""),
    author: c.author_name as string,
    authorBio: (c.author_bio as string) || undefined,
    thumbnail: (c.thumbnail_url as string) || undefined,
    views: (c.views as number) || 0,
    likes: (c.likes as number) || 0,
    premium: (c.premium as boolean) || false,
    requiredRank: (c.required_rank as MemberRank) || "all",
    url: (c.url as string) || undefined,
    duration: (c.duration as string) || undefined,
  }
}
GET function · typescript · L5-L76 (72 LOC)
app/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get("code")
  const inviteCode = searchParams.get("invite_code")

  if (code) {
    const cookieStore = await cookies()
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          getAll() {
            return cookieStore.getAll()
          },
          setAll(cookiesToSet) {
            try {
              cookiesToSet.forEach(({ name, value, options }) =>
                cookieStore.set(name, value, options)
              )
            } catch {
              // Server Component context — ignore
            }
          },
        },
      }
    )

    const { data: { session } } = await supabase.auth.exchangeCodeForSession(code)

    if (session?.user) {
      // 招待コードがある場合(OAuth 新規登録)→ 後付けリンク
      if (inviteCode) {
        await supabase.rpc("
GET function · typescript · L5-L43 (39 LOC)
app/auth/reset-callback/route.ts
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get("code")

  if (code) {
    const cookieStore = await cookies()
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          getAll() {
            return cookieStore.getAll()
          },
          setAll(cookiesToSet) {
            try {
              cookiesToSet.forEach(({ name, value, options }) =>
                cookieStore.set(name, value, options)
              )
            } catch {
              // Server Component context — ignore
            }
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return NextResponse.redirect(`${origin}/reset-password`)
    }
  }

  // コード無効またはエラー → ゲートウェイへ
  return NextResponse.redirect(
    `${origin}/?message=${encod
FavoritesPage function · typescript · L34-L192 (159 LOC)
app/favorites/page.tsx
export default function FavoritesPage() {
  const [mounted, setMounted] = useState(false)
  const [activeTab, setActiveTab] = useState<TabKey>("likes")
  const [likedItems, setLikedItems] = useState<ContentItem[]>([])
  const [bookmarkedItems, setBookmarkedItems] = useState<ContentItem[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setMounted(true)
    Promise.all([getLikedContents(), getBookmarkedContents()]).then(
      ([likeResult, bookmarkResult]) => {
        if (likeResult && "data" in likeResult && likeResult.data) {
          setLikedItems(likeResult.data as ContentItem[])
        }
        if (bookmarkResult && "data" in bookmarkResult && bookmarkResult.data) {
          setBookmarkedItems(bookmarkResult.data as ContentItem[])
        }
        setLoading(false)
      }
    )
  }, [])

  if (!mounted) return null

  const items = activeTab === "likes" ? likedItems : bookmarkedItems

  return (
    <div className="min-h-screen bg-[#F8F9FA]">
   
formatDate function · typescript · L29-L34 (6 LOC)
app/feed/page.tsx
function formatDate(dateStr: string): string {
  if (!dateStr) return ""
  const d = new Date(dateStr)
  if (isNaN(d.getTime())) return dateStr
  return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
}
All rows above produced by Repobility · https://repobility.com
mapDbContent function · typescript · L36-L53 (18 LOC)
app/feed/page.tsx
function mapDbContent(c: Record<string, unknown>): Content {
  return {
    id: c.id as string,
    type: c.type as ContentType,
    title: c.title as string,
    description: (c.description as string) || "",
    body: (c.body as string) || "",
    status: c.status as ContentStatus,
    publishDate: formatDate((c.publish_date as string) || ""),
    author: c.author_name as string,
    thumbnail: c.thumbnail_url as string | undefined,
    views: (c.views as number) || 0,
    premium: (c.premium as boolean) || false,
    requiredRank: (c.required_rank as MemberRank) || "all",
    url: c.url as string | undefined,
    duration: c.duration as string | undefined,
  }
}
FeedPage function · typescript · L55-L452 (398 LOC)
app/feed/page.tsx
export default function FeedPage() {
  const [mounted, setMounted] = useState(false)
  const [items, setItems] = useState<Content[]>([])

  useEffect(() => {
    setMounted(true)
    getPublishedContents().then((result) => {
      if ("data" in result && result.data) {
        setItems(result.data.map(mapDbContent))
      }
    })
  }, [])

  const [activeTab, setActiveTab] = useState<TabKey>("articles")

  const recommended = items.slice(0, 3)
  const articles = items.filter(c => c.type === "article")
  const videos = items.filter(c => c.type === "video")
  const externalLinks = items.filter(c => c.type === "external")

  const tabs = [
    { key: "articles", label: "記事", icon: FileText, count: articles.length },
    { key: "videos", label: "動画", icon: Video, count: videos.length },
    { key: "external", label: "外部リンク", icon: Link2, count: externalLinks.length },
  ] as const

  return (
    <div className="min-h-screen bg-[#F8F9FA]">
      <AppHeader />

      <main className="mx-au
RootLayout function · typescript · L18-L33 (16 LOC)
app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="ja">
      <body className={`${_notoSans.variable} ${_notoSerif.variable} font-sans antialiased text-[#1B3022]`}>
        <AuthProvider>
          {children}
        </AuthProvider>
        <Toaster />
      </body>
    </html>
  )
}
page 1 / 2next ›