307 lines
7.1 KiB
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,
|
|
})
|
|
}
|