From 3a618e384d63c6b939a92ad33081851a76806f8d Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 21 Feb 2026 17:45:08 -0600 Subject: [PATCH] Add full CRUD for all database tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New API endpoints: - /api/meta (GET, POST, DELETE) - Meta key-value store CRUD - /api/users (GET, PATCH, DELETE) - User admin CRUD New CLI commands: - user get/update/delete - Admin user management - meta list/get/set/delete - Meta key-value management Tables now with full CRUD: ✅ tasks - Full CRUD ✅ projects - Full CRUD ✅ sprints - Full CRUD ✅ users - Full CRUD (list, get, create via register, update, delete) ✅ meta - Full CRUD (list, get, set/create/update, delete) Internal tables (no public CRUD needed): - sessions - Managed by auth system - password_reset_tokens - Managed by forgot/reset password flows --- scripts/gantt.sh | 127 +++++++++++++++++++++++++++++++++++++ src/app/api/meta/route.ts | 101 +++++++++++++++++++++++++++++ src/app/api/users/route.ts | 118 ++++++++++++++++++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 src/app/api/meta/route.ts create mode 100644 src/app/api/users/route.ts diff --git a/scripts/gantt.sh b/scripts/gantt.sh index 8846f6a..7f4cf40 100755 --- a/scripts/gantt.sh +++ b/scripts/gantt.sh @@ -502,6 +502,98 @@ cmd_auth_users() { api_call GET "/auth/users" } +#=================== +# USER ADMIN OPERATIONS +#=================== + +cmd_user_get() { + local user_id="$1" + + if [ -z "$user_id" ]; then + log_error "Usage: user get " + exit 1 + fi + + log_info "Fetching user $user_id..." + api_call GET "/users?id=$user_id" | jq '.user' +} + +cmd_user_update() { + local user_id="$1" + local field="$2" + local value="$3" + + if [ -z "$user_id" ] || [ -z "$field" ] || [ -z "$value" ]; then + log_error "Usage: user update " + echo "Fields: name, email, avatar_url" + exit 1 + fi + + log_info "Updating user $user_id: $field = $value" + local data + data=$(jq -n --arg id "$user_id" --arg field "$field" --arg value "$value" \ + '{id: $id, ($field): $value}') + api_call PATCH "/users" "$data" +} + +cmd_user_delete() { + local user_id="$1" + + if [ -z "$user_id" ]; then + log_error "Usage: user delete " + exit 1 + fi + + log_warn "Deleting user $user_id..." + api_call DELETE "/users" "{\"id\": \"$user_id\"}" +} + +#=================== +# META OPERATIONS +#=================== + +cmd_meta_list() { + log_info "Fetching meta entries..." + api_call GET "/meta" | jq '.meta' +} + +cmd_meta_get() { + local key="$1" + + if [ -z "$key" ]; then + log_error "Usage: meta get " + exit 1 + fi + + log_info "Fetching meta key: $key" + api_call GET "/meta?key=$key" | jq '.meta | .[0]' +} + +cmd_meta_set() { + local key="$1" + local value="$2" + + if [ -z "$key" ] || [ -z "$value" ]; then + log_error "Usage: meta set " + exit 1 + fi + + log_info "Setting meta $key = $value" + api_call POST "/meta" "{\"key\": \"$key\", \"value\": \"$value\"}" +} + +cmd_meta_delete() { + local key="$1" + + if [ -z "$key" ]; then + log_error "Usage: meta delete " + exit 1 + fi + + log_warn "Deleting meta key: $key" + api_call DELETE "/meta" "{\"key\": \"$key\"}" +} + #=================== # DEBUG OPERATIONS #=================== @@ -565,6 +657,19 @@ AUTH COMMANDS: auth account Update account (name, email) auth users List all users +USER ADMIN COMMANDS: + user list List all users (same as auth users) + user get Get specific user + user update Update user field + Fields: name, email, avatar_url + user delete Delete a user + +META COMMANDS: + meta list List all meta entries + meta get Get specific meta value + meta set Set meta key-value pair + meta delete Delete meta entry + OTHER COMMANDS: debug Call debug endpoint help Show this help message @@ -654,6 +759,28 @@ main() { *) log_error "Unknown auth command: $subcmd"; show_help; exit 1 ;; esac ;; + user|users) + local subcmd="${1:-list}" + shift || true + case "$subcmd" in + list|ls) cmd_auth_users "$@" ;; # Reuse auth users list + get|show) cmd_user_get "$@" ;; + update|set|edit) cmd_user_update "$@" ;; + delete|rm|remove) cmd_user_delete "$@" ;; + *) log_error "Unknown user command: $subcmd"; show_help; exit 1 ;; + esac + ;; + meta) + local subcmd="${1:-list}" + shift || true + case "$subcmd" in + list|ls) cmd_meta_list "$@" ;; + get|show) cmd_meta_get "$@" ;; + set|create|update) cmd_meta_set "$@" ;; + delete|rm|remove) cmd_meta_delete "$@" ;; + *) log_error "Unknown meta command: $subcmd"; show_help; exit 1 ;; + esac + ;; debug) cmd_debug "$@" ;; help|--help|-h) show_help ;; *) log_error "Unknown command: $cmd"; show_help; exit 1 ;; diff --git a/src/app/api/meta/route.ts b/src/app/api/meta/route.ts new file mode 100644 index 0000000..e79fb9f --- /dev/null +++ b/src/app/api/meta/route.ts @@ -0,0 +1,101 @@ +import { NextResponse } from "next/server"; +import { getServiceSupabase } from "@/lib/supabase/client"; +import { getAuthenticatedUser } from "@/lib/server/auth"; + +export const runtime = "nodejs"; + +// GET - fetch all meta entries or specific key +export async function GET(request: Request) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const key = searchParams.get("key"); + + const supabase = getServiceSupabase(); + + let query = supabase.from("meta").select("*"); + if (key) { + query = query.eq("key", key); + } + + const { data, error } = await query.order("key"); + + if (error) throw error; + + return NextResponse.json({ meta: data || [] }); + } catch (error) { + console.error(">>> API GET /meta error:", error); + return NextResponse.json({ error: "Failed to fetch meta" }, { status: 500 }); + } +} + +// POST - create or update a meta entry +export async function POST(request: Request) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { key, value } = body; + + if (!key || typeof key !== "string") { + return NextResponse.json({ error: "Missing key" }, { status: 400 }); + } + + if (value === undefined) { + return NextResponse.json({ error: "Missing value" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const now = new Date().toISOString(); + + const { data, error } = await supabase + .from("meta") + .upsert({ + key, + value: String(value), + updated_at: now, + }) + .select() + .single(); + + if (error) throw error; + + return NextResponse.json({ success: true, meta: data }); + } catch (error) { + console.error(">>> API POST /meta error:", error); + return NextResponse.json({ error: "Failed to save meta" }, { status: 500 }); + } +} + +// DELETE - delete a meta entry +export async function DELETE(request: Request) { + try { + const user = await getAuthenticatedUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { key } = await request.json(); + + if (!key) { + return NextResponse.json({ error: "Missing key" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const { error } = await supabase.from("meta").delete().eq("key", key); + + if (error) throw error; + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(">>> API DELETE /meta error:", error); + return NextResponse.json({ error: "Failed to delete meta" }, { status: 500 }); + } +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 0000000..ef7cd66 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,118 @@ +import { NextResponse } from "next/server"; +import { getServiceSupabase } from "@/lib/supabase/client"; +import { getAuthenticatedUser } from "@/lib/server/auth"; + +export const runtime = "nodejs"; + +// GET - fetch single user by ID +export async function GET(request: Request) { + try { + const currentUser = await getAuthenticatedUser(); + if (!currentUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return NextResponse.json({ error: "Missing user id" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const { data, error } = await supabase + .from("users") + .select("id, name, email, avatar_url, created_at") + .eq("id", id) + .single(); + + if (error) { + if (error.code === "PGRST116") { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + throw error; + } + + return NextResponse.json({ user: data }); + } catch (error) { + console.error(">>> API GET /user error:", error); + return NextResponse.json({ error: "Failed to fetch user" }, { status: 500 }); + } +} + +// PATCH - update user fields (admin only) +export async function PATCH(request: Request) { + try { + const currentUser = await getAuthenticatedUser(); + if (!currentUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { id, ...updates } = body; + + if (!id) { + return NextResponse.json({ error: "Missing user id" }, { status: 400 }); + } + + // Only allow certain fields to be updated + const allowedFields = ["name", "email", "avatar_url"]; + const dbUpdates: Record = {}; + + for (const field of allowedFields) { + if (updates[field] !== undefined) { + dbUpdates[field] = updates[field]; + } + } + + if (Object.keys(dbUpdates).length === 0) { + return NextResponse.json({ error: "No valid fields to update" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const { data, error } = await supabase + .from("users") + .update(dbUpdates) + .eq("id", id) + .select("id, name, email, avatar_url, created_at") + .single(); + + if (error) throw error; + + return NextResponse.json({ success: true, user: data }); + } catch (error) { + console.error(">>> API PATCH /user error:", error); + return NextResponse.json({ error: "Failed to update user" }, { status: 500 }); + } +} + +// DELETE - delete a user (admin only) +export async function DELETE(request: Request) { + try { + const currentUser = await getAuthenticatedUser(); + if (!currentUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await request.json(); + + if (!id) { + return NextResponse.json({ error: "Missing user id" }, { status: 400 }); + } + + // Prevent self-deletion + if (id === currentUser.id) { + return NextResponse.json({ error: "Cannot delete yourself" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const { error } = await supabase.from("users").delete().eq("id", id); + + if (error) throw error; + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(">>> API DELETE /user error:", error); + return NextResponse.json({ error: "Failed to delete user" }, { status: 500 }); + } +}