Checkpoint: password reset, rule expiry, migrations
- Self-service and admin password-change endpoints
(PUT /v1/auth/password, PUT /v1/accounts/{id}/password)
- Policy rule time-scoped expiry (not_before / expires_at)
with migration 000006 and engine filtering
- golang-migrate integration; embedded SQL migrations
- PolicyRecord fieldalignment lint fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
||||
@@ -90,6 +91,8 @@ func (s *Server) handleSetTags(w http.ResponseWriter, r *http.Request) {
|
||||
type policyRuleResponse struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
NotBefore *string `json:"not_before,omitempty"`
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
Description string `json:"description"`
|
||||
RuleBody policy.RuleBody `json:"rule"`
|
||||
ID int64 `json:"id"`
|
||||
@@ -102,15 +105,24 @@ func policyRuleToResponse(rec *model.PolicyRuleRecord) (policyRuleResponse, erro
|
||||
if err := json.Unmarshal([]byte(rec.RuleJSON), &body); err != nil {
|
||||
return policyRuleResponse{}, fmt.Errorf("decode rule body: %w", err)
|
||||
}
|
||||
return policyRuleResponse{
|
||||
resp := policyRuleResponse{
|
||||
ID: rec.ID,
|
||||
Priority: rec.Priority,
|
||||
Description: rec.Description,
|
||||
RuleBody: body,
|
||||
Enabled: rec.Enabled,
|
||||
CreatedAt: rec.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: rec.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}, nil
|
||||
CreatedAt: rec.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: rec.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if rec.NotBefore != nil {
|
||||
s := rec.NotBefore.UTC().Format(time.RFC3339)
|
||||
resp.NotBefore = &s
|
||||
}
|
||||
if rec.ExpiresAt != nil {
|
||||
s := rec.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
resp.ExpiresAt = &s
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleListPolicyRules(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -133,6 +145,8 @@ func (s *Server) handleListPolicyRules(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
type createPolicyRuleRequest struct {
|
||||
Description string `json:"description"`
|
||||
NotBefore *string `json:"not_before,omitempty"`
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
Rule policy.RuleBody `json:"rule"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
@@ -157,6 +171,29 @@ func (s *Server) handleCreatePolicyRule(w http.ResponseWriter, r *http.Request)
|
||||
priority = 100 // default
|
||||
}
|
||||
|
||||
// Parse optional time-scoped validity window.
|
||||
var notBefore, expiresAt *time.Time
|
||||
if req.NotBefore != nil {
|
||||
t, err := time.Parse(time.RFC3339, *req.NotBefore)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "not_before must be RFC3339", "bad_request")
|
||||
return
|
||||
}
|
||||
notBefore = &t
|
||||
}
|
||||
if req.ExpiresAt != nil {
|
||||
t, err := time.Parse(time.RFC3339, *req.ExpiresAt)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "expires_at must be RFC3339", "bad_request")
|
||||
return
|
||||
}
|
||||
expiresAt = &t
|
||||
}
|
||||
if notBefore != nil && expiresAt != nil && !expiresAt.After(*notBefore) {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "expires_at must be after not_before", "bad_request")
|
||||
return
|
||||
}
|
||||
|
||||
ruleJSON, err := json.Marshal(req.Rule)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
@@ -171,7 +208,7 @@ func (s *Server) handleCreatePolicyRule(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
rec, err := s.db.CreatePolicyRule(req.Description, priority, string(ruleJSON), createdBy)
|
||||
rec, err := s.db.CreatePolicyRule(req.Description, priority, string(ruleJSON), createdBy, notBefore, expiresAt)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
@@ -202,10 +239,14 @@ func (s *Server) handleGetPolicyRule(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type updatePolicyRuleRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Rule *policy.RuleBody `json:"rule,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
NotBefore *string `json:"not_before,omitempty"`
|
||||
ExpiresAt *string `json:"expires_at,omitempty"`
|
||||
Rule *policy.RuleBody `json:"rule,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
ClearNotBefore *bool `json:"clear_not_before,omitempty"`
|
||||
ClearExpiresAt *bool `json:"clear_expires_at,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdatePolicyRule(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -230,11 +271,39 @@ func (s *Server) handleUpdatePolicyRule(w http.ResponseWriter, r *http.Request)
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
}
|
||||
s := string(b)
|
||||
ruleJSON = &s
|
||||
js := string(b)
|
||||
ruleJSON = &js
|
||||
}
|
||||
|
||||
if err := s.db.UpdatePolicyRule(rec.ID, req.Description, req.Priority, ruleJSON); err != nil {
|
||||
// Parse optional time-scoped validity window updates.
|
||||
// Double-pointer semantics: nil = no change, non-nil→nil = clear, non-nil→non-nil = set.
|
||||
var notBefore, expiresAt **time.Time
|
||||
if req.ClearNotBefore != nil && *req.ClearNotBefore {
|
||||
var nilTime *time.Time
|
||||
notBefore = &nilTime // non-nil outer, nil inner → set to NULL
|
||||
} else if req.NotBefore != nil {
|
||||
t, err := time.Parse(time.RFC3339, *req.NotBefore)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "not_before must be RFC3339", "bad_request")
|
||||
return
|
||||
}
|
||||
tp := &t
|
||||
notBefore = &tp
|
||||
}
|
||||
if req.ClearExpiresAt != nil && *req.ClearExpiresAt {
|
||||
var nilTime *time.Time
|
||||
expiresAt = &nilTime // non-nil outer, nil inner → set to NULL
|
||||
} else if req.ExpiresAt != nil {
|
||||
t, err := time.Parse(time.RFC3339, *req.ExpiresAt)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusBadRequest, "expires_at must be RFC3339", "bad_request")
|
||||
return
|
||||
}
|
||||
tp := &t
|
||||
expiresAt = &tp
|
||||
}
|
||||
|
||||
if err := s.db.UpdatePolicyRule(rec.ID, req.Description, req.Priority, ruleJSON, notBefore, expiresAt); err != nil {
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user