Tenant-Feature Phase 3b: Login-Redirect + Tenant-Context in Manage-UI
- reqcontext-Package: shared contextKey für httpapi und manage - Login-Redirect: Tenant-User → /manage/<slug>, Admin → /admin - GetUserByUsername: LEFT JOIN tenants für TenantSlug-Befüllung - manage/ui.go: reqcontext.UserFromContext statt hardcoded "morz" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b21be6469
commit
27c4562175
5 changed files with 67 additions and 24 deletions
|
|
@ -31,8 +31,14 @@ func HandleLoginUI(authStore *store.AuthStore) http.HandlerFunc {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Redirect if already logged in.
|
||||
if cookie, err := r.Cookie(sessionCookieName); err == nil {
|
||||
if _, err := authStore.GetSessionUser(r.Context(), cookie.Value); err == nil {
|
||||
if u, err := authStore.GetSessionUser(r.Context(), cookie.Value); err == nil {
|
||||
if u.Role == "admin" {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
} else if u.TenantSlug != "" {
|
||||
http.Redirect(w, r, "/manage/"+u.TenantSlug, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -108,11 +114,14 @@ func HandleLoginPost(authStore *store.AuthStore, cfg config.Config) http.Handler
|
|||
case "admin":
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
default:
|
||||
// Tenant users – Phase 3 will provide the full tenant slug; for now fall back to /admin.
|
||||
if user.TenantSlug != "" {
|
||||
http.Redirect(w, r, "/manage/"+user.TenantSlug, http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLogoutPost deletes the session and clears the cookie (POST /logout).
|
||||
func HandleLogoutPost(authStore *store.AuthStore) http.HandlerFunc {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier"
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext"
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
|
||||
)
|
||||
|
||||
|
|
@ -90,7 +91,10 @@ func HandleManageUI(
|
|||
return
|
||||
}
|
||||
|
||||
tenant, _ := tenants.Get(r.Context(), "morz") // v1: single tenant
|
||||
var tenant *store.Tenant
|
||||
if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
|
||||
tenant, _ = tenants.Get(r.Context(), u.TenantSlug)
|
||||
}
|
||||
if tenant == nil {
|
||||
tenant = &store.Tenant{ID: screen.TenantID, Name: "MORZ"}
|
||||
}
|
||||
|
|
@ -151,9 +155,13 @@ func HandleCreateScreenUI(tenants *store.TenantStore, screens *store.ScreenStore
|
|||
orientation = "landscape"
|
||||
}
|
||||
|
||||
tenant, err := tenants.Get(r.Context(), "morz")
|
||||
tenantSlug := "morz"
|
||||
if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
|
||||
tenantSlug = u.TenantSlug
|
||||
}
|
||||
tenant, err := tenants.Get(r.Context(), tenantSlug)
|
||||
if err != nil {
|
||||
http.Error(w, "standard-tenant nicht gefunden", http.StatusInternalServerError)
|
||||
http.Error(w, "tenant nicht gefunden", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -194,9 +202,13 @@ func HandleProvisionUI(tenants *store.TenantStore, screens *store.ScreenStore) h
|
|||
orientation = "landscape"
|
||||
}
|
||||
|
||||
tenant, err := tenants.Get(r.Context(), "morz")
|
||||
tenantSlug := "morz"
|
||||
if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
|
||||
tenantSlug = u.TenantSlug
|
||||
}
|
||||
tenant, err := tenants.Get(r.Context(), tenantSlug)
|
||||
if err != nil {
|
||||
http.Error(w, "standard-tenant nicht gefunden", http.StatusInternalServerError)
|
||||
http.Error(w, "tenant nicht gefunden", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,22 +5,16 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext"
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
|
||||
)
|
||||
|
||||
// contextKey is an unexported type for context keys in this package.
|
||||
// Using a named type prevents collisions with keys from other packages.
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
contextKeyUser contextKey = iota
|
||||
)
|
||||
|
||||
// UserFromContext returns the authenticated *store.User stored in ctx,
|
||||
// or nil if none is present.
|
||||
// It delegates to reqcontext so that sub-packages (e.g. manage) can share
|
||||
// the same context key without creating an import cycle.
|
||||
func UserFromContext(ctx context.Context) *store.User {
|
||||
u, _ := ctx.Value(contextKeyUser).(*store.User)
|
||||
return u
|
||||
return reqcontext.UserFromContext(ctx)
|
||||
}
|
||||
|
||||
// RequireAuth returns middleware that validates the morz_session cookie.
|
||||
|
|
@ -41,7 +35,7 @@ func RequireAuth(authStore *store.AuthStore) func(http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), contextKeyUser, user)
|
||||
ctx := reqcontext.WithUser(r.Context(), user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
26
server/backend/internal/reqcontext/reqcontext.go
Normal file
26
server/backend/internal/reqcontext/reqcontext.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Package reqcontext provides a shared context key and helpers for storing
|
||||
// the authenticated user in a request context. It lives in its own package so
|
||||
// that both httpapi (middleware) and httpapi/manage (handlers) can import it
|
||||
// without creating an import cycle.
|
||||
package reqcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const contextKeyUser contextKey = 0
|
||||
|
||||
// WithUser returns a new context that carries u.
|
||||
func WithUser(ctx context.Context, u *store.User) context.Context {
|
||||
return context.WithValue(ctx, contextKeyUser, u)
|
||||
}
|
||||
|
||||
// UserFromContext returns the *store.User stored in ctx, or nil if none.
|
||||
func UserFromContext(ctx context.Context) *store.User {
|
||||
u, _ := ctx.Value(contextKeyUser).(*store.User)
|
||||
return u
|
||||
}
|
||||
|
|
@ -44,12 +44,14 @@ type AuthStore struct{ pool *pgxpool.Pool }
|
|||
func NewAuthStore(pool *pgxpool.Pool) *AuthStore { return &AuthStore{pool} }
|
||||
|
||||
// GetUserByUsername returns the user with the given username or pgx.ErrNoRows.
|
||||
// TenantSlug is populated via LEFT JOIN on tenants.
|
||||
func (s *AuthStore) GetUserByUsername(ctx context.Context, username string) (*User, error) {
|
||||
row := s.pool.QueryRow(ctx,
|
||||
`select id, tenant_id, username, password_hash, role, created_at
|
||||
from users
|
||||
where username = $1`, username)
|
||||
return scanUser(row)
|
||||
`select u.id, u.tenant_id, coalesce(t.slug, ''), u.username, u.password_hash, u.role, u.created_at
|
||||
from users u
|
||||
left join tenants t on t.id = u.tenant_id
|
||||
where u.username = $1`, username)
|
||||
return scanUserWithSlug(row)
|
||||
}
|
||||
|
||||
// CreateSession inserts a new session for userID with the given TTL and returns the session.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue