Compare commits
2 Commits
7d40459211
...
f24b230703
| Author | SHA1 | Date | |
|---|---|---|---|
| f24b230703 | |||
| 3a618e384d |
@ -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
|
||||||
|
|||||||
127
scripts/gantt.sh
127
scripts/gantt.sh
@ -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
101
src/app/api/meta/route.ts
Normal 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
118
src/app/api/users/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user