go: add usr updating [wip]
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
leo 2023-06-02 20:00:14 +02:00
parent 32aa8d8852
commit 5f8548958f
Signed by: wanderer
SSH Key Fingerprint: SHA256:Dp8+iwKHSlrMEHzE3bJnPng70I7LEsa3IJXRH/U+idQ
6 changed files with 419 additions and 8 deletions

View File

@ -58,8 +58,10 @@ func (a *App) SetupRoutes() error {
e.GET("/manage/users", handlers.ManageUsers(), handlers.MiddlewareSession) e.GET("/manage/users", handlers.ManageUsers(), handlers.MiddlewareSession)
e.GET("/manage/users/new", handlers.ManageUsers(), handlers.MiddlewareSession) e.GET("/manage/users/new", handlers.ManageUsers(), handlers.MiddlewareSession)
e.GET("/manage/users/:id", handlers.ViewUser(), handlers.MiddlewareSession)
e.POST("/manage/users/create", handlers.CreateUser(), handlers.MiddlewareSession) e.POST("/manage/users/create", handlers.CreateUser(), handlers.MiddlewareSession)
e.GET("/manage/users/:id", handlers.ViewUser(), handlers.MiddlewareSession)
e.GET("/manage/users/:id/edit", handlers.EditUser(), handlers.MiddlewareSession, handlers.MiddlewareCache, compress)
e.POST("/manage/users/:id/update", handlers.UpdateUser(), handlers.MiddlewareSession)
e.GET("/logout", handlers.Logout(), compress) e.GET("/logout", handlers.Logout(), compress)
e.POST("/logout", handlers.Logout()) e.POST("/logout", handlers.Logout())

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"git.dotya.ml/mirre-mt/pcmt/ent" "git.dotya.ml/mirre-mt/pcmt/ent"
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user" moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
@ -306,7 +307,7 @@ func ViewUser() echo.HandlerFunc {
uid := new(userID) uid := new(userID)
err := c.Bind(uid) err := c.Bind(uid)
if err == nil { if err == nil { //nolint:dupl
usr, err := getUserByID(ctx, dbclient, uid.ID) usr, err := getUserByID(ctx, dbclient, uid.ID)
if err != nil { if err != nil {
if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic
@ -372,3 +373,293 @@ func ViewUser() echo.HandlerFunc {
) )
} }
} }
//nolint:dupl
func EditUser() echo.HandlerFunc {
return func(c echo.Context) error {
addHeaders(c)
u, ok := c.Get("sessUsr").(moduser.User)
if !ok {
return renderErrorPage(
c,
http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized),
"username was nil",
)
}
if !u.IsAdmin {
c.Logger().Debug("this is a restricted endpoint")
status := http.StatusUnauthorized
msg := http.StatusText(status)
return renderErrorPage(
c, status, msg+": You should not be here", "Restricted endpoint",
)
}
ctx, ok := c.Get("sloggerCtx").(context.Context)
if !ok {
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
}
p := page{
AppName: setting.AppName(),
AppVer: appver,
Title: "Manage Users - Edit User",
DevelMode: setting.IsDevel(),
Current: "manage-users-edit-user",
User: u,
}
tmpl := "manage/user-edit.tmpl"
data := make(map[string]any)
id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/edit"), "/manage/users/")
usr, err := getUserByID(ctx, dbclient, id)
if err != nil {
//nolint:dupl
switch {
case errors.Is(err, moduser.ErrUserNotFound):
c.Logger().Errorf("user not found by ID: '%s'", id)
data["flash"] = fmt.Sprintf("No user found with the UUID: %q", id)
p.Data = data
return c.Render(
http.StatusNotFound,
tmpl,
p,
)
case errors.Is(err, moduser.ErrFailedToQueryUser):
c.Logger().Errorf("failed to query user by ID: '%s'", id)
data["flash"] = fmt.Sprintf("failed to query user by UUID %q", id)
p.Data = data
return c.Render(
http.StatusInternalServerError,
tmpl,
p,
)
case errors.Is(err, moduser.ErrBadUUID):
c.Logger().Errorf("Invalid UUID '%s': %q", id, err)
data["flash"] = fmt.Sprintf("invalid UUID %q", id)
p.Data = data
return c.Render(
http.StatusBadRequest,
"manage/user-details.tmpl",
p,
)
}
c.Logger().Errorf("UUID-related issue for UUID '%s': %q", id, err)
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
data["user"] = usr
p.Data = data
return c.Render(
http.StatusOK,
tmpl,
p,
)
}
}
//nolint:dupl
func UpdateUser() echo.HandlerFunc { //nolint:gocognit
return func(c echo.Context) error {
addHeaders(c)
u, ok := c.Get("sessUsr").(moduser.User)
if !ok {
return renderErrorPage(
c,
http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized),
"username was nil",
)
}
if !u.IsAdmin {
c.Logger().Debug("this is a restricted endpoint")
status := http.StatusUnauthorized
msg := http.StatusText(status)
return renderErrorPage(
c, status, msg+": You should not be here", "Restricted endpoint",
)
}
ctx, ok := c.Get("sloggerCtx").(context.Context)
if !ok {
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
}
p := page{
AppName: setting.AppName(),
AppVer: appver,
Title: "Manage Users - Edit User",
DevelMode: setting.IsDevel(),
Current: "manage-users-edit-user",
User: u,
}
tmpl := "manage/user-edit.tmpl"
data := make(map[string]any)
id := strings.TrimPrefix(strings.TrimSuffix(c.Request().URL.Path, "/update"), "/manage/users/")
usr, err := getUserByID(ctx, dbclient, id)
if err != nil {
switch {
case errors.Is(err, moduser.ErrUserNotFound):
c.Logger().Errorf("user not found by ID: '%s'", id)
data["flash"] = fmt.Sprintf("No user found with the UUID: %q", id)
p.Data = data
return c.Render(
http.StatusNotFound,
tmpl,
p,
)
case errors.Is(err, moduser.ErrFailedToQueryUser):
c.Logger().Errorf("failed to query user by ID: '%s'", id)
data["flash"] = fmt.Sprintf("failed to query user by UUID %q", id)
p.Data = data
return c.Render(
http.StatusInternalServerError,
tmpl,
p,
)
case errors.Is(err, moduser.ErrBadUUID):
c.Logger().Errorf("Invalid UUID '%s': %q", id, err)
data["flash"] = fmt.Sprintf("invalid UUID %q", id)
p.Data = data
return c.Render(
http.StatusBadRequest,
"manage/user-details.tmpl",
p,
)
}
c.Logger().Errorf("UUID-related issue for UUID '%s': %q", id, err)
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
uu := new(userCreate)
if err := c.Bind(uu); err != nil {
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest),
err.Error(),
)
}
if uu.Username == "" || uu.Password == "" || uu.RepeatPassword == "" || uu.Password != uu.RepeatPassword {
c.Logger().Error("username or password not set, returning to /manage/users/edit")
msg := "Error: both username and password need to be set"
if uu.Password != uu.RepeatPassword {
msg += "; the same password needs to be passed in twice"
}
data["flash"] = msg
data["form"] = uu
p.Data = data
return c.Render(
http.StatusBadRequest,
"manage/user-edit.tmpl",
p,
)
}
if usr.Username != uu.Username {
exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username)
if err != nil {
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
if exists {
msg := fmt.Sprintf("Error: username %q is already taken", uu.Username)
c.Logger().Warn(msg)
data["flash"] = msg
p.Data = data
return c.Render(
http.StatusBadRequest,
"manage/user-edit.tmpl",
p,
)
}
}
if usr.Email != uu.Email {
exists, err := moduser.EmailExists(ctx, dbclient, uu.Email)
if err != nil {
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
if exists {
msg := fmt.Sprintf("Error: email %q is already taken", uu.Email)
c.Logger().Error(msg)
return renderErrorPage(
c,
http.StatusBadRequest,
msg,
msg,
)
}
}
// now update
data["user"] = usr
p.Data = data
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/manage/users/%s/edit", usr.ID))
}
}

