inou/api/api_v1.go

1011 lines
25 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"inou/lib"
)
// --- AUTH & HELPERS ---
func v1Auth(r *http.Request) (string, error) {
var tokenStr string
// Try Authorization header first
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
tokenStr = strings.TrimPrefix(auth, "Bearer ")
}
// Fall back to query parameter (for Grok and other tools that can't set headers)
if tokenStr == "" {
tokenStr = r.URL.Query().Get("token")
}
if tokenStr == "" {
return "", nil
}
// Try encrypted token first (short-lived, from /api/v1/token)
token, err := lib.TokenParse(tokenStr)
if err == nil {
return token.DossierID, nil
}
// Try session token (long-lived API token from dashboard)
if d := lib.DossierGetBySessionToken(tokenStr); d != nil {
return d.DossierID, nil
}
return "", nil // No valid auth
}
func v1AuthRequired(w http.ResponseWriter, r *http.Request) (string, bool) {
dossierID, err := v1Auth(r)
if err != nil {
v1Error(w, err.Error(), http.StatusUnauthorized)
return "", false
}
if dossierID == "" {
v1Error(w, "Authorization required", http.StatusUnauthorized)
return "", false
}
return dossierID, true
}
func v1CanAccess(authID, targetID string) bool {
if authID == targetID {
return true
}
records, _ := lib.AccessList(&lib.AccessFilter{
AccessorID: authID,
TargetID: targetID,
Status: intPtr(1),
})
return len(records) > 0
}
func v1CanWrite(authID, targetID string) bool {
if authID == targetID {
return true
}
records, _ := lib.AccessList(&lib.AccessFilter{
AccessorID: authID,
TargetID: targetID,
Status: intPtr(1),
})
for _, r := range records {
if r.CanEdit {
return true
}
}
return false
}
func intPtr(i int) *int { return &i }
func v1Error(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
func v1JSON(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
// --- TOKEN ---
// v1Token issues a short-lived access token
// POST /api/v1/token - uses dossier ID from Authorization header
func v1Token(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
v1Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
// Verify dossier exists (nil ctx - v1 API has own auth)
d, err := lib.DossierGet(nil, authID)
if err != nil || d == nil {
v1Error(w, "Invalid dossier", http.StatusUnauthorized)
return
}
// Create 4-hour token
token := lib.TokenCreate(authID, 4*time.Hour)
expiresIn := 4 * 60 * 60 // seconds
v1JSON(w, map[string]any{
"token": token,
"expires_in": expiresIn,
"dossier_id": authID,
})
}
// --- DOSSIERS ---
func v1Dossiers(w http.ResponseWriter, r *http.Request) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
// Get dossiers this user can access (deduplicated)
access, _ := lib.AccessList(&lib.AccessFilter{AccessorID: authID, Status: intPtr(1)})
seen := map[string]bool{authID: true}
targetIDs := []string{authID}
for _, a := range access {
if !seen[a.TargetDossierID] {
seen[a.TargetDossierID] = true
targetIDs = append(targetIDs, a.TargetDossierID)
}
}
sexStr := map[int]string{1: "male", 2: "female", 9: "other"}
var result []map[string]any
for _, tid := range targetIDs {
d, err := lib.DossierGet(nil, tid) // nil ctx - v1 API has own auth
if err != nil {
continue
}
// Get available categories for this dossier
categories := getDossierCategories(tid)
result = append(result, map[string]any{
"id": d.DossierID,
"name": d.Name,
"date_of_birth": d.DateOfBirth,
"sex": sexStr[d.Sex],
"categories": categories,
"self": tid == authID,
})
}
v1JSON(w, result)
}
func getDossierCategories(dossierID string) []string {
// Query distinct categories for this dossier
var counts []struct {
Category int `db:"category"`
Count int `db:"cnt"`
}
lib.Query("SELECT category, COUNT(*) as cnt FROM entries WHERE dossier_id = ? AND category > 0 GROUP BY category", []any{dossierID}, &counts)
categories := []string{} // Empty slice, not nil
for _, c := range counts {
if c.Count > 0 {
// Use lib.CategoryName to get proper name for all categories
name := lib.CategoryName(c.Category)
if name != "unknown" {
categories = append(categories, name)
}
}
}
return categories
}
func v1Dossier(w http.ResponseWriter, r *http.Request, id string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, id) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
d, err := lib.DossierGet(nil, id) // nil ctx - v1 API has own auth
if err != nil {
v1Error(w, "Dossier not found", http.StatusNotFound)
return
}
v1JSON(w, map[string]any{
"id": d.DossierID,
"name": d.Name,
})
}
// --- ENTRIES ---
func v1Entries(w http.ResponseWriter, r *http.Request, dossierID string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
q := r.URL.Query()
parentID := q.Get("parent")
category := 0
if cat := q.Get("category"); cat != "" {
// Support both "genome" and "category004" formats
if strings.HasPrefix(cat, "category") {
// Parse "category004" -> 4
fmt.Sscanf(cat, "category%d", &category)
} else {
// Parse name like "genome", "upload", etc.
category = lib.CategoryFromString[cat]
}
}
filter := &lib.EntryFilter{
DossierID: dossierID,
Type: q.Get("type"),
SearchKey: q.Get("search_key"),
}
if from := q.Get("from"); from != "" {
filter.FromDate, _ = strconv.ParseInt(from, 10, 64)
}
if to := q.Get("to"); to != "" {
filter.ToDate, _ = strconv.ParseInt(to, 10, 64)
}
if limit := q.Get("limit"); limit != "" {
filter.Limit, _ = strconv.Atoi(limit)
}
entries, err := lib.EntryList(lib.SystemAccessorID, parentID, category, filter) // nil ctx - v1 API has own auth
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
var result []map[string]any
for _, e := range entries {
entry := map[string]any{
"id": e.EntryID,
"parent_id": e.ParentID,
"category": lib.CategoryKey(e.Category),
"type": e.Type,
"summary": e.Summary,
"ordinal": e.Ordinal,
"timestamp": e.Timestamp,
}
result = append(result, entry)
}
// If listing children of a series, include contact_sheet_url
if parentID != "" {
parent, err := lib.EntryGet(nil, parentID) // nil ctx - v1 API has own auth
if err == nil && parent.Type == "series" {
v1JSON(w, map[string]any{
"slices": result,
"contact_sheet_url": "https://inou.com/contact-sheet.webp/" + parentID + "?token=" + dossierID,
})
return
}
}
v1JSON(w, result)
}
func v1Entry(w http.ResponseWriter, r *http.Request, dossierID, entryID string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
e, err := lib.EntryGet(nil, entryID) // nil ctx - v1 API has own auth
if err != nil || e.DossierID != dossierID {
v1Error(w, "Entry not found", http.StatusNotFound)
return
}
detail := r.URL.Query().Get("detail")
entry := map[string]any{
"id": e.EntryID,
"parent_id": e.ParentID,
"category": lib.CategoryKey(e.Category),
"type": e.Type,
"summary": e.Summary,
"ordinal": e.Ordinal,
"timestamp": e.Timestamp,
"tags": e.Tags,
}
if detail == "full" && e.Data != "" {
var data map[string]any
json.Unmarshal([]byte(e.Data), &data)
entry["data"] = data
}
// Get children
children, _ := lib.EntryList(lib.SystemAccessorID, entryID, 0, &lib.EntryFilter{
DossierID: e.DossierID,
}) // nil ctx - v1 API has own auth
if len(children) > 0 {
var childList []map[string]any
for _, c := range children {
child := map[string]any{
"id": c.EntryID,
"type": c.Type,
"summary": c.Summary,
"ordinal": c.Ordinal,
}
childList = append(childList, child)
}
entry["children"] = childList
}
v1JSON(w, entry)
}
// --- ACCESS ---
func v1Access(w http.ResponseWriter, r *http.Request, dossierID string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
records, err := lib.AccessList(&lib.AccessFilter{TargetID: dossierID, Status: intPtr(1)})
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
var result []map[string]any
for _, a := range records {
result = append(result, map[string]any{
"accessor_id": a.AccessorDossierID,
"relation": a.Relation,
"is_care_receiver": a.IsCareReceiver,
"can_edit": a.CanEdit,
})
}
v1JSON(w, result)
}
// --- AUDIT ---
func v1Audit(w http.ResponseWriter, r *http.Request, dossierID string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
q := r.URL.Query()
filter := &lib.AuditFilter{TargetID: dossierID}
if action := q.Get("action"); action != "" {
filter.Action = action
}
if from := q.Get("from"); from != "" {
filter.FromDate, _ = strconv.ParseInt(from, 10, 64)
}
if to := q.Get("to"); to != "" {
filter.ToDate, _ = strconv.ParseInt(to, 10, 64)
}
if limit := q.Get("limit"); limit != "" {
filter.Limit, _ = strconv.Atoi(limit)
}
entries, err := lib.AuditList(filter)
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
var result []map[string]any
for _, a := range entries {
result = append(result, map[string]any{
"id": a.AuditID,
"action": a.Action,
"actor_id": a.Actor1ID,
"target_id": a.TargetID,
"timestamp": a.Timestamp,
"details": a.Details,
})
}
v1JSON(w, result)
}
// --- PARSE (LLM triage + extraction) ---
type ParseRequest struct {
Input string `json:"input"`
}
type ParseEntryResponse struct {
ID string `json:"id"`
Category string `json:"category"`
Type string `json:"type"`
Value string `json:"value"`
Timestamp int64 `json:"timestamp"`
}
type ParseTrackerResponse struct {
ID string `json:"id"`
Category string `json:"category"`
Type string `json:"type"`
Question string `json:"question"`
InputType string `json:"input_type"`
InputConfig any `json:"input_config,omitempty"`
NextAsk int64 `json:"next_ask"`
}
type ParseResponse struct {
Category string `json:"category"`
Type string `json:"type"`
Entries []ParseEntryResponse `json:"entries"`
Tracker *ParseTrackerResponse `json:"prompt,omitempty"`
Error string `json:"error,omitempty"`
}
// POST /api/v1/dossiers/{id}/parse
// Parses free-form health input, creates entries and prompts, returns what happened
func v1Parse(w http.ResponseWriter, r *http.Request, dossierID string) {
if r.Method != "POST" {
v1Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
var req ParseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
v1Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Input == "" {
v1Error(w, "input is required", http.StatusBadRequest)
return
}
// Run triage + extraction
generated, err := callLLMForTracker(req.Input, dossierID)
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
// If triage returned an error (not_health_related, no_medical_advice, etc.)
if generated.Error != "" {
v1JSON(w, ParseResponse{
Error: generated.Error,
})
return
}
resp := ParseResponse{
Category: generated.Category,
Type: generated.Type,
Entries: []ParseEntryResponse{},
}
now := time.Now()
// Create entries from extracted data
for _, entryData := range generated.Entries {
entryJSON, _ := json.Marshal(entryData.Data)
entry := &lib.Entry{
DossierID: dossierID,
Category: lib.CategoryFromString[generated.Category],
Type: generated.Type,
Value: entryData.Value,
Data: string(entryJSON),
Timestamp: now.Unix(),
}
if err := lib.EntryAdd(entry); err != nil {
continue // Skip failed entries but don't fail the whole request
}
resp.Entries = append(resp.Entries, ParseEntryResponse{
ID: entry.EntryID,
Category: generated.Category,
Type: generated.Type,
Value: entryData.Value,
Timestamp: entry.Timestamp,
})
}
// Create tracker if there's a follow-up question with schedule
if generated.Question != "" && len(generated.Schedule) > 0 {
nextAsk := calculateNextAskFromSchedule(generated.Schedule, now)
scheduleJSON, _ := json.Marshal(generated.Schedule)
inputConfigJSON, _ := json.Marshal(generated.InputConfig)
tracker := &lib.Tracker{
DossierID: dossierID,
Category: generated.Category,
Type: generated.Type,
Question: generated.Question,
Schedule: string(scheduleJSON),
InputType: generated.InputType,
InputConfig: string(inputConfigJSON),
SourceInput: req.Input,
NextAsk: nextAsk,
Active: true,
}
// Pre-fill last response from initial extracted data
if len(generated.Entries) > 0 && generated.Entries[0].Data != nil {
initialData, _ := json.Marshal(generated.Entries[0].Data)
tracker.LastResponse = string(initialData)
tracker.LastResponseRaw = generated.Entries[0].Value
tracker.LastResponseAt = now.Unix()
}
if err := lib.TrackerAdd(tracker); err == nil {
var inputConfig any
json.Unmarshal([]byte(tracker.InputConfig), &inputConfig)
resp.Tracker = &ParseTrackerResponse{
ID: tracker.TrackerID,
Category: tracker.Category,
Type: tracker.Type,
Question: tracker.Question,
InputType: tracker.InputType,
InputConfig: inputConfig,
NextAsk: tracker.NextAsk,
}
}
}
v1JSON(w, resp)
}
// --- PROMPTS ---
func v1Prompts(w http.ResponseWriter, r *http.Request, dossierID string) {
authID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(authID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
q := r.URL.Query()
filter := &lib.TrackerFilter{DossierID: dossierID}
if cat := q.Get("category"); cat != "" {
filter.Category = cat
}
if typ := q.Get("type"); typ != "" {
filter.Type = typ
}
if q.Get("active") == "true" {
filter.ActiveOnly = true
}
trackers, err := lib.TrackerList(filter)
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
var result []map[string]any
for _, p := range trackers {
result = append(result, map[string]any{
"id": p.TrackerID,
"category": p.Category,
"type": p.Type,
"question": p.Question,
"active": p.Active,
"dismissed": p.Dismissed,
"time_of_day": p.TimeOfDay,
})
}
v1JSON(w, result)
}
// --- IMAGE ---
func v1Image(w http.ResponseWriter, r *http.Request, id string) {
// Image endpoint accepts token as query param for browser compatibility
tokenStr := r.URL.Query().Get("token")
if tokenStr == "" {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
tokenStr = strings.TrimPrefix(auth, "Bearer ")
}
}
if tokenStr == "" {
v1Error(w, "Authorization required", http.StatusUnauthorized)
return
}
// Try encrypted token first, fallback to raw dossier ID
var authDossierID string
token, err := lib.TokenParse(tokenStr)
if err == nil {
authDossierID = token.DossierID
} else if len(tokenStr) == 16 {
authDossierID = tokenStr
} else {
v1Error(w, err.Error(), http.StatusUnauthorized)
return
}
// Load entry to verify access (nil ctx - v1 API has own auth)
e, err := lib.EntryGet(nil, id)
if err != nil {
v1Error(w, "Image not found", http.StatusNotFound)
return
}
if !v1CanAccess(authDossierID, e.DossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
// Parse window/level options
q := r.URL.Query()
opts := &lib.ImageOpts{}
if wc := q.Get("wc"); wc != "" {
opts.WC, _ = strconv.ParseFloat(wc, 64)
}
if ww := q.Get("ww"); ww != "" {
opts.WW, _ = strconv.ParseFloat(ww, 64)
}
data, err := lib.ImageGet(nil, id, opts) // nil ctx - v1 API has own auth
if err != nil {
v1Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/png")
w.Write(data)
}
// --- CATEGORIES ---
func v1Categories(w http.ResponseWriter, r *http.Request) {
authID, _ := v1Auth(r)
lang := lib.DossierLanguage(authID)
cats := lib.Categories()
var result []map[string]any
for _, c := range cats {
result = append(result, map[string]any{
"id": c.ID,
"key": c.Name,
"name": lib.CategoryTranslate(c.ID, lang),
"types": c.Types,
})
}
v1JSON(w, result)
}
func v1CategoryTypes(w http.ResponseWriter, r *http.Request, category string) {
types, ok := lib.CategoryTypes[category]
if !ok {
v1Error(w, "Category not found", http.StatusNotFound)
return
}
v1JSON(w, types)
}
// --- HEALTH ---
type HealthResponse struct {
Status string `json:"status"`
Time int64 `json:"time"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
}
func v1Health(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
v1Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
checks := make(map[string]string)
allOK := true
if err := lib.DBPing(); err != nil {
checks["db"] = "error: " + err.Error()
allOK = false
} else {
checks["db"] = "ok"
}
status := "ok"
httpStatus := http.StatusOK
if !allOK {
status = "degraded"
httpStatus = http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache, no-store")
w.WriteHeader(httpStatus)
json.NewEncoder(w).Encode(HealthResponse{
Status: status,
Time: time.Now().Unix(),
Version: Version,
Checks: checks,
})
}
// --- ROUTER ---
// v1ListJournals handles GET /dossiers/{id}/journal
func v1ListJournals(w http.ResponseWriter, r *http.Request, dossierID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(actorID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
// Parse query parameters
days := 30 // default
if daysStr := r.URL.Query().Get("days"); daysStr != "" {
if d, err := strconv.Atoi(daysStr); err == nil {
days = d
}
}
var status *int
if statusStr := r.URL.Query().Get("status"); statusStr != "" {
if s, err := strconv.Atoi(statusStr); err == nil {
status = &s
}
}
journalType := r.URL.Query().Get("type")
// List journals
journals, err := lib.ListJournals(lib.ListJournalsInput{
DossierID: dossierID,
Days: days,
Status: status,
Type: journalType,
})
if err != nil {
v1Error(w, fmt.Sprintf("Failed to list journals: %v", err), http.StatusInternalServerError)
return
}
v1JSON(w, map[string]interface{}{"journals": journals})
}
// v1GetJournal handles GET /dossiers/{id}/journal/{entry_id}
func v1GetJournal(w http.ResponseWriter, r *http.Request, dossierID, entryID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(actorID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
journal, err := lib.GetJournal(dossierID, entryID)
if err != nil {
if strings.Contains(err.Error(), "access denied") {
v1Error(w, "Access denied", http.StatusForbidden)
} else if strings.Contains(err.Error(), "not found") {
v1Error(w, "Journal entry not found", http.StatusNotFound)
} else {
v1Error(w, fmt.Sprintf("Failed to get journal: %v", err), http.StatusInternalServerError)
}
return
}
v1JSON(w, journal)
}
// v1CreateJournal handles POST /dossiers/{id}/journal
func v1CreateJournal(w http.ResponseWriter, r *http.Request, dossierID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanWrite(actorID, dossierID) {
v1Error(w, "Write access denied", http.StatusForbidden)
return
}
// Parse request body
var req struct {
Type string `json:"type"`
Title string `json:"title"`
Summary string `json:"summary"`
Content string `json:"content"`
Tags []string `json:"tags"`
Status int `json:"status"`
RelatedEntries []string `json:"related_entries"`
Source string `json:"source"`
Reasoning string `json:"reasoning"`
Metadata map[string]string `json:"metadata"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
v1Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Create journal
entryID, err := lib.CreateJournal(lib.CreateJournalInput{
DossierID: dossierID,
Type: req.Type,
Title: req.Title,
Summary: req.Summary,
Content: req.Content,
Tags: req.Tags,
Status: req.Status,
RelatedEntries: req.RelatedEntries,
Source: req.Source,
Reasoning: req.Reasoning,
Metadata: req.Metadata,
})
if err != nil {
v1Error(w, fmt.Sprintf("Failed to create journal: %v", err), http.StatusInternalServerError)
return
}
// Return created entry
journal, err := lib.GetJournal(dossierID, entryID)
if err != nil {
v1Error(w, "Created but failed to retrieve", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
v1JSON(w, journal)
}
// v1UpdateJournal handles PATCH /dossiers/{id}/journal/{entry_id}
func v1UpdateJournal(w http.ResponseWriter, r *http.Request, dossierID, entryID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanWrite(actorID, dossierID) {
v1Error(w, "Write access denied", http.StatusForbidden)
return
}
// Parse request body
var req struct {
Status *int `json:"status"`
AppendNote string `json:"append_note"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
v1Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Update journal
err := lib.UpdateJournalStatus(lib.UpdateJournalStatusInput{
DossierID: dossierID,
EntryID: entryID,
Status: req.Status,
AppendNote: req.AppendNote,
})
if err != nil {
if strings.Contains(err.Error(), "access denied") {
v1Error(w, "Access denied", http.StatusForbidden)
} else {
v1Error(w, fmt.Sprintf("Failed to update journal: %v", err), http.StatusInternalServerError)
}
return
}
v1JSON(w, map[string]interface{}{"updated": time.Now().Format(time.RFC3339)})
}
func v1Router(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1")
parts := strings.Split(strings.Trim(path, "/"), "/")
switch {
// GET /health (public, no auth)
case len(parts) == 1 && parts[0] == "health" && r.Method == "GET":
v1Health(w, r)
// POST /token
case len(parts) == 1 && parts[0] == "token" && r.Method == "POST":
v1Token(w, r)
// GET /dossiers
case len(parts) == 1 && parts[0] == "dossiers" && r.Method == "GET":
v1Dossiers(w, r)
// GET /dossiers/{id}
case len(parts) == 2 && parts[0] == "dossiers" && r.Method == "GET":
v1Dossier(w, r, parts[1])
// GET /dossiers/{id}/entries
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "entries" && r.Method == "GET":
v1Entries(w, r, parts[1])
// GET /dossiers/{id}/entries/{entry_id}
case len(parts) == 4 && parts[0] == "dossiers" && parts[2] == "entries" && r.Method == "GET":
v1Entry(w, r, parts[1], parts[3])
// GET /dossiers/{id}/access
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "access" && r.Method == "GET":
v1Access(w, r, parts[1])
// GET /dossiers/{id}/audit
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "audit" && r.Method == "GET":
v1Audit(w, r, parts[1])
// GET /dossiers/{id}/prompts
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "trackers" && r.Method == "GET":
v1Prompts(w, r, parts[1])
// GET /dossiers/{id}/journal
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "GET":
v1ListJournals(w, r, parts[1])
// GET /dossiers/{id}/journal/{entry_id}
case len(parts) == 4 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "GET":
v1GetJournal(w, r, parts[1], parts[3])
// POST /dossiers/{id}/journal
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "POST":
v1CreateJournal(w, r, parts[1])
// PATCH /dossiers/{id}/journal/{entry_id}
case len(parts) == 4 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "PATCH":
v1UpdateJournal(w, r, parts[1], parts[3])
// POST /dossiers/{id}/parse
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "parse" && r.Method == "POST":
v1Parse(w, r, parts[1])
// GET /images/{id}
case len(parts) == 2 && parts[0] == "images" && r.Method == "GET":
v1Image(w, r, parts[1])
// GET /categories
case len(parts) == 1 && parts[0] == "categories" && r.Method == "GET":
v1Categories(w, r)
// GET /categories/{name}/types
case len(parts) == 3 && parts[0] == "categories" && parts[2] == "types" && r.Method == "GET":
v1CategoryTypes(w, r, parts[1])
default:
v1Error(w, "Not found", http.StatusNotFound)
}
}
func RegisterV1Routes() {
http.HandleFunc("/api/v1/", v1Router)
}