nutzer einladen + sesssion cache correct clear

This commit is contained in:
Simon Martens
2025-05-24 10:57:31 +02:00
parent a46e2bc474
commit 74de26f560
7 changed files with 226 additions and 124 deletions

View File

@@ -99,6 +99,28 @@ func (c *UserSessionCache) Delete(sessionTokenClear string) {
} }
} }
func (c *UserSessionCache) DeleteSessionByUserID(uid string) {
if uid == "" {
return
}
c.cache.Range(func(key, value any) bool {
entry, ok := value.(*cacheEntry)
if !ok {
c.cache.Delete(key)
return true
}
if entry.user.Id == uid {
c.cache.Delete(key)
c.mu.Lock()
c.approximateSize--
c.mu.Unlock()
}
return true
})
}
func (c *UserSessionCache) Clear() { func (c *UserSessionCache) Clear() {
c.cache.Clear() c.cache.Clear()
c.mu.Lock() c.mu.Lock()

View File

@@ -6,6 +6,7 @@ import (
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware" "github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels" "github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating" "github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/router"
) )
@@ -76,6 +77,32 @@ func (p *UserEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
} }
} }
func DeleteSessionsForUser(app core.App, uid string) error {
defer middleware.SESSION_CACHE.DeleteSessionByUserID(uid)
records := []*core.Record{}
err := app.RecordQuery(dbmodels.SESSIONS_TABLE).
Where(dbx.HashExp{dbmodels.SESSIONS_USER_FIELD: uid}).
All(&records)
if err != nil {
return err
}
err = app.RunInTransaction(func(tx core.App) error {
for _, r := range records {
if err := tx.Delete(r); err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func InvalidDataResponse(engine *templating.Engine, e *core.RequestEvent, error string, user *dbmodels.FixedUser) error { func InvalidDataResponse(engine *templating.Engine, e *core.RequestEvent, error string, user *dbmodels.FixedUser) error {
data := make(map[string]any) data := make(map[string]any)
data["error"] = error data["error"] = error
@@ -153,10 +180,12 @@ func (p *UserEditPage) POST(engine *templating.Engine, app core.App) HandleFunc
user_proxy.SetEmail(formdata.Email) user_proxy.SetEmail(formdata.Email)
user_proxy.SetName(formdata.Name) user_proxy.SetName(formdata.Name)
rolechanged := false
if formdata.Role != "" && formdata.Role != user_proxy.Role() { if formdata.Role != "" && formdata.Role != user_proxy.Role() {
if user.Role == "Admin" && if user.Role == "Admin" &&
(formdata.Role == "User" || formdata.Role == "Editor" || formdata.Role == "Admin") { (formdata.Role == "User" || formdata.Role == "Editor" || formdata.Role == "Admin") {
user_proxy.SetRole(formdata.Role) user_proxy.SetRole(formdata.Role)
rolechanged = true
} else { } else {
return InvalidDataResponse(engine, e, "Rolle nicht erlaubt", &fu) return InvalidDataResponse(engine, e, "Rolle nicht erlaubt", &fu)
} }
@@ -180,8 +209,16 @@ func (p *UserEditPage) POST(engine *templating.Engine, app core.App) HandleFunc
return InvalidDataResponse(engine, e, err.Error(), &fu) return InvalidDataResponse(engine, e, err.Error(), &fu)
} }
// TODO: this is lazy, we just need to delete the sessions of the changed user if rolechanged {
middleware.SESSION_CACHE.Clear() if err := DeleteSessionsForUser(app, user_proxy.Id); err != nil {
return InvalidDataResponse(engine, e, "Fehler beim Löschen der Sitzungen: "+err.Error(), &fu)
}
if user_proxy.Id == user.Id {
// INFO: user changed his own role, so we log him out
return e.Redirect(303, "/login/")
}
}
fu = user_proxy.Fixed() fu = user_proxy.Fixed()
data["user"] = &fu data["user"] = &fu

File diff suppressed because one or more lines are too long

View File

@@ -33,7 +33,7 @@
{{ end }} {{ end }}
<form class="w-full grid grid-cols-3 gap-4" method="POST" x-data="{ openpw: false }"> <form class="w-full grid grid-cols-3 gap-4" method="POST" x-data="{ openpw: false }">
<div <div
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="username" class="text-sm text-gray-700 font-bold"> <label for="username" class="text-sm text-gray-700 font-bold">
@@ -43,7 +43,7 @@
type="text" type="text"
name="name" name="name"
id="name" id="name"
class="mt-1 block w-full rounded-md focus:border-none focus:outline-none" class="mt-1 block w-full focus:border-none focus:outline-none"
placeholder="" placeholder=""
required required
autocomplete="off" autocomplete="off"
@@ -51,7 +51,7 @@
autofocus /> autofocus />
</div> </div>
<div <div
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="username" class="text-sm text-gray-700 font-bold"> <label for="username" class="text-sm text-gray-700 font-bold">
@@ -68,7 +68,7 @@
value="{{ $model.user.Email }}" /> value="{{ $model.user.Email }}" />
</div> </div>
<div <div
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="role" class="text-sm text-gray-700 font-bold"> <label for="role" class="text-sm text-gray-700 font-bold">
@@ -94,6 +94,32 @@
</option> </option>
</select> </select>
</div> </div>
{{- if and
(eq $model.request.user.Role "Admin")
(eq $model.request.user.Id
$model.user.Id)
-}}
<div class="flex flex-row col-span-3">
<div class="align-top">
<i class="ri-information-line text-gray-700 mt-2 mr-2 align-top"></i>
</div>
<p class="text-sm text-gray-700 max-w-[80ch]">
Achtung! Wenn Sie Ihre eigenen Berechtigungen ändern, verlieren Sie die Möglichkeit,
diese Änderungen rückgängig zu machen. Weiter werden Sie von allen laufenden Sitzungen
abgemeldet und müssen sich erneut anmelden.
</p>
</div>
{{- else if (eq $model.request.user.Role "Admin") -}}
<div class="flex flex-row col-span-3">
<div class="align-top">
<i class="ri-information-line text-gray-700 mt-2 mr-2 align-top"></i>
</div>
<p class="text-sm text-gray-700 max-w-[80ch]">
Achtung! Wenn Sie die Rolle eines Benutzers ändern, wird dieser von allen laufenden
Sitzungen abgemeldet und muss sich erneut anmelden.
</p>
</div>
{{- end -}}
<div class="col-span-3"> <div class="col-span-3">
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" name="openpw" id="openpw" x-model="openpw" class="mr-2" /> <input type="checkbox" name="openpw" id="openpw" x-model="openpw" class="mr-2" />
@@ -105,7 +131,7 @@
{{- if not (eq $model.request.user.Role "Admin") -}} {{- if not (eq $model.request.user.Role "Admin") -}}
<div <div
x-bind:style="!openpw ? 'display:none' : ''" x-bind:style="!openpw ? 'display:none' : ''"
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="password_old" class="text-sm text-gray-700 font-bold"> Altes Passwort </label> <label for="password_old" class="text-sm text-gray-700 font-bold"> Altes Passwort </label>
@@ -120,7 +146,7 @@
{{- end -}} {{- end -}}
<div <div
x-bind:style="!openpw ? 'display:none' : ''" x-bind:style="!openpw ? 'display:none' : ''"
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="password" class="text-sm text-gray-700 font-bold"> Neues Passwort </label> <label for="password" class="text-sm text-gray-700 font-bold"> Neues Passwort </label>
@@ -135,7 +161,7 @@
</div> </div>
<div <div
x-bind:style="!openpw ? 'display:none' : ''" x-bind:style="!openpw ? 'display:none' : ''"
class="col-span-3 border-2 border-transparent px-2 class="rounded-xs col-span-3 border-2 border-transparent px-3
py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600 py-1 pb-1.5 border-l-2 focus-within:border-l-slate-600
bg-slate-200 focus-within:bg-slate-100 transition-all duration-100"> bg-slate-200 focus-within:bg-slate-100 transition-all duration-100">
<label for="password_repeat" class="text-sm text-gray-700 font-bold"> <label for="password_repeat" class="text-sm text-gray-700 font-bold">

View File

@@ -88,39 +88,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="user-invites flex container-normal mx-auto px-8 mt-4"> <div class="user-invites flex container-normal mx-auto px-8 mt-4">
<div class="flex flex-row w-full">
<div class="flex-col max-w-xl w-full"> <div class="flex-col max-w-xl w-full">
<div <!-- QR Code -->
class="col-span-9 grid grid-cols-3 justify-between mb-4 py-1 px-1 border rounded-xs gap-x-2 <div class="mb-4 p-4 border rounded-xs hidden" id="qr"></div>
bg-slate-200 user-chooser">
<a <!-- Access URL and Token -->
href="/user/management/access/User?redirectTo={{ $model.redirect_url }}" <div class="mb-6 flex flex-col">
class="text-center px-4 text-gray-700 hover:text-slate-950 block"
{{ if eq $model.role "User" }}aria-current="page"{{ end }}>
Benutzer
</a>
<a
href="/user/management/access/Editor?redirectTo={{ $model.redirect_url }}"
class="text-gray-700 hover:text-slate-950 block px-4 text-center"
{{ if eq $model.role "Editor" }}aria-current="page"{{ end }}>
Redakteur
</a>
<a
href="/user/management/access/Admin?redirectTo={{ $model.redirect_url }}"
class="text-gray-700 hover:text-slate-950 text-center px-4 block"
{{ if eq $model.role "Admin" }}aria-current="page"{{ end }}>
Admin
</a>
</div>
<!--
<div class="flex justify-center mt-8 items-baseline">
<div>
<img class="h-20 w-20 border" src="/assets/favicon.png" />
</div>
</div>
-->
<div class="col-span-7 col-start-2 mb-4 p-4 border rounded-xs hidden" id="qr"></div>
<div class="col-span-9 mb-6 flex flex-col">
<div class="flex flex-row"> <div class="flex flex-row">
<input <input
type="text" type="text"
@@ -164,7 +140,6 @@
id="csrf_token" id="csrf_token"
required required
value="{{ $model.csrf_token }}" /> value="{{ $model.csrf_token }}" />
<div class="col-span-9 flex flex-row items-center justify-center"> <div class="col-span-9 flex flex-row items-center justify-center">
<button <button
type="submit" type="submit"
@@ -178,12 +153,26 @@
</div> </div>
</form> </form>
</div> </div>
<div class="col-span-9 text-right text-slate-400 mb-1 mt-3"> <div class="text-right text-slate-400 mb-1 mt-3">
Gültig bis Gültig bis
{{ $model.validUntil }} {{ $model.validUntil }}
</div> </div>
</div>
<!-- End of Access URL and Token -->
</div>
<!-- End of First Column -->
<!-- Second column -->
<div class="ml-12 flex flex-col">
<!-- User Role Chooser -->
<div class="user-chooser flex flex-col gap-y-3">
<a
href="/user/management/access/User?redirectTo={{ $model.redirect_url }}"
{{ if eq $model.role "User" }}aria-current="page"{{ end }}>
Benutzer
</a>
{{ if eq $model.role "User" }} {{ if eq $model.role "User" }}
<div class="max-w-[60ch] mt-1 text-sm text-blue-600 flex flex-row gap-x-2 hyphens-auto"> <div class="max-w-[60ch] text-sm text-blue-600 flex flex-row gap-x-2 hyphens-auto">
<div> <div>
<i class="ri-error-warning-line"></i> <i class="ri-error-warning-line"></i>
</div> </div>
@@ -192,18 +181,14 @@
löschen. löschen.
</div> </div>
</div> </div>
{{ else if eq $model.role "Admin" }} {{- end -}}
<div class="max-w-[60ch] mt-1 text-sm text-red-600 flex flex-row gap-x-2 hyphens-auto"> <a
<div> href="/user/management/access/Editor?redirectTo={{ $model.redirect_url }}"
<i class="ri-error-warning-line"></i> {{ if eq $model.role "Editor" }}aria-current="page"{{ end }}>
</div> Redakteur
<div> </a>
Administratoren können alle Felder und Datensätze einsehen, bearbeiten und löschen. {{- if eq $model.role "Editor" -}}
Administratoren können Passwörter setzen, Nutzer einladen und deaktivieren. <div class="max-w-[60ch] text-sm text-orange-600 flex flex-row gap-x-2 hyphens-auto">
</div>
</div>
{{ else if eq $model.role "Editor" }}
<div class="max-w-[60ch] mt-1 text-sm text-orange-600 flex flex-row gap-x-2 hyphens-auto">
<div> <div>
<i class="ri-error-warning-line"></i> <i class="ri-error-warning-line"></i>
</div> </div>
@@ -213,6 +198,25 @@
</div> </div>
</div> </div>
{{- end -}} {{- end -}}
<a
href="/user/management/access/Admin?redirectTo={{ $model.redirect_url }}"
{{ if eq $model.role "Admin" }}aria-current="page"{{ end }}>
Admin
</a>
{{- if eq $model.role "Admin" -}}
<div class="max-w-[60ch] text-sm text-red-600 flex flex-row gap-x-2 hyphens-auto">
<div>
<i class="ri-error-warning-line"></i>
</div>
<div>
Administratoren können alle Felder und Datensätze einsehen, bearbeiten und löschen.
Administratoren können Passwörter setzen, Nutzer einladen und deaktivieren.
</div> </div>
</div> </div>
{{- end -}}
</div>
<!-- End of User Role Chooser -->
</div>
<!-- End of second column -->
</div>
</div> </div>

View File

@@ -0,0 +1 @@
<title>Musenalm &ndash; Nutzer einladen</title>

View File

@@ -512,12 +512,24 @@
@apply !text-slate-900 bg-stone-50; @apply !text-slate-900 bg-stone-50;
} }
.user-invites .user-chooser a { .user-chooser a {
@apply py-1 rounded-xs no-underline; @apply px-4 py-2 no-underline text-gray-500 hover:text-slate-900 font-serif font-bold border-l-4 border-transparent hover:bg-slate-100 transition-all duration-75 rounded-xs;
} }
.user-invites .user-chooser a[aria-current="page"] { .user-chooser a[aria-current="page"] {
@apply font-bold !bg-stone-50 relative border-b z-20 shadow; @apply text-slate-900 font-bold bg-slate-100 border-slate-900 shadow-sm;
}
.user-chooser a[aria-current="page"]:nth-child(1) {
@apply border-blue-500;
}
.user-chooser a[aria-current="page"]:nth-child(2) {
@apply border-orange-600;
}
.user-chooser a[aria-current="page"]:nth-child(3) {
@apply border-red-600;
} }
@keyframes spin { @keyframes spin {