View File

@ -153,14 +153,14 @@ func Exists(ctx context.Context, client *ent.Client, username, email string) (bo
if err != nil { if err != nil {
log.Warn("failed to check whether username is taken", "error", err, "username requested", username) log.Warn("failed to check whether username is taken", "error", err, "username requested", username)
return false, fmt.Errorf("failed querying user: %w", err) return false, ErrFailedToQueryUser
} }
emailExists, err := EmailExists(ctx, client, email) emailExists, err := EmailExists(ctx, client, email)
if err != nil { if err != nil {
log.Warn("failed to check whether user email exists", "error", err, "user email requested", email) log.Warn("failed to check whether user email exists", "error", err, "user email requested", email)
return false, fmt.Errorf("failed querying user: %w", err) return false, ErrFailedToQueryUser
} }
switch { switch {

View File

@ -12,6 +12,11 @@
</a> </a>
</div> </div>
{{if and .Data .Data.user -}} {{if and .Data .Data.user -}}
<div class="flex justify-end gap-2 lg:gap-0 place-items-center">
<a href="/manage/users/{{- .Data.user.ID -}}/edit" class="w-auto py-1 mt-0 text-center text-blue-500 dark:text-blue-400 md:mx-2 lg:mx-4 hover:underline hover:text-blue-600 dark:hover:text-blue-300">
Edit
</a>
</div>
<div class="p-2 mt-3 lg:mx-auto border-2 dark:border-slate-500 rounded-sm space-y-0"> <div class="p-2 mt-3 lg:mx-auto border-2 dark:border-slate-500 rounded-sm space-y-0">
<div class="flex max-h-14 place-items-baseline justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700"> <div class="flex max-h-14 place-items-baseline justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
<span class="w-24 md:w-32 text-purple-500 dark:text-purple-300">ID:</span> <span class="w-24 md:w-32 text-purple-500 dark:text-purple-300">ID:</span>

View File

@ -0,0 +1,113 @@
{{ template "head.tmpl" . }}
<body class="h-screen bg-white dark:bg-gray-900">
{{ template "navbar.tmpl" . }}
<main class="grow min-w-[300px]">
<!-- <main class="grow"> -->
<!-- <div class="container mx-auto place-items-center px-8 md:px-12 lg:px-14"> -->
<!-- {{ if and .Data .Data.flash }} -->
<!-- <h2 class="text-xl text-pink-600 dark:text-pink-500 capitalize py-2"> -->
<!-- {{- .Data.flash -}} -->
<!-- </h2> -->
<!-- {{- end}} -->
<!-- <div class="flex justify-between place-items-center"> -->
<!-- <h1 class="text-xl font-bold text-fuchsia-600 dark:text-fuchsia-400 capitalize py-2"> -->
<!-- Edit user -->
<!-- </h1> -->
<!-- </div> -->
<!-- </div> -->
<section class="bg-white dark:bg-gray-900">
<div class="container mx-auto md:mx-4 px-6 sm:px-0:py-0 md:py-16 lg:py-32">
<div class="lg:flex">
<div class="lg:w-1/2">
<h1 class="mt-4 text-2xl font-medium text-fuchsia-600 dark:text-fuchsia-400 capitalize lg:text-3xl">
Edit user
</h1>
{{ if and .Data .Data.flash }}
<h2 class="text-xl text-pink-600 dark:text-pink-500 py-2">
{{- .Data.flash -}}
</h2>
{{- end }}
</div>
<div class="mt-8 lg:w-1/2 lg:mt-0">
{{if and .Data .Data.user -}}
<div class="flex justify-end gap-2 lg:gap-0 place-items-center">
<a href="/manage/users/{{.Data.user.ID}}" class="w-auto py-1 mt-2 mb-4 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
Back to detail
</a>
<a href="/manage/users" class="w-auto py-1 mt-2 mb-4 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-4 hover:underline dark:text-blue-400">
⏎ All users
</a>
</div>
{{end}}
<form method="post" action="/manage/users/{{.Data.user.ID}}/update" class="w-full lg:max-w-xl">
<input type="hidden" name="_csrf" value="{{- .CSRF -}}">
<div class="relative flex items-center">
<span class="absolute" role="img" aria-label="person outline icon for username">
{{ template "svg-user.tmpl" }}
</span>
<input name="username" type="text" {{if and .Data.user .Data.user.Username}}value="{{.Data.user.Username}}"{{end}} placeholder="Username" required class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
</div>
<div class="relative flex items-center mt-4">
<span class="absolute" role="img" aria-label="email icon">
{{ template "svg-email.tmpl" }}
</span>
<input name="email" type="email" {{if and .Data.user .Data.user.Email}}value="{{.Data.user.Email}}"{{end}} placeholder="Email address" required class="peer block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
<p class="mt-2 mx-4 peer-placeholder-shown:collapse peer-valid:hidden peer-invalid:block text-pink-600 text-sm">
Please provide a valid email address.
</p>
</div>
<div class="relative flex items-center mt-4">
<span class="absolute" role="img" aria-label="password lock icon">
{{ template "svg-password.tmpl" }}
</span>
<input name="password" type="password" {{if and .Data.user .Data.user.Password}}value="{{.Data.user.Password}}"{{end}} placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
</div>
<div class="relative flex items-center mt-4">
<span class="absolute" role="img" aria-label="password lock icon">
{{ template "svg-password.tmpl" }}
</span>
<input name="repeatPassword" type="password" {{if and .Data.user .Data.user.Password}}value="{{.Data.user.Password}}"{{end}} placeholder="Repeat Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
</div>
<div class="flex pt-2 px-2 items-center justify-center gap-6">
<div class="mb-1 block min-h-3">
<input
type="checkbox"
value="true"
id="isAdmin"
name="isAdmin"
{{- if and .Data.user .Data.user.IsAdmin}}checked{{ end -}}
/>
<label
class="inline-block pl-1 hover:cursor-pointer text-gray-700 dark:text-gray-300"
for="isAdmin">
Is admin?
</label>
</div>
<div class="mb-1 block min-h-3">
<input
type="checkbox"
value="true"
id="isActive"
name="isActive"
{{- if and .Data.user .Data.user.IsActive}}checked{{ end -}}
/>
<label
class="inline-block pl-1 hover:cursor-pointer text-gray-700 dark:text-gray-300"
for="isActive">
Is active?
</label>
</div>
</div>
<div class="mt-8 md:flex md:items-center">
<button class="w-full px-6 py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/2 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
Update
</button>
</div>
</form>
</div>
</div>
</div>
</section>
</main>
{{ template "footer.tmpl" . }}

View File

@ -25,11 +25,11 @@
</li> </li>
{{ if and .User .User.IsLoggedIn }} {{ if and .User .User.IsLoggedIn }}
<li> <li>
{{ if or (pageIs .Current "manage-users") (pageIs .Current "manage-users-new") (pageIs .Current "manage-users-user-details") }} {{- if or (pageIs .Current "manage-users") (pageIs .Current "manage-users-new") (pageIs .Current "manage-users-user-details") (pageIs .Current "manage-users-edit-user") -}}
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page"> <a href="/manage/users" class="block py-2 pl-3 pr-4 text-white bg-blue-500 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-500 md:dark:bg-transparent" aria-current="page">
{{ else }} {{else}}
<a href="/manage/users" class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"> <a href="/manage/users" class="block py-2 pl-3 pr-4 text-gray-900 rounded hover:bg-gray-300 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
{{ end }} {{end}}
User management User management
</a> </a>
</li> </li>