add owner handle

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-06-14 13:40:51 +02:00
commit f862abe5a4
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
12 changed files with 274 additions and 55 deletions

View file

@ -38,6 +38,7 @@ func (s *ts) do(method, path string, body any) *http.Response {
if err != nil { if err != nil {
s.Fatalf("new request: %v", err) s.Fatalf("new request: %v", err)
} }
req.Header.Set("X-Owner-ID", "1")
if body != nil { if body != nil {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
} }

41
internal/auth/auth.go Normal file
View file

@ -0,0 +1,41 @@
package auth
import (
"context"
"fmt"
"net/http"
"strconv"
)
type contextKey struct{}
func WithOwner(ctx context.Context, ownerID int32) context.Context {
return context.WithValue(ctx, contextKey{}, ownerID)
}
func OwnerFromContext(ctx context.Context) (int32, bool) {
id, ok := ctx.Value(contextKey{}).(int32)
return id, ok
}
// Middleware extrait X-Owner-ID du header et l'injecte dans le contexte.
// Pour le dev, c'est l'ID numérique de l'owner. À remplacer par JWT en prod.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
v := r.Header.Get("X-Owner-ID")
if v == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, `{"error":"missing X-Owner-ID header"}`)
return
}
id, err := strconv.Atoi(v)
if err != nil || id <= 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, `{"error":"invalid X-Owner-ID"}`)
return
}
next.ServeHTTP(w, r.WithContext(WithOwner(r.Context(), int32(id))))
})
}

View file

@ -35,7 +35,11 @@ func (h *AccountHandler) RegisterRoutes(mux *http.ServeMux) {
// --- Comptes maîtres --- // --- Comptes maîtres ---
func (h *AccountHandler) list(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) list(w http.ResponseWriter, r *http.Request) {
accounts, err := h.store.ListAccounts(r.Context()) ownerID, ok := requireOwner(w, r)
if !ok {
return
}
accounts, err := h.store.ListAccounts(r.Context(), ownerID)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list accounts") writeError(w, http.StatusInternalServerError, "failed to list accounts")
return return
@ -44,11 +48,22 @@ func (h *AccountHandler) list(w http.ResponseWriter, r *http.Request) {
} }
func (h *AccountHandler) get(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) get(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.VerifyAccountOwner(r.Context(), id, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get account")
return
}
account, err := h.store.GetAccountWithEnvelopes(r.Context(), id) account, err := h.store.GetAccountWithEnvelopes(r.Context(), id)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found") writeError(w, http.StatusNotFound, "account not found")
@ -62,6 +77,10 @@ func (h *AccountHandler) get(w http.ResponseWriter, r *http.Request) {
} }
func (h *AccountHandler) create(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) create(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
var p store.CreateAccountParams var p store.CreateAccountParams
if err := json.NewDecoder(r.Body).Decode(&p); err != nil { if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body") writeError(w, http.StatusBadRequest, "invalid request body")
@ -71,7 +90,7 @@ func (h *AccountHandler) create(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "nom and type are required") writeError(w, http.StatusBadRequest, "nom and type are required")
return return
} }
account, err := h.store.CreateAccount(r.Context(), p) account, err := h.store.CreateAccount(r.Context(), p, ownerID)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create account") writeError(w, http.StatusInternalServerError, "failed to create account")
return return
@ -80,6 +99,10 @@ func (h *AccountHandler) create(w http.ResponseWriter, r *http.Request) {
} }
func (h *AccountHandler) update(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) update(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
@ -90,7 +113,7 @@ func (h *AccountHandler) update(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid request body") writeError(w, http.StatusBadRequest, "invalid request body")
return return
} }
account, err := h.store.UpdateAccount(r.Context(), id, p) account, err := h.store.UpdateAccount(r.Context(), id, p, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found") writeError(w, http.StatusNotFound, "account not found")
return return
@ -103,12 +126,16 @@ func (h *AccountHandler) update(w http.ResponseWriter, r *http.Request) {
} }
func (h *AccountHandler) delete(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) delete(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.DeleteAccount(r.Context(), id); err != nil { if err := h.store.DeleteAccount(r.Context(), id, ownerID); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete account") writeError(w, http.StatusInternalServerError, "failed to delete account")
return return
} }
@ -118,11 +145,22 @@ func (h *AccountHandler) delete(w http.ResponseWriter, r *http.Request) {
// --- Enveloppes --- // --- Enveloppes ---
func (h *AccountHandler) listEnvelopes(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) listEnvelopes(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.VerifyAccountOwner(r.Context(), id, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to verify account")
return
}
envelopes, err := h.store.ListEnvelopes(r.Context(), id) envelopes, err := h.store.ListEnvelopes(r.Context(), id)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list envelopes") writeError(w, http.StatusInternalServerError, "failed to list envelopes")
@ -132,6 +170,10 @@ func (h *AccountHandler) listEnvelopes(w http.ResponseWriter, r *http.Request) {
} }
func (h *AccountHandler) createEnvelope(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) createEnvelope(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
masterID, err := parseID(r) masterID, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
@ -146,7 +188,7 @@ func (h *AccountHandler) createEnvelope(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusBadRequest, "nom is required") writeError(w, http.StatusBadRequest, "nom is required")
return return
} }
envelope, err := h.store.CreateEnvelope(r.Context(), masterID, p) envelope, err := h.store.CreateEnvelope(r.Context(), masterID, p, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found") writeError(w, http.StatusNotFound, "account not found")
return return
@ -163,6 +205,10 @@ func (h *AccountHandler) createEnvelope(w http.ResponseWriter, r *http.Request)
} }
func (h *AccountHandler) updateEnvelope(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) updateEnvelope(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
@ -173,7 +219,7 @@ func (h *AccountHandler) updateEnvelope(w http.ResponseWriter, r *http.Request)
writeError(w, http.StatusBadRequest, "invalid request body") writeError(w, http.StatusBadRequest, "invalid request body")
return return
} }
envelope, err := h.store.UpdateEnvelope(r.Context(), id, p) envelope, err := h.store.UpdateEnvelope(r.Context(), id, p, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "envelope not found") writeError(w, http.StatusNotFound, "envelope not found")
return return
@ -186,12 +232,16 @@ func (h *AccountHandler) updateEnvelope(w http.ResponseWriter, r *http.Request)
} }
func (h *AccountHandler) deleteEnvelope(w http.ResponseWriter, r *http.Request) { func (h *AccountHandler) deleteEnvelope(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseID(r) id, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.DeleteAccount(r.Context(), id); err != nil { if err := h.store.DeleteAccount(r.Context(), id, ownerID); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete envelope") writeError(w, http.StatusInternalServerError, "failed to delete envelope")
return return
} }

View file

@ -3,6 +3,8 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"git.g3e.fr/H6N/account/internal/auth"
) )
func writeJSON(w http.ResponseWriter, status int, v any) { func writeJSON(w http.ResponseWriter, status int, v any) {
@ -14,3 +16,13 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
func writeError(w http.ResponseWriter, status int, msg string) { func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg}) writeJSON(w, status, map[string]string{"error": msg})
} }
// requireOwner extrait l'owner du contexte ou écrit 401 et retourne false.
func requireOwner(w http.ResponseWriter, r *http.Request) (int32, bool) {
id, ok := auth.OwnerFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "missing owner")
return 0, false
}
return id, true
}

View file

