clavitor/clavitor.com/account/api.go

307 lines
7.1 KiB
Go

package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
func randomToken(n int) string {
b := make([]byte, n)
rand.Read(b)
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
}
func jsonResp(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func jsonErr(w http.ResponseWriter, status int, msg string) {
jsonResp(w, status, map[string]string{"error": msg})
}
// Get email from session cookie
func authEmail(r *http.Request) string {
c, err := r.Cookie("v1984_session")
if err != nil {
return ""
}
email, err := sessionGet(c.Value)
if err != nil {
return ""
}
return email
}
func setSessionCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: "v1984_session",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400,
})
}
func clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "v1984_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})
}
// POST /api/auth/email — send login code (stubbed: always 123456)
func apiAuthEmail(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" {
jsonErr(w, 400, "email required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
// Auto-create account if it doesn't exist
exists, _, _ := accountGet(email)
if !exists {
if err := accountCreate(email); err != nil {
log.Printf("account auto-create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("account auto-created: %s", email)
}
// Store code (stub: always 123456)
code := "123456"
if err := loginCodeSet(email, code); err != nil {
log.Printf("login code error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("login code for %s: %s (stub — not emailed)", email, code)
jsonResp(w, 200, map[string]string{"status": "sent"})
}
// POST /api/auth/verify — verify login code
func apiAuthVerify(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
Code string `json:"code"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" || req.Code == "" {
jsonErr(w, 400, "email and code required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
ok, err := loginCodeVerify(email, req.Code)
if err != nil || !ok {
jsonErr(w, 401, "invalid or expired code")
return
}
token, err := sessionCreate(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
setSessionCookie(w, token)
jsonResp(w, 200, map[string]string{"status": "ok"})
}
// POST /api/auth/logout
func apiAuthLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("v1984_session"); err == nil {
sessionDelete(c.Value)
}
clearSessionCookie(w)
jsonResp(w, 200, map[string]string{"status": "ok"})
}
// POST /api/checkout — create account (stub: no Stripe yet)
func apiCheckout(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" {
jsonErr(w, 400, "email required")
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
// Create account (idempotent)
if err := accountCreate(email); err != nil {
log.Printf("account create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
// Auto-login after checkout (stub — real flow goes through Stripe first)
token, err := sessionCreate(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
setSessionCookie(w, token)
log.Printf("account created (stub): %s", email)
// In production: return Stripe checkout URL
// For now: redirect to regions page
jsonResp(w, 200, map[string]string{"url": basePath + "/regions"})
}
// GET /api/vaults — list vaults for authenticated user
func apiVaults(w http.ResponseWriter, r *http.Request) {
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
vaults, err := vaultList(email)
if err != nil {
jsonErr(w, 500, "internal error")
return
}
if vaults == nil {
vaults = []Vault{}
}
count, _ := vaultCount(email)
jsonResp(w, 200, map[string]any{
"email": email,
"vaults": vaults,
"count": count,
"max": 1, // consumer: 1 vault
})
}
// POST /api/vault/create
func apiVaultCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
var req struct {
VaultID string `json:"vault_id"`
Region string `json:"region"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Region == "" {
jsonErr(w, 400, "region required")
return
}
// Check capacity
count, _ := vaultCount(email)
if count >= 1 {
jsonErr(w, 409, "vault limit reached")
return
}
// Generate vault ID if not provided (normally comes from PRF on the POP)
vaultID := req.VaultID
if vaultID == "" {
vaultID = randomToken(4)[:6] // 6 chars, stub
}
if err := vaultCreate(vaultID, email, req.Region); err != nil {
log.Printf("vault create error: %v", err)
jsonErr(w, 500, "internal error")
return
}
log.Printf("vault created: %s for %s in %s", vaultID, email, req.Region)
jsonResp(w, 201, map[string]string{
"vault_id": vaultID,
"region": req.Region,
"expires_at": time.Now().AddDate(1, 0, 0).UTC().Format(time.RFC3339),
})
}
// POST /api/vault/{id}/delete
func apiVaultDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
jsonErr(w, 405, "method not allowed")
return
}
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
// Extract vault ID from path: /api/vault/{id}/delete
parts := strings.Split(r.URL.Path, "/")
var vaultID string
for i, p := range parts {
if p == "vault" && i+1 < len(parts) {
vaultID = parts[i+1]
break
}
}
if vaultID == "" {
jsonErr(w, 400, "vault_id required")
return
}
if err := vaultDelete(vaultID, email); err != nil {
jsonErr(w, 404, "vault not found")
return
}
log.Printf("vault deleted: %s for %s", vaultID, email)
jsonResp(w, 200, map[string]string{"status": "deleted"})
}
// GET /api/account — account info
func apiAccount(w http.ResponseWriter, r *http.Request) {
email := authEmail(r)
if email == "" {
jsonErr(w, 401, "not authenticated")
return
}
exists, stripeID, _ := accountGet(email)
if !exists {
jsonErr(w, 404, "account not found")
return
}
count, _ := vaultCount(email)
jsonResp(w, 200, map[string]any{
"email": email,
"stripe_id": stripeID,
"vaults": count,
"max": 1,
})
}