Add separate production, testserver, and demo deployments
CI / test (push) Failing after 10s

This commit is contained in:
2026-09-14 19:58:31 +02:00
parent b87aa0aa17
commit d06ff4a94a
37 changed files with 1293 additions and 121 deletions
+10 -5
View File
@@ -141,15 +141,20 @@ func (app *application) accountEmailConfirmPost(w http.ResponseWriter, r *http.R
func (app *application) renderAccount(w http.ResponseWriter, r *http.Request, form accountForm, status int) {
data := app.newTemplateData(r)
data.Form = form
if user, ok := userFromContext(r.Context()); ok {
data.AccountProtected = app.config.DemoAccountEmail != "" && strings.EqualFold(strings.TrimSpace(user.Email), app.config.DemoAccountEmail)
}
if !app.loadOptionalGarden(w, r, data) {
return
}
sessions, _, err := client.FromContext(r.Context()).AccountSessions(r.Context())
if err != nil {
app.handleAPIError(w, r, err)
return
if !data.AccountProtected {
sessions, _, err := client.FromContext(r.Context()).AccountSessions(r.Context())
if err != nil {
app.handleAPIError(w, r, err)
return
}
data.AccountSessions = sessions
}
data.AccountSessions = sessions
app.render(w, status, "account.tmpl", data)
}
func (app *application) copyAccountError(form *accountForm, err error) {
+1
View File
@@ -204,6 +204,7 @@ func (app *application) webEnvironmentVariables() []client.EnvironmentVariable {
{Component: "Web", Name: "GARDOMATIC_WEB_HOST", Value: app.config.Host},
{Component: "Web", Name: "GARDOMATIC_WEB_PORT", Value: fmt.Sprint(app.config.Port)},
{Component: "Web", Name: "GARDOMATIC_API_BASE_URL", Value: app.config.APIBaseURL},
{Component: "Web", Name: "GARDOMATIC_DEMO_ACCOUNT_EMAIL", Value: app.config.DemoAccountEmail},
{Component: "Web", Name: "GARDOMATIC_SESSION_COOKIE_NAME", Value: app.config.SessionCookieName},
{Component: "Web", Name: "GARDOMATIC_COOKIE_SECURE", Value: fmt.Sprint(app.config.CookieSecure)},
}
+4 -1
View File
@@ -47,7 +47,10 @@ type CommonTemplateData struct {
}
// AccountTemplateData contains account-management page data.
type AccountTemplateData struct{ AccountSessions []client.AccountSession }
type AccountTemplateData struct {
AccountSessions []client.AccountSession
AccountProtected bool
}
// AdminTemplateData contains application-administration page data.
type AdminTemplateData struct {
@@ -1,8 +1,10 @@
{{define "title"}}Benutzerkonto{{end}}
{{define "main"}}
<header class='page-heading'><div><p class='eyebrow'>Persönlich</p><h2>Benutzerkonto</h2></div></header>{{$form:=.Form}}{{with $form.Message}}<p class='form-message'>{{.}}</p>{{end}}
{{if .AccountProtected}}<section class='panel'><h3>Gemeinsames Demokonto</h3><p>E-Mail-Adresse, Passwort, Profil und aktive Sitzungen sind geschützt, damit alle Besucher weiterhin Zugang zur Demo haben. Die Garteninhalte kannst du frei ausprobieren; sie werden jede Nacht zurückgesetzt.</p></section>{{else}}
<div class='account-grid'><section class='panel'><h3>Profil</h3><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.profile") "garden" .ID}}{{else}}{{webPath "account.profile"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='name'>Name</label><input id='name' name='name' value='{{$form.Name}}' required>{{with index $form.Errors "name"}}<p class='field-error'>{{.}}</p>{{end}}<label for='color'>Farbe für deine Einträge</label><input id='color' name='color' type='color' value='{{$form.Color}}' required><p class='muted'>Diese Farbe wird verwendet, um deine Tagebucheinträge schnell zu erkennen.</p>{{with index $form.Errors "color"}}<p class='field-error'>{{.}}</p>{{end}}<button>Profil speichern</button></form></section>
<section class='panel'><h3>E-Mail-Adresse</h3><p>Aktuell: {{.CurrentUser.Email}}</p><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.email") "garden" .ID}}{{else}}{{webPath "account.email"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='email'>Neue E-Mail-Adresse</label><input id='email' type='email' name='email' value='{{$form.Email}}' required><label for='email-password'>Aktuelles Passwort</label><input id='email-password' type='password' name='current_password' required>{{with index $form.Errors "email"}}<p class='field-error'>{{.}}</p>{{end}}{{with index $form.Errors "current_password"}}<p class='field-error'>{{.}}</p>{{end}}<button>Bestätigung senden</button></form></section>
<section class='panel'><h3>Passwort</h3><form method='POST' action='{{with .Garden}}{{pathWithQuery (webPath "account.password") "garden" .ID}}{{else}}{{webPath "account.password"}}{{end}}'><input type='hidden' name='csrf_token' value='{{.CSRFToken}}'><label for='current-password'>Aktuelles Passwort</label><input id='current-password' type='password' name='current_password' required><label for='new-password'>Neues Passwort</label><input id='new-password' type='password' name='new_password' minlength='8' required>{{with index $form.Errors "new_password"}}<p class='field-error'>{{.}}</p>{{end}}<label for='confirm-password'>Neues Passwort bestätigen</label><input id='confirm-password' type='password' name='new_password_confirmation' required>{{with index $form.Errors "new_password_confirmation"}}<p class='field-error'>{{.}}</p>{{end}}{{with index $form.Errors "current_password"}}<p class='field-error'>{{.}}</p>{{end}}<button>Passwort ändern</button></form></section>
<section class='panel'><h3>Aktive Sitzungen</h3>{{if .AccountSessions}}{{range .AccountSessions}}{{$session := .}}<div class='member-row'><div><strong>{{if .Current}}Dieses Gerät{{else}}Weitere Sitzung{{end}}</strong><br><span class='muted'>Angemeldet: {{humanDate .CreatedAt}} · gültig bis {{humanDate .ExpiresAt}}</span></div><form action='{{with $.Garden}}{{pathWithQuery (webPath "account.session.delete" $session.ID) "garden" .ID}}{{else}}{{webPath "account.session.delete" .ID}}{{end}}' method='POST'><input type='hidden' name='csrf_token' value='{{$.CSRFToken}}'><button class='danger'>Widerrufen</button></form></div>{{end}}{{else}}<p>Keine aktiven Sitzungen gefunden.</p>{{end}}</section></div>
{{end}}
{{end}}
+21
View File
@@ -128,6 +128,27 @@ func TestGardenContextIsKeptInAccountAndSettingsLinks(t *testing.T) {
}
}
func TestProtectedDemoAccountHidesAccountMutationForms(t *testing.T) {
app := newTestApplication(t)
user := client.User{ID: 1, Name: "Demo", Email: "demo@example.com", Activated: true}
data := &templateData{
commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Form: accountForm{Errors: map[string]string{}}},
accountTemplateData: accountTemplateData{AccountProtected: true},
}
response := httptest.NewRecorder()
app.render(response, http.StatusOK, "account.tmpl", data)
body := response.Body.String()
if !strings.Contains(body, "Gemeinsames Demokonto") {
t.Fatalf("protected account notice is missing: %s", body)
}
for _, forbidden := range []string{"action='/account/profile'", "action='/account/password'", "action='/account/email'", "Widerrufen"} {
if strings.Contains(body, forbidden) {
t.Errorf("protected account still offers %q: %s", forbidden, body)
}
}
}
func TestAdminTemplateKeepsSelectedGardenInNavigationAndForms(t *testing.T) {
app := newTestApplication(t)
user := client.User{ID: 1, Name: "Alice", Activated: true, Permissions: []string{"roles:manage"}}
+2 -1
View File
@@ -15,8 +15,8 @@ import (
"syscall"
"time"
"gardomatic.kleiax.de/lib/client"
"gardomatic.kleiax.de/internal/vcs"
"gardomatic.kleiax.de/lib/client"
"github.com/go-playground/form/v4"
)
@@ -30,6 +30,7 @@ type Config struct {
Port int
Env string
APIBaseURL string
DemoAccountEmail string
SessionCookieName string
CookieSecure bool
}