Function bodies 91 total
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.registgetDashboardStats 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.ggetAdminRewards 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 = rewaupdateAdminReward 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: `アップロードに失敗しました: ${uploaupdateContentThumbnail 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-4formatDate 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=${encodFavoritesPage 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-auRootLayout 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 ›