Add optional email verification and registration control

Implemented email service infrastructure with SMTP support and optional email verification for self-hosted deployments. Registration flow now supports open/closed modes and hardcoded Apocapoc branding.

Key features:
- Email service with SMTP and template rendering
- Optional email verification (auto-verified without SMTP config)
- Registration modes: open/closed for access control
- Hardcoded Apocapoc branding (AppName, AppURL, DefaultFrom)
- Separate registration and login flows (registration no longer returns tokens)
This commit is contained in:
2025-11-28 08:21:51 +01:00
parent a95a703905
commit 00f6b51228
28 changed files with 890 additions and 99 deletions
@@ -9,15 +9,7 @@ func TestHabitCRUDFlow(t *testing.T) {
ts := setupTestServer(t)
defer ts.Close()
registerBody := RegisterRequest{
Email: "habituser@example.com",
Password: "Password123!",
Timezone: "UTC",
}
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "")
var authResp AuthResponse
decodeResponse(t, rr, &authResp)
token := authResp.Token
token := registerAndLogin(t, *ts.Router, "habituser@example.com", "Password123!")
var habitID string
@@ -131,30 +123,22 @@ func TestHabitCRUDFlow(t *testing.T) {
})
t.Run("Access other user's habit", func(t *testing.T) {
registerBody := RegisterRequest{
Email: "otheruser@example.com",
Password: "Password123!",
Timezone: "UTC",
}
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "")
var authResp AuthResponse
decodeResponse(t, rr, &authResp)
otherToken := authResp.Token
otherToken := registerAndLogin(t, *ts.Router, "otheruser@example.com", "Password123!")
reqBody := CreateHabitRequest{
Name: "Other User Habit",
Type: "BOOLEAN",
Frequency: "DAILY",
}
rr = makeRequest(t, *ts.Router, "POST", "/api/v1/habits", reqBody, otherToken)
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/habits", reqBody, otherToken)
var createResp map[string]string
decodeResponse(t, rr, &createResp)
otherHabitID := createResp["id"]
rr = makeRequest(t, *ts.Router, "GET", "/api/v1/habits/"+otherHabitID, nil, token)
rr2 := makeRequest(t, *ts.Router, "GET", "/api/v1/habits/"+otherHabitID, nil, token)
if rr.Code != http.StatusForbidden {
t.Errorf("Expected status 403, got %d", rr.Code)
if rr2.Code != http.StatusForbidden {
t.Errorf("Expected status 403, got %d", rr2.Code)
}
})
}