@ -10,6 +10,26 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
func (h *SnapshotHandler) verifyAccountOwner(w http.ResponseWriter, r *http.Request) (int32, int32, bool) {
ownerID, ok := requireOwner(w, r)
if !ok {
return 0, 0, false
}
accountID, err := parseID(r)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid account id")
return 0, 0, false
}
if err := h.engine.Store().VerifyAccountOwner(r.Context(), accountID, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found")
return 0, 0, false
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to verify account")
return 0, 0, false
}
return ownerID, accountID, true
}
type SnapshotHandler struct { type SnapshotHandler struct {
engine *snapshot.Engine engine *snapshot.Engine
} }
@ -37,9 +57,8 @@ func parseDateParam(r *http.Request, key string) (time.Time, error) {
// Liste les snapshots agrégés (valeur totale) d'un compte. // Liste les snapshots agrégés (valeur totale) d'un compte.
// from/to optionnels : sans borne → pas de filtre de ce côté. // from/to optionnels : sans borne → pas de filtre de ce côté.
func (h *SnapshotHandler) listAccountSnapshots(w http.ResponseWriter, r *http.Request) { func (h *SnapshotHandler) listAccountSnapshots(w http.ResponseWriter, r *http.Request) {
accountID, err := parseID(r) _, accountID, ok := h.verifyAccountOwner(w, r)
if err != nil { if !ok {
writeError(w, http.StatusBadRequest, "invalid account id")
return return
} }
from, err := parseDateParam(r, "from") from, err := parseDateParam(r, "from")
@ -67,9 +86,8 @@ func (h *SnapshotHandler) listAccountSnapshots(w http.ResponseWriter, r *http.Re
// GET /accounts/{id}/snapshots/positions?from=YYYY-MM-DD&to=YYYY-MM-DD // GET /accounts/{id}/snapshots/positions?from=YYYY-MM-DD&to=YYYY-MM-DD
// Liste les positions détaillées (par instrument) d'un compte. // Liste les positions détaillées (par instrument) d'un compte.
func (h *SnapshotHandler) listPositionSnapshots(w http.ResponseWriter, r *http.Request) { func (h *SnapshotHandler) listPositionSnapshots(w http.ResponseWriter, r *http.Request) {
accountID, err := parseID(r) _, accountID, ok := h.verifyAccountOwner(w, r)
if err != nil { if !ok {
writeError(w, http.StatusBadRequest, "invalid account id")
return return
} }
from, err := parseDateParam(r, "from") from, err := parseDateParam(r, "from")
@ -98,9 +116,8 @@ func (h *SnapshotHandler) listPositionSnapshots(w http.ResponseWriter, r *http.R
// Recalcule les snapshots du compte depuis sa date d'invalidation jusqu'à aujourd'hui. // Recalcule les snapshots du compte depuis sa date d'invalidation jusqu'à aujourd'hui.
// Retourne 204 si aucune invalidation n'est en attente. // Retourne 204 si aucune invalidation n'est en attente.
func (h *SnapshotHandler) recomputeAccount(w http.ResponseWriter, r *http.Request) { func (h *SnapshotHandler) recomputeAccount(w http.ResponseWriter, r *http.Request) {
accountID, err := parseID(r) _, accountID, ok := h.verifyAccountOwner(w, r)
if err != nil { if !ok {
writeError(w, http.StatusBadRequest, "invalid account id")
return return
} }

View file

@ -28,11 +28,22 @@ func (h *TransactionHandler) RegisterRoutes(mux *http.ServeMux) {
} }
func (h *TransactionHandler) listForAccount(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) listForAccount(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
accountID, err := parseID(r) accountID, err := parseID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid account id") writeError(w, http.StatusBadRequest, "invalid account id")
return return
} }
if err := h.store.VerifyAccountOwner(r.Context(), accountID, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "account not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to verify account")
return
}
f := store.AccountTransactionFilters{ f := store.AccountTransactionFilters{
Validated: parseBoolParam(r, "validated"), Validated: parseBoolParam(r, "validated"),
Pending: parseBoolParam(r, "pending"), Pending: parseBoolParam(r, "pending"),
@ -48,12 +59,16 @@ func (h *TransactionHandler) listForAccount(w http.ResponseWriter, r *http.Reque
} }
func (h *TransactionHandler) get(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) get(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseTxID(r) id, err := parseTxID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
tx, err := h.store.GetTransaction(r.Context(), id) tx, err := h.store.GetTransaction(r.Context(), id, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "transaction not found") writeError(w, http.StatusNotFound, "transaction not found")
return return
@ -66,6 +81,10 @@ func (h *TransactionHandler) get(w http.ResponseWriter, r *http.Request) {
} }
func (h *TransactionHandler) create(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) create(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
var p store.CreateTransactionParams var p store.CreateTransactionParams
if err := json.NewDecoder(r.Body).Decode(&p); err != nil { if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body") writeError(w, http.StatusBadRequest, "invalid request body")
@ -79,6 +98,25 @@ func (h *TransactionHandler) create(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "date is required") writeError(w, http.StatusBadRequest, "date is required")
return return
} }
// Vérifier que les comptes référencés appartiennent à l'owner.
if p.AccountSourceID != nil {
if err := h.store.VerifyAccountOwner(r.Context(), *p.AccountSourceID, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "source account not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to verify source account")
return
}
}
if p.AccountDestID != nil {
if err := h.store.VerifyAccountOwner(r.Context(), *p.AccountDestID, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "dest account not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to verify dest account")
return
}
}
tx, err := h.store.CreateTransaction(r.Context(), p) tx, err := h.store.CreateTransaction(r.Context(), p)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create transaction") writeError(w, http.StatusInternalServerError, "failed to create transaction")
@ -88,6 +126,10 @@ func (h *TransactionHandler) create(w http.ResponseWriter, r *http.Request) {
} }
func (h *TransactionHandler) update(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) update(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseTxID(r) id, err := parseTxID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
@ -102,7 +144,7 @@ func (h *TransactionHandler) update(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "label is required") writeError(w, http.StatusBadRequest, "label is required")
return return
} }
tx, err := h.store.UpdateTransaction(r.Context(), id, p) tx, err := h.store.UpdateTransaction(r.Context(), id, p, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "transaction not found") writeError(w, http.StatusNotFound, "transaction not found")
return return
@ -115,12 +157,19 @@ func (h *TransactionHandler) update(w http.ResponseWriter, r *http.Request) {
} }
func (h *TransactionHandler) delete(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) delete(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseTxID(r) id, err := parseTxID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
return return
} }
if err := h.store.DeleteTransaction(r.Context(), id); err != nil { if err := h.store.DeleteTransaction(r.Context(), id, ownerID); errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "transaction not found")
return
} else if err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete transaction") writeError(w, http.StatusInternalServerError, "failed to delete transaction")
return return
} }
@ -128,6 +177,10 @@ func (h *TransactionHandler) delete(w http.ResponseWriter, r *http.Request) {
} }
func (h *TransactionHandler) validate(w http.ResponseWriter, r *http.Request) { func (h *TransactionHandler) validate(w http.ResponseWriter, r *http.Request) {
ownerID, ok := requireOwner(w, r)
if !ok {
return
}
id, err := parseTxID(r) id, err := parseTxID(r)
if err != nil { if err != nil {
writeError(w, http.StatusBadRequest, "invalid id") writeError(w, http.StatusBadRequest, "invalid id")
@ -140,7 +193,7 @@ func (h *TransactionHandler) validate(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid request body") writeError(w, http.StatusBadRequest, "invalid request body")
return return
} }
tx, err := h.store.SetValidated(r.Context(), id, body.Validated) tx, err := h.store.SetValidated(r.Context(), id, body.Validated, ownerID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "transaction not found") writeError(w, http.StatusNotFound, "transaction not found")
return return

View file

@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"git.g3e.fr/H6N/account/internal/auth"
"git.g3e.fr/H6N/account/internal/config" "git.g3e.fr/H6N/account/internal/config"
"git.g3e.fr/H6N/account/internal/handler" "git.g3e.fr/H6N/account/internal/handler"
"git.g3e.fr/H6N/account/internal/snapshot" "git.g3e.fr/H6N/account/internal/snapshot"
@ -31,7 +32,12 @@ func New(cfg *config.Config, pool *pgxpool.Pool, logger *slog.Logger) *Server {
} }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r) // /health est exempt d'auth. Toutes les autres routes passent par le middleware owner.
if r.URL.Path == "/health" {
s.mux.ServeHTTP(w, r)
return
}
auth.Middleware(s.mux).ServeHTTP(w, r)
} }
func (s *Server) routes() { func (s *Server) routes() {

View file

@ -167,7 +167,7 @@ func (e *Engine) recomputeAccount(ctx context.Context, accountID int32, date tim
var totalValeur float64 var totalValeur float64
for _, pos := range positions { for _, pos := range positions {
if pos.Quantite <= 0 { if pos.Quantite == 0 {
continue continue
} }

View file

@ -8,13 +8,14 @@ import (
) )
type Account struct { type Account struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Nom string `json:"nom"` Nom string `json:"nom"`
Type string `json:"type"` Type string `json:"type"`
DeviseReference string `json:"devise_reference"` DeviseReference string `json:"devise_reference"`
Plafond *float64 `json:"plafond,omitempty"` Plafond *float64 `json:"plafond,omitempty"`
MasterAccountID *int32 `json:"master_account_id,omitempty"` MasterAccountID *int32 `json:"master_account_id,omitempty"`
Objectif *string `json:"objectif,omitempty"` Objectif *string `json:"objectif,omitempty"`
OwnerID int32 `json:"-"`
} }
type CreateAccountParams struct { type CreateAccountParams struct {
@ -31,11 +32,27 @@ type CreateEnvelopeParams struct {
var ErrMasterIsSubAccount = errors.New("master account cannot itself be a sub-account") var ErrMasterIsSubAccount = errors.New("master account cannot itself be a sub-account")
const accountCols = `id, nom, type, devise_reference, plafond, master_account_id, objectif` const accountCols = `id, nom, type, devise_reference, plafond, master_account_id, objectif, owner_id`
func (s *Store) ListAccounts(ctx context.Context) ([]Account, error) { // VerifyAccountOwner retourne pgx.ErrNoRows si le compte n'appartient pas à ownerID.
func (s *Store) VerifyAccountOwner(ctx context.Context, accountID, ownerID int32) error {
var exists bool
err := s.pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM account WHERE id = $1 AND owner_id = $2)`,
accountID, ownerID).Scan(&exists)
if err != nil {
return err
}
if !exists {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) ListAccounts(ctx context.Context, ownerID int32) ([]Account, error) {
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`SELECT `+accountCols+` FROM account WHERE master_account_id IS NULL ORDER BY nom`) `SELECT `+accountCols+` FROM account WHERE master_account_id IS NULL AND owner_id = $1 ORDER BY nom`,
ownerID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -80,22 +97,22 @@ func (s *Store) GetAccountWithEnvelopes(ctx context.Context, id int32) (AccountW
return AccountWithEnvelopes{Account: account, Envelopes: envelopes}, nil return AccountWithEnvelopes{Account: account, Envelopes: envelopes}, nil
} }
func (s *Store) CreateAccount(ctx context.Context, p CreateAccountParams) (Account, error) { func (s *Store) CreateAccount(ctx context.Context, p CreateAccountParams, ownerID int32) (Account, error) {
if p.DeviseReference == "" { if p.DeviseReference == "" {
p.DeviseReference = "EUR" p.DeviseReference = "EUR"
} }
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`INSERT INTO account (nom, type, devise_reference, plafond) `INSERT INTO account (nom, type, devise_reference, plafond, owner_id)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING `+accountCols, RETURNING `+accountCols,
p.Nom, p.Type, p.DeviseReference, p.Plafond) p.Nom, p.Type, p.DeviseReference, p.Plafond, ownerID)
if err != nil { if err != nil {
return Account{}, err return Account{}, err
} }
return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account]) return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account])
} }
func (s *Store) CreateEnvelope(ctx context.Context, masterID int32, p CreateEnvelopeParams) (Account, error) { func (s *Store) CreateEnvelope(ctx context.Context, masterID int32, p CreateEnvelopeParams, ownerID int32) (Account, error) {
master, err := s.GetAccount(ctx, masterID) master, err := s.GetAccount(ctx, masterID)
if err != nil { if err != nil {
return Account{}, err return Account{}, err
@ -103,45 +120,48 @@ func (s *Store) CreateEnvelope(ctx context.Context, masterID int32, p CreateEnve
if master.MasterAccountID != nil { if master.MasterAccountID != nil {
return Account{}, ErrMasterIsSubAccount return Account{}, ErrMasterIsSubAccount
} }
if master.OwnerID != ownerID {
return Account{}, pgx.ErrNoRows
}
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`INSERT INTO account (nom, type, devise_reference, master_account_id, objectif) `INSERT INTO account (nom, type, devise_reference, master_account_id, objectif, owner_id)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING `+accountCols, RETURNING `+accountCols,
p.Nom, master.Type, master.DeviseReference, masterID, p.Objectif) p.Nom, master.Type, master.DeviseReference, masterID, p.Objectif, ownerID)
if err != nil { if err != nil {
return Account{}, err return Account{}, err
} }
return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account]) return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account])
} }
func (s *Store) UpdateAccount(ctx context.Context, id int32, p CreateAccountParams) (Account, error) { func (s *Store) UpdateAccount(ctx context.Context, id int32, p CreateAccountParams, ownerID int32) (Account, error) {
if p.DeviseReference == "" { if p.DeviseReference == "" {
p.DeviseReference = "EUR" p.DeviseReference = "EUR"
} }
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`UPDATE account SET nom = $2, type = $3, devise_reference = $4, plafond = $5 `UPDATE account SET nom = $2, type = $3, devise_reference = $4, plafond = $5
WHERE id = $1 AND master_account_id IS NULL WHERE id = $1 AND master_account_id IS NULL AND owner_id = $6
RETURNING `+accountCols, RETURNING `+accountCols,
id, p.Nom, p.Type, p.DeviseReference, p.Plafond) id, p.Nom, p.Type, p.DeviseReference, p.Plafond, ownerID)
if err != nil { if err != nil {
return Account{}, err return Account{}, err
} }
return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account]) return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account])
} }
func (s *Store) UpdateEnvelope(ctx context.Context, id int32, p CreateEnvelopeParams) (Account, error) { func (s *Store) UpdateEnvelope(ctx context.Context, id int32, p CreateEnvelopeParams, ownerID int32) (Account, error) {
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`UPDATE account SET nom = $2, objectif = $3 `UPDATE account SET nom = $2, objectif = $3
WHERE id = $1 AND master_account_id IS NOT NULL WHERE id = $1 AND master_account_id IS NOT NULL AND owner_id = $4
RETURNING `+accountCols, RETURNING `+accountCols,
id, p.Nom, p.Objectif) id, p.Nom, p.Objectif, ownerID)
if err != nil { if err != nil {
return Account{}, err return Account{}, err
} }
return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account]) return pgx.CollectOneRow(rows, pgx.RowToStructByName[Account])
} }
func (s *Store) DeleteAccount(ctx context.Context, id int32) error { func (s *Store) DeleteAccount(ctx context.Context, id int32, ownerID int32) error {
_, err := s.pool.Exec(ctx, `DELETE FROM account WHERE id = $1`, id) _, err := s.pool.Exec(ctx, `DELETE FROM account WHERE id = $1 AND owner_id = $2`, id, ownerID)
return err return err
} }

View file

@ -106,9 +106,22 @@ func (s *Store) ListAccountTransactions(ctx context.Context, accountID int32, f
return pgx.CollectRows(rows, pgx.RowToStructByName[AccountTransaction]) return pgx.CollectRows(rows, pgx.RowToStructByName[AccountTransaction])
} }
func (s *Store) GetTransaction(ctx context.Context, id int64) (Transaction, error) { // GetTransaction retourne une transaction. Si ownerID > 0, vérifie que l'un des
rows, err := s.pool.Query(ctx, // comptes impliqués appartient à cet owner (retourne ErrNoRows sinon).
`SELECT `+txCols+` FROM transaction WHERE id = $1`, id) func (s *Store) GetTransaction(ctx context.Context, id int64, ownerID int32) (Transaction, error) {
q := `SELECT t.id, t.date::text, t.account_source_id, t.instrument_source_id, t.quantite_source,
t.account_dest_id, t.instrument_dest_id, t.quantite_dest,
t.tiers, t.label, t.categorie, t.validated, t.recurring_rule_id
FROM transaction t
LEFT JOIN account src ON src.id = t.account_source_id
LEFT JOIN account dst ON dst.id = t.account_dest_id
WHERE t.id = $1`
args := []any{id}
if ownerID > 0 {
q += ` AND (src.owner_id = $2 OR dst.owner_id = $2)`
args = append(args, ownerID)
}
rows, err := s.pool.Query(ctx, q, args...)
if err != nil { if err != nil {
return Transaction{}, err return Transaction{}, err
} }
@ -138,9 +151,9 @@ func (s *Store) CreateTransaction(ctx context.Context, p CreateTransactionParams
return tx, nil return tx, nil
} }
func (s *Store) UpdateTransaction(ctx context.Context, id int64, p CreateTransactionParams) (Transaction, error) { func (s *Store) UpdateTransaction(ctx context.Context, id int64, p CreateTransactionParams, ownerID int32) (Transaction, error) {
// Récupérer l'ancienne date pour invalider à partir du MIN(ancienne, nouvelle). // Récupérer l'ancienne date pour invalider à partir du MIN(ancienne, nouvelle).
old, err := s.GetTransaction(ctx, id) old, err := s.GetTransaction(ctx, id, ownerID)
if err != nil { if err != nil {
return Transaction{}, err return Transaction{}, err
} }
@ -172,7 +185,11 @@ func (s *Store) UpdateTransaction(ctx context.Context, id int64, p CreateTransac
return tx, nil return tx, nil
} }
func (s *Store) SetValidated(ctx context.Context, id int64, validated bool) (Transaction, error) { func (s *Store) SetValidated(ctx context.Context, id int64, validated bool, ownerID int32) (Transaction, error) {
// Vérifie l'ownership avant modification.
if _, err := s.GetTransaction(ctx, id, ownerID); err != nil {
return Transaction{}, err
}
rows, err := s.pool.Query(ctx, rows, err := s.pool.Query(ctx,
`UPDATE transaction SET validated = $2 WHERE id = $1 RETURNING `+txCols, `UPDATE transaction SET validated = $2 WHERE id = $1 RETURNING `+txCols,
id, validated) id, validated)
@ -187,8 +204,8 @@ func (s *Store) SetValidated(ctx context.Context, id int64, validated bool) (Tra
return tx, nil return tx, nil
} }
func (s *Store) DeleteTransaction(ctx context.Context, id int64) error { func (s *Store) DeleteTransaction(ctx context.Context, id int64, ownerID int32) error {
tx, err := s.GetTransaction(ctx, id) tx, err := s.GetTransaction(ctx, id, ownerID)
if err != nil { if err != nil {
return err return err
} }

View file

@ -0,0 +1 @@
ALTER TABLE account DROP COLUMN owner_id;

View file

@ -0,0 +1 @@
ALTER TABLE account ADD COLUMN owner_id INTEGER NOT NULL DEFAULT 1;