Compare commits

...

2 Commits

Author SHA1 Message Date
Max
f24b230703 Update README with user and meta CRUD documentation 2026-02-21 17:45:59 -06:00
Max
3a618e384d Add full CRUD for all database tables
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
2026-02-21 17:45:08 -06:00
4 changed files with 406 additions and 0 deletions

View File

@ -113,6 +113,56 @@ A unified CLI that covers all API operations.
# Log out # Log out
./scripts/gantt.sh auth logout ./scripts/gantt.sh auth logout
# Register new account
./scripts/gantt.sh auth register <email> <password> [name]
# Request password reset
./scripts/gantt.sh auth forgot-password <email>
# Reset password with token
./scripts/gantt.sh auth reset-password <token> <new-password>
# Update account
./scripts/gantt.sh auth account <field> <value>
# List all users
./scripts/gantt.sh auth users
```
### User Admin Commands
```bash
# List all users
./scripts/gantt.sh user list
# Get specific user
./scripts/gantt.sh user get <user-id>
# Update user field
./scripts/gantt.sh user update <user-id> <field> <value>
./scripts/gantt.sh user update abc-123 name "New Name"
./scripts/gantt.sh user update abc-123 email "new@example.com"
# Delete user
./scripts/gantt.sh user delete <user-id>
```
### Meta Commands
```bash
# List all meta entries
./scripts/gantt.sh meta list
# Get specific meta value
./scripts/gantt.sh meta get <key>
# Set meta key-value
./scripts/gantt.sh meta set <key> <value>
./scripts/gantt.sh meta set lastBackup "2026-02-22"
# Delete meta entry
./scripts/gantt.sh meta delete <key>
``` ```
### Debug ### Debug
@ -182,6 +232,16 @@ Displays text files in terminal, saves binary files to `/tmp/`.
| Auth reset-password | ✅ | ✅ | ❌ | | Auth reset-password | ✅ | ✅ | ❌ |
| Auth account update | ✅ | ✅ | ❌ | | Auth account update | ✅ | ✅ | ❌ |
| Auth list users | ✅ | ✅ | ❌ | | Auth list users | ✅ | ✅ | ❌ |
| **User Admin** ||||
| Get user | ✅ | ✅ | ❌ |
| Update user | ✅ | ✅ | ❌ |
| Delete user | ✅ | ✅ | ❌ |
| **Meta** ||||
| List meta | ✅ | ✅ | ❌ |
| Get meta | ✅ | ✅ | ❌ |
| Set meta | ✅ | ✅ | ❌ |
| Delete meta | ✅ | ✅ | ❌ |
| **Other** ||||
| View attachments | ✅ | ❌ | ✅ | | View attachments | ✅ | ❌ | ✅ |
### Auditing Coverage ### Auditing Coverage

View File

@ -502,6 +502,98 @@ cmd_auth_users() {
api_call GET "/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 <user-id>"
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 <user-id> <field> <value>"
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 <user-id>"
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 <key>"
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 <key> <value>"
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 <key>"
exit 1
fi
log_warn "Deleting meta key: $key"
api_call DELETE "/meta" "{\"key\": \"$key\"}"
}
#=================== #===================
# DEBUG OPERATIONS # DEBUG OPERATIONS
#=================== #===================
@ -565,6 +657,19 @@ AUTH COMMANDS:
auth account <field> <value> Update account (name, email) auth account <field> <value> Update account (name, email)
auth users List all users auth users List all users
USER ADMIN COMMANDS:
user list List all users (same as auth users)
user get <user-id> Get specific user
user update <id> <field> <val> Update user field
Fields: name, email, avatar_url
user delete <user-id> Delete a user
META COMMANDS:
meta list List all meta entries
meta get <key> Get specific meta value
meta set <key> <value> Set meta key-value pair
meta delete <key> Delete meta entry
OTHER COMMANDS: OTHER COMMANDS:
debug Call debug endpoint debug Call debug endpoint
help Show this help message help Show this help message
@ -654,6 +759,28 @@ main() {
*) log_error "Unknown auth command: $subcmd"; show_help; exit 1 ;; *) log_error "Unknown auth command: $subcmd"; show_help; exit 1 ;;
esac 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 "$@" ;; debug) cmd_debug "$@" ;;
help|--help|-h) show_help ;; help|--help|-h) show_help ;;
*) log_error "Unknown command: $cmd"; show_help; exit 1 ;; *) log_error "Unknown command: $cmd"; show_help; exit 1 ;;

101
src/app/api/meta/route.ts Normal file
View File

@ -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 });
}
}

118
src/app/api/users/route.ts Normal file
View File

@ -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<string, unknown> = {};
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 });
}
}