mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-28 16:55:32 +00:00
Registration form for new users
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
type FixedAccessToken struct {
|
type FixedAccessToken struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
CSRF string `json:"csrf"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Created string `json:"created"`
|
Created string `json:"created"`
|
||||||
Updated string `json:"updated"`
|
Updated string `json:"updated"`
|
||||||
@@ -15,6 +16,10 @@ type FixedAccessToken struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FixedAccessToken) IsExpired() bool {
|
||||||
|
return s.Expires.IsZero() || s.Expires.Before(types.NowDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
var _ core.RecordProxy = (*AccessToken)(nil)
|
var _ core.RecordProxy = (*AccessToken)(nil)
|
||||||
|
|
||||||
type AccessToken struct {
|
type AccessToken struct {
|
||||||
@@ -43,6 +48,14 @@ func (u *AccessToken) User() string {
|
|||||||
return u.GetString(ACCESS_TOKENS_USER_FIELD)
|
return u.GetString(ACCESS_TOKENS_USER_FIELD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AccessToken) CSRF() string {
|
||||||
|
return u.GetString(ACCESS_TOKENS_CSRF_FIELD)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AccessToken) SetCSRF(csrf string) {
|
||||||
|
u.Set(ACCESS_TOKENS_CSRF_FIELD, csrf)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *AccessToken) SetUser(userId string) {
|
func (u *AccessToken) SetUser(userId string) {
|
||||||
u.Set(ACCESS_TOKENS_USER_FIELD, userId)
|
u.Set(ACCESS_TOKENS_USER_FIELD, userId)
|
||||||
}
|
}
|
||||||
@@ -79,6 +92,10 @@ func (u *AccessToken) SetStatus(status string) {
|
|||||||
u.Set(ACCESS_TOKENS_STATUS_FIELD, status)
|
u.Set(ACCESS_TOKENS_STATUS_FIELD, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AccessToken) IsExpired() bool {
|
||||||
|
return u.Expires().IsZero() || u.Expires().Before(types.NowDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
func (u *AccessToken) Fixed() *FixedAccessToken {
|
func (u *AccessToken) Fixed() *FixedAccessToken {
|
||||||
return &FixedAccessToken{
|
return &FixedAccessToken{
|
||||||
Token: u.Token(),
|
Token: u.Token(),
|
||||||
|
|||||||
47
dbmodels/access_tokens_functions.go
Normal file
47
dbmodels/access_tokens_functions.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package dbmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateAccessToken(
|
||||||
|
app core.App,
|
||||||
|
userID string,
|
||||||
|
url string,
|
||||||
|
duration time.Duration,
|
||||||
|
) (*AccessToken, error) {
|
||||||
|
collection, err := app.FindCollectionByNameOrId(ACCESS_TOKENS_TABLE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find '%s' collection: %w", ACCESS_TOKENS_TABLE, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accesTokenClear, err := generateSecureRandomToken(secureTokenByteLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate session token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csrfTokenClear, err := generateSecureRandomToken(secureTokenByteLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate CSRF token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := core.NewRecord(collection)
|
||||||
|
token := NewAccessToken(record)
|
||||||
|
|
||||||
|
token.SetToken(accesTokenClear)
|
||||||
|
token.SetCSRF(csrfTokenClear)
|
||||||
|
token.SetUser(userID)
|
||||||
|
token.SetURL(url)
|
||||||
|
token.SetStatus(TOKEN_STATUS_VALUES[0]) // Active
|
||||||
|
token.SetExpires(types.NowDateTime().Add(duration))
|
||||||
|
|
||||||
|
if err := app.Save(record); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save access token record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
35
dbmodels/user_functions.go
Normal file
35
dbmodels/user_functions.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package dbmodels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateUser(
|
||||||
|
app core.App,
|
||||||
|
email string,
|
||||||
|
password string,
|
||||||
|
name string,
|
||||||
|
role string,
|
||||||
|
) (*User, error) {
|
||||||
|
collection, err := app.FindCollectionByNameOrId(USERS_TABLE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find '%s' collection: %w", USERS_TABLE, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := core.NewRecord(collection)
|
||||||
|
user := NewUser(record)
|
||||||
|
user.SetEmail(email)
|
||||||
|
user.SetPassword(password)
|
||||||
|
user.SetName(name)
|
||||||
|
user.SetVerified(true)
|
||||||
|
user.SetDeactivated(false)
|
||||||
|
user.SetRole(role)
|
||||||
|
|
||||||
|
if err := app.Save(record); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save user record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
26
middleware/accesstoken.go
Normal file
26
middleware/accesstoken.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HasToken() func(*core.RequestEvent) error {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
token := req.AccessToken()
|
||||||
|
if token == nil {
|
||||||
|
return e.Error(401, "Unauthorized", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.IsExpired() {
|
||||||
|
return e.Error(403, "Forbidden", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.URL != e.Request.URL.Path {
|
||||||
|
return e.Error(403, "Forbidden", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
22
middleware/admin.go
Normal file
22
middleware/admin.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsAdmin() func(*core.RequestEvent) error {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
user := req.User()
|
||||||
|
if user == nil {
|
||||||
|
return e.Error(401, "Unauthorized", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Role != "Admin" {
|
||||||
|
return e.Error(403, "Forbidden", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ func accessTokensFields(usersCollectionId string) core.FieldsList {
|
|||||||
},
|
},
|
||||||
&core.RelationField{
|
&core.RelationField{
|
||||||
Name: dbmodels.ACCESS_TOKENS_USER_FIELD,
|
Name: dbmodels.ACCESS_TOKENS_USER_FIELD,
|
||||||
Required: true,
|
Required: false,
|
||||||
CollectionId: usersCollectionId,
|
CollectionId: usersCollectionId,
|
||||||
CascadeDelete: true,
|
CascadeDelete: true,
|
||||||
Presentable: true,
|
Presentable: true,
|
||||||
|
|||||||
@@ -36,4 +36,7 @@ const (
|
|||||||
|
|
||||||
P_LOGIN_NAME = "login"
|
P_LOGIN_NAME = "login"
|
||||||
P_LOGOUT_NAME = "logout"
|
P_LOGOUT_NAME = "logout"
|
||||||
|
|
||||||
|
P_USER_MGMT_ACCESS_NAME = "user_management_access"
|
||||||
|
P_USER_CREATE_NAME = "user_create"
|
||||||
)
|
)
|
||||||
|
|||||||
136
pages/user_create.go
Normal file
136
pages/user_create.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
URL_USER_CREATE = "/user/new/"
|
||||||
|
PATH_VALUE_ROLE = "role"
|
||||||
|
TEMPLATE_USER_CREATE = "/user/new/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ucp := &UserCreatePage{
|
||||||
|
StaticPage: pagemodels.StaticPage{
|
||||||
|
Name: pagemodels.P_USER_CREATE_NAME,
|
||||||
|
Layout: "blank",
|
||||||
|
Template: TEMPLATE_USER_CREATE,
|
||||||
|
URL: URL_USER_CREATE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Register(ucp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCreatePage struct {
|
||||||
|
pagemodels.StaticPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserCreatePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||||
|
rg := router.Group(URL_USER_CREATE)
|
||||||
|
rg.BindFunc(middleware.HasToken())
|
||||||
|
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
|
||||||
|
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserCreatePage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
role := e.Request.PathValue("role")
|
||||||
|
if role != "User" && role != "Editor" && role != "Admin" {
|
||||||
|
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check access token
|
||||||
|
data := make(map[string]any)
|
||||||
|
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data["role"] = role
|
||||||
|
data["csrf_nonce"] = nonce
|
||||||
|
data["csrf_token"] = token
|
||||||
|
|
||||||
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InvalidSignupResponse(engine *templating.Engine, e *core.RequestEvent, error string) error {
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["error"] = error
|
||||||
|
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data["csrf_nonce"] = nonce
|
||||||
|
data["csrf_token"] = token
|
||||||
|
str, err := engine.RenderToString(e, data, TEMPLATE_USER_CREATE, "blank")
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.HTML(400, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserCreatePage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
role := e.Request.PathValue("role")
|
||||||
|
if role != "User" && role != "Editor" && role != "Admin" {
|
||||||
|
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check access token
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["role"] = role
|
||||||
|
|
||||||
|
formdata := struct {
|
||||||
|
Username string `form:"username"`
|
||||||
|
Password string `form:"password"`
|
||||||
|
PasswordRepeat string `form:"password_repeat"`
|
||||||
|
Name string `form:"name"`
|
||||||
|
CsrfNonce string `form:"csrf_nonce"`
|
||||||
|
CsrfToken string `form:"csrf_token"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := e.BindBody(&formdata); err != nil {
|
||||||
|
return engine.Response500(e, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data["formdata"] = formdata
|
||||||
|
|
||||||
|
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
|
||||||
|
return InvalidSignupResponse(engine, e, "CSRF-Token ungültig.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if formdata.Username == "" || formdata.Password == "" || formdata.Name == "" {
|
||||||
|
return InvalidSignupResponse(engine, e, "Bitte alle Felder ausfüllen.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if formdata.Password != formdata.PasswordRepeat {
|
||||||
|
return InvalidSignupResponse(engine, e, "Passwörter stimmen nicht überein.")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := app.FindAuthRecordByEmail(dbmodels.USERS_TABLE, formdata.Username)
|
||||||
|
if err == nil {
|
||||||
|
return InvalidSignupResponse(engine, e, "Benutzer existiert bereits.")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := dbmodels.CreateUser(app, formdata.Username, formdata.Password, formdata.Name, role)
|
||||||
|
if err != nil {
|
||||||
|
return InvalidSignupResponse(engine, e, fmt.Sprintf("Fehler beim Erstellen des Benutzers: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
data["user"] = user
|
||||||
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
119
pages/user_management_access.go
Normal file
119
pages/user_management_access.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||||
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
URL_USER_MANAGEMENT_ACCESS = "/user/management/access/"
|
||||||
|
TEMPLATE_USER_MANAGEMENT_ACCESS = "/user/management/access/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ump := &UserManagementAccessPage{
|
||||||
|
StaticPage: pagemodels.StaticPage{
|
||||||
|
Name: pagemodels.P_USER_MGMT_ACCESS_NAME,
|
||||||
|
Layout: "blank",
|
||||||
|
Template: TEMPLATE_USER_MANAGEMENT_ACCESS,
|
||||||
|
URL: URL_USER_MANAGEMENT_ACCESS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Register(ump)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserManagementAccessPage struct {
|
||||||
|
pagemodels.StaticPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserManagementAccessPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||||
|
rg := router.Group(URL_USER_MANAGEMENT_ACCESS)
|
||||||
|
rg.BindFunc(middleware.IsAdmin())
|
||||||
|
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
|
||||||
|
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserManagementAccessPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
role := e.Request.PathValue("role")
|
||||||
|
if role != "User" && role != "Editor" && role != "Admin" {
|
||||||
|
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
path_access := URL_USER_CREATE + role
|
||||||
|
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
|
||||||
|
var access_token *dbmodels.AccessToken
|
||||||
|
if err != nil {
|
||||||
|
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, nil)
|
||||||
|
}
|
||||||
|
access_token = token
|
||||||
|
} else {
|
||||||
|
access_token = dbmodels.NewAccessToken(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if access token exists, if not generate
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["role"] = role
|
||||||
|
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + access_token.Token()
|
||||||
|
data["relative_url"] = path_access + "?token=" + access_token.Token()
|
||||||
|
data["validUntil"] = access_token.Expires().Time().Local().Format("02.01.2006 15:04")
|
||||||
|
|
||||||
|
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, data)
|
||||||
|
}
|
||||||
|
data["csrf_nonce"] = nonce
|
||||||
|
data["csrf_token"] = token
|
||||||
|
|
||||||
|
redirect_url := e.Request.URL.Query().Get("redirectTo")
|
||||||
|
if redirect_url != "" {
|
||||||
|
data["redirect_url"] = redirect_url
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserManagementAccessPage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
role := e.Request.PathValue("role")
|
||||||
|
if role != "User" && role != "Editor" && role != "Admin" {
|
||||||
|
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
path_access := URL_USER_CREATE + role
|
||||||
|
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
|
||||||
|
if err == nil {
|
||||||
|
go app.Delete(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response500(e, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["role"] = role
|
||||||
|
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + token.Token()
|
||||||
|
data["relative_url"] = path_access + "?token=" + token.Token()
|
||||||
|
data["validUntil"] = token.Expires().Time().Format("02.01.2006 15:04")
|
||||||
|
|
||||||
|
redirect_url := e.Request.URL.Query().Get("redirectTo")
|
||||||
|
if redirect_url != "" {
|
||||||
|
data["redirect_url"] = redirect_url
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
views/assets/js/qrcode.min.js
vendored
Normal file
1
views/assets/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,31 @@
|
|||||||
{{- $date := Today -}}
|
{{- $date := Today -}}
|
||||||
<footer class="container-normal pb-1.5 text-base text-gray-800">
|
<footer
|
||||||
|
class="container-normal pb-1.5 text-base text-gray-800 relative"
|
||||||
|
x-data="{ openusermenu: false }">
|
||||||
|
<div class="" x-show="openusermenu">
|
||||||
|
<div
|
||||||
|
class="absolute right-0 bottom-10 bg-white border border-gray-300 rounded-md shadow mt-2
|
||||||
|
[&>a]:no-underline [&>a]:text-gray-700 [&>a]:hover:bg-gray-100 [&>a]:hover:text-gray-900
|
||||||
|
[&>a]:block [&>a]:px-3 [&>a]:py-2 [&>a]:text-sm [&>a]:rounded-md [&>a]:w-full [&>a]:text-left
|
||||||
|
[&>a]:whitespace-nowrap [&>a]:transition-all [&>a]:duration-200 [&>a]:border-b
|
||||||
|
[&>a]:last:border-b-0">
|
||||||
|
<a href="/user/edit" class="">
|
||||||
|
<i class="ri-user-3-line"></i>
|
||||||
|
Profil bearbeiten
|
||||||
|
</a>
|
||||||
|
{{ if and .request.user (eq .request.user.Role "Admin") }}
|
||||||
|
<a href="/user/management/access/User?redirectTo={{ .request.fullpath }}" class="">
|
||||||
|
<i class="ri-group-3-line"></i>
|
||||||
|
Benutzer einladen
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
<a href="/logout?redirectTo={{ .request.fullpath }}" class="">
|
||||||
|
<i class="ri-logout-box-line"></i>
|
||||||
|
Ausloggen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-12 pt-3 flex flex-row justify-between">
|
<div class="mt-12 pt-3 flex flex-row justify-between">
|
||||||
<div>
|
<div>
|
||||||
<i class="ri-creative-commons-line"></i>
|
<i class="ri-creative-commons-line"></i>
|
||||||
@@ -19,15 +45,20 @@
|
|||||||
<a href="https://github.com/Theodor-Springmann-Stiftung/musenalm">Code</a>
|
<a href="https://github.com/Theodor-Springmann-Stiftung/musenalm">Code</a>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
{{ if .request.user }}
|
{{ if .request.user }}
|
||||||
<i class="ri-user-3-line"></i>
|
<button class="inline-block cursor-pointer" @click="openusermenu = !openusermenu">
|
||||||
{{ if .request.user.Name }}
|
<i class="ri-user-3-line"></i>
|
||||||
<b>{{ .request.user.Name }}</b>
|
{{ if .request.user.Name }}
|
||||||
{{ else }}
|
<b>{{ .request.user.Name }}</b>
|
||||||
<b>{{ .request.user.Email }}</b>
|
{{ else }}
|
||||||
{{ end }}
|
<b>{{ .request.user.Email }}</b>
|
||||||
|
{{ end }}
|
||||||
|
<i class="ri-arrow-up-s-fill"></i>
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
|
|
|
||||||
<i class="ri-logout-box-line"></i>
|
<i class="ri-logout-box-line"></i>
|
||||||
<a href="/logout?redirectTo={{ .request.fullpath }}">Logout</a>
|
<a href="/logout?redirectTo={{ .request.fullpath }}">Logout</a>
|
||||||
|
-->
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<i class="ri-login-box-line"></i>
|
<i class="ri-login-box-line"></i>
|
||||||
<a href="/login?redirectTo={{ .request.fullpath }}">Login</a>
|
<a href="/login?redirectTo={{ .request.fullpath }}">Login</a>
|
||||||
|
|||||||
1
views/public/js/qrcode.min.js
vendored
Normal file
1
views/public/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
views/routes/user/edit/body.gohtml
Normal file
0
views/routes/user/edit/body.gohtml
Normal file
0
views/routes/user/edit/head.hohtml
Normal file
0
views/routes/user/edit/head.hohtml
Normal file
207
views/routes/user/management/access/body.gohtml
Normal file
207
views/routes/user/management/access/body.gohtml
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
{{ $model := . }}
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/assets/js/qrcode.min.js"></script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const qrElement = document.getElementById('qr');
|
||||||
|
const tokenElement = document.getElementById('token');
|
||||||
|
const accessUrl = "{{ $model.access_url }}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} timeout - Maximum time to wait in milliseconds.
|
||||||
|
* @param {number} interval - How often to check in milliseconds.
|
||||||
|
* @returns {Promise<Function>} Resolves with the QRCode constructor when available.
|
||||||
|
*/
|
||||||
|
function getQRCodeWhenAvailable(timeout = 5000, interval = 100) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let elapsedTime = 0;
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
if (typeof window.QRCode === 'function') {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve(window.QRCode); // Resolve with the QRCode object/function
|
||||||
|
} else {
|
||||||
|
elapsedTime += interval;
|
||||||
|
if (elapsedTime >= timeout) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
console.error('Timed out waiting for QRCode to become available.');
|
||||||
|
reject(new Error('QRCode not available after ' + timeout + 'ms. Check if qrcode.min.js is loaded correctly and sets window.QRCode.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// INFO: We have to wait for the QRCode object to be available. It's messy.
|
||||||
|
async function genQRCode() {
|
||||||
|
const QRCode = await getQRCodeWhenAvailable();
|
||||||
|
if (qrElement && accessUrl && qrElement.innerHTML.trim() === '') {
|
||||||
|
new QRCode(qrElement, {
|
||||||
|
text: accessUrl,
|
||||||
|
width: 1280,
|
||||||
|
height: 1280,
|
||||||
|
colorDark: "#000000",
|
||||||
|
colorLight: "#ffffff",
|
||||||
|
correctLevel: QRCode.CorrectLevel.H
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
qrElement.classList.remove('hidden');
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genQRCode();
|
||||||
|
if (tokenElement) {
|
||||||
|
tokenElement.addEventListener('focus', () => tokenElement.select());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex max-w-md mx-auto !pt-44 user-invites">
|
||||||
|
<div class="flex-col w-full">
|
||||||
|
{{- if not $model.redirect_url -}}
|
||||||
|
<a href="/" class="text-gray-700 hover:text-slate-950 mb-9 block">
|
||||||
|
<i class="ri-arrow-left-s-line"></i> Startseite
|
||||||
|
</a>
|
||||||
|
{{- else -}}
|
||||||
|
<a href="{{ $model.redirect_url }}" class="text-gray-700 hover:text-slate-950 mb-9 block">
|
||||||
|
<i class="ri-arrow-left-s-line"></i> Zurück
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
<!--
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-slate-900 text-center">Benutzer einladen</h1>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="col-span-9 grid grid-cols-3 justify-between mb-4 py-1 px-1 border rounded-md gap-x-2
|
||||||
|
bg-slate-200 user-chooser">
|
||||||
|
<a
|
||||||
|
href="/user/management/access/User?redirectTo={{ $model.redirect_url }}"
|
||||||
|
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-lg shadow hidden" id="qr"></div>
|
||||||
|
<div class="col-span-9 mb-6 flex flex-col">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="token"
|
||||||
|
id="token"
|
||||||
|
class="w-full text-center border border-slate-300 rounded-md shadow-sm
|
||||||
|
focus:border-slate-500 focus:ring-slate-500 p-1 px-2 overflow-ellipsis"
|
||||||
|
value="{{ $model.access_url }}"
|
||||||
|
deactive
|
||||||
|
readonly />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-2 inline-flex justify-center py-2 px-3 border border-transparent rounded-md
|
||||||
|
shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer
|
||||||
|
focus:outline-none no-underline
|
||||||
|
focus:ring-2 focus:ring-offset-2 focus:ring-slate-500"
|
||||||
|
onclick="navigator.clipboard.writeText('{{ $model.access_url }}')">
|
||||||
|
<i class="ri-file-copy-line"></i>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="{{ $model.relative_url }}"
|
||||||
|
target="_blank"
|
||||||
|
class="ml-2 inline-flex justify-center py-2 px-3 border border-transparent rounded-md
|
||||||
|
shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer
|
||||||
|
focus:outline-none no-underline
|
||||||
|
focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
|
||||||
|
<i class="ri-external-link-line"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-9 text-center text-slate-400 mb-1 mt-3">
|
||||||
|
Gültig bis zum
|
||||||
|
{{ $model.validUntil }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-slate-600 flex flex-row gap-x-2">
|
||||||
|
<div>
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Unter diesem Link können neue Accounts registriert werden. Geben Sie diesen Link an neue
|
||||||
|
Nutzer der Datenbank weiter.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ if eq $model.role "User" }}
|
||||||
|
<div class="mt-1 text-sm text-blue-600 flex flex-row gap-x-2">
|
||||||
|
<div>
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>Benutzer können private Felder und Daten einsehen, aber nicht bearbeiten.</div>
|
||||||
|
</div>
|
||||||
|
{{ else if eq $model.role "Admin" }}
|
||||||
|
<div class="mt-1 text-sm text-red-600 flex flex-row gap-x-2">
|
||||||
|
<div>
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Administratoren können alle Felder und Daten einsehen und bearbeiten. Administratoren
|
||||||
|
können Nutzer einladen und löschen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ else if eq $model.role "Editor" }}
|
||||||
|
<div class="mt-1 text-sm text-orange-600 flex flex-row gap-x-2">
|
||||||
|
<div>
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>Redakteure können alle Felder und Daten der Datenbank einsehen und bearbeiten.</div>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
<form class="w-full grid grid-cols-9 gap-4" method="POST">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="csrf_nonce"
|
||||||
|
id="csrf_nonce"
|
||||||
|
required
|
||||||
|
value="{{ $model.csrf_nonce }}" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="csrf_token"
|
||||||
|
id="csrf_token"
|
||||||
|
required
|
||||||
|
value="{{ $model.csrf_token }}" />
|
||||||
|
|
||||||
|
<div class="col-span-9 flex flex-row items-center justify-center">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex justify-center py-2 px-3 border border-transparent rounded-full
|
||||||
|
shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer
|
||||||
|
focus:outline-none no-underline
|
||||||
|
focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
|
||||||
|
<i class="ri-loop-left-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="col-span-3">
|
||||||
|
<a href="/forgot-password" class="text-sm text-slate-600 hover:text-slate-900">
|
||||||
|
Passwort vergessen?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
0
views/routes/user/management/access/head.gohtml
Normal file
0
views/routes/user/management/access/head.gohtml
Normal file
0
views/routes/user/management/body.gohtml
Normal file
0
views/routes/user/management/body.gohtml
Normal file
0
views/routes/user/management/head.gohtml
Normal file
0
views/routes/user/management/head.gohtml
Normal file
143
views/routes/user/new/body.gohtml
Normal file
143
views/routes/user/new/body.gohtml
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{{ $model := . }}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex max-w-md mx-auto !pt-44">
|
||||||
|
<div class="flex-col w-full">
|
||||||
|
<a href="/" class="text-gray-700 hover:text-slate-950">
|
||||||
|
<i class="ri-arrow-left-s-line"></i> Startseite
|
||||||
|
</a>
|
||||||
|
<div class="flex justify-center mt-8 items-baseline">
|
||||||
|
<div>
|
||||||
|
<img class="h-20 w-20 border" src="/assets/favicon.png" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl self-baseline text-center w-full my-6">
|
||||||
|
Musenalm<br class="mb-3" />
|
||||||
|
Neuer
|
||||||
|
{{ if eq $model.role "User" -}}
|
||||||
|
Nutzer
|
||||||
|
{{ else if eq $model.role "Admin" -}}
|
||||||
|
Administrator
|
||||||
|
{{ else -}}
|
||||||
|
Redakteur
|
||||||
|
{{- end -}}
|
||||||
|
</h1>
|
||||||
|
{{ if $model.error }}
|
||||||
|
<div
|
||||||
|
class="text-red-800 text-sm mt-2 rounded bg-red-200 p-2 font-bold border-red-700
|
||||||
|
border-2 mb-3">
|
||||||
|
{{ $model.error }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if not $model.user }}
|
||||||
|
<form class="w-full grid grid-cols-3 gap-4" method="POST">
|
||||||
|
<div
|
||||||
|
class="col-span-3 border-2 border-transparent focus-within:border-slate-600 px-2 py-1 pb-1.5
|
||||||
|
bg-slate-200 focus-within:bg-slate-50 rounded-md transition-all duration-100">
|
||||||
|
<label for="username" class="text-sm text-gray-700 font-bold">
|
||||||
|
Name <i class="ri-text"></i>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
class="mt-1 block w-full rounded-md focus:border-none focus:outline-none"
|
||||||
|
placeholder=""
|
||||||
|
required
|
||||||
|
autocomplete="name"
|
||||||
|
value=""
|
||||||
|
autofocus />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-span-3 border-2 border-transparent focus-within:border-slate-600 px-2 py-1 pb-1.5
|
||||||
|
bg-slate-200 focus-within:bg-slate-50 rounded-md transition-all duration-100">
|
||||||
|
<label for="username" class="text-sm text-gray-700 font-bold">
|
||||||
|
E-Mail <i class="ri-at-line"></i>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
class="mt-1 block w-full rounded-md focus:border-none focus:outline-none"
|
||||||
|
placeholder=""
|
||||||
|
required
|
||||||
|
value=""
|
||||||
|
autofocus />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-span-3 border-2 border-transparent focus-within:border-slate-600 px-2 py-1 pb-1.5
|
||||||
|
bg-slate-200 focus-within:bg-slate-50 rounded-md transition-all duration-100">
|
||||||
|
<label for="password" class="text-sm text-gray-700 font-bold">
|
||||||
|
Passwort <i class="ri-key-2-line"></i>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
minlength="10"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
class="mt-1 block w-full rounded-md focus:border-none focus:outline-none"
|
||||||
|
placeholder=""
|
||||||
|
required />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-span-3 border-2 border-transparent focus-within:border-slate-600 px-2 py-1 pb-1.5
|
||||||
|
bg-slate-200 focus-within:bg-slate-50 rounded-md transition-all duration-100">
|
||||||
|
<label for="password" class="text-sm text-gray-700 font-bold">
|
||||||
|
Passwort wiederholen <i class="ri-key-2-line"></i>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
minlength="10"
|
||||||
|
name="password_repeat"
|
||||||
|
id="password_repeat"
|
||||||
|
class="mt-1 block w-full rounded-md focus:border-none focus:outline-none"
|
||||||
|
placeholder=""
|
||||||
|
required />
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="csrf_nonce"
|
||||||
|
id="csrf_nonce"
|
||||||
|
required
|
||||||
|
value="{{ $model.csrf_nonce }}" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="csrf_token"
|
||||||
|
id="csrf_token"
|
||||||
|
required
|
||||||
|
value="{{ $model.csrf_token }}" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md
|
||||||
|
shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
|
||||||
|
Registrieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="col-span-3">
|
||||||
|
<a href="/forgot-password" class="text-sm text-slate-600 hover:text-slate-900">
|
||||||
|
Passwort vergessen?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
|
{{- else -}}
|
||||||
|
<div
|
||||||
|
class="text-green-800 text-sm mt-2 rounded bg-green-200 p-2 font-bold border-green-700
|
||||||
|
border-2 mb-3">
|
||||||
|
Benutzer {{ $model.user.Name }} erfolgreich angelegt.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="/login"
|
||||||
|
class="w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md
|
||||||
|
shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-offset-2 focus:ring-slate-500">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
0
views/routes/user/new/head.gohtml
Normal file
0
views/routes/user/new/head.gohtml
Normal file
@@ -512,6 +512,14 @@
|
|||||||
@apply !text-slate-900 bg-stone-50;
|
@apply !text-slate-900 bg-stone-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-invites .user-chooser a {
|
||||||
|
@apply py-1 rounded no-underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-invites .user-chooser a[aria-current="page"] {
|
||||||
|
@apply font-bold !bg-stone-50 relative border-b z-20 shadow;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
Reference in New Issue
Block a user