feat(admin): handle user deletion
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
db9629a618
commit
f9277d3b32
@ -71,7 +71,7 @@ const (
|
|||||||
type requestAdminUpdateUser struct {
|
type requestAdminUpdateUser struct {
|
||||||
User string `form:"user"`
|
User string `form:"user"`
|
||||||
Password *string `form:"password"`
|
Password *string `form:"password"`
|
||||||
IsAdmin *string `form:"is_admin"`
|
IsAdmin *bool `form:"is_admin"`
|
||||||
Operation operationType `form:"operation"`
|
Operation operationType `form:"operation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,21 +268,27 @@ func (api *API) appGetAdminUsers(c *gin.Context) {
|
|||||||
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
||||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||||
|
|
||||||
var rAdminUserUpdate requestAdminUpdateUser
|
var rUpdate requestAdminUpdateUser
|
||||||
if err := c.ShouldBind(&rAdminUserUpdate); err != nil {
|
if err := c.ShouldBind(&rUpdate); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid user update")
|
appErrorPage(c, http.StatusNotFound, "Invalid user parameters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Username
|
||||||
|
if rUpdate.User == "" {
|
||||||
|
appErrorPage(c, http.StatusInternalServerError, "User cannot be empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
switch rAdminUserUpdate.Operation {
|
switch rUpdate.Operation {
|
||||||
case opCreate:
|
case opCreate:
|
||||||
err = api.createUser(rAdminUserUpdate)
|
err = api.createUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||||
case opUpdate:
|
case opUpdate:
|
||||||
err = api.updateUser(rAdminUserUpdate)
|
err = api.updateUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||||
case opDelete:
|
case opDelete:
|
||||||
err = api.deleteUser(rAdminUserUpdate)
|
err = api.deleteUser(rUpdate.User)
|
||||||
default:
|
default:
|
||||||
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
||||||
return
|
return
|
||||||
@ -611,14 +617,6 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
}
|
}
|
||||||
defer backupFile.Close()
|
defer backupFile.Close()
|
||||||
|
|
||||||
// Vacuum DB
|
|
||||||
_, err = api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to vacuum DB: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save Backup File
|
// Save Backup File
|
||||||
w := bufio.NewWriter(backupFile)
|
w := bufio.NewWriter(backupFile)
|
||||||
err = api.createBackup(w, []string{"covers", "documents"})
|
err = api.createBackup(w, []string{"covers", "documents"})
|
||||||
@ -712,8 +710,13 @@ func (api *API) removeData() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) createBackup(w io.Writer, directories []string) error {
|
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||||
ar := zip.NewWriter(w)
|
// Vacuum DB
|
||||||
|
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to vacuum database")
|
||||||
|
}
|
||||||
|
|
||||||
|
ar := zip.NewWriter(w)
|
||||||
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -781,29 +784,43 @@ func (api *API) createBackup(w io.Writer, directories []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) createUser(createRequest requestAdminUpdateUser) error {
|
func (api *API) isLastAdmin(userID string) (bool, error) {
|
||||||
// Validate Necessary Parameters
|
allUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||||
if createRequest.User == "" {
|
if err != nil {
|
||||||
return fmt.Errorf("username can't be empty")
|
return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||||
}
|
}
|
||||||
if createRequest.Password == nil || *createRequest.Password == "" {
|
|
||||||
|
hasAdmin := false
|
||||||
|
for _, user := range allUsers {
|
||||||
|
if user.Admin && user.ID != userID {
|
||||||
|
hasAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !hasAdmin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) createUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||||
|
// Validate Necessary Parameters
|
||||||
|
if rawPassword == nil || *rawPassword == "" {
|
||||||
return fmt.Errorf("password can't be empty")
|
return fmt.Errorf("password can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base Params
|
// Base Params
|
||||||
createParams := database.CreateUserParams{
|
createParams := database.CreateUserParams{
|
||||||
ID: createRequest.User,
|
ID: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Admin (Explicit or False)
|
// Handle Admin (Explicit or False)
|
||||||
if createRequest.IsAdmin != nil {
|
if isAdmin != nil {
|
||||||
createParams.Admin = *createRequest.IsAdmin == "true"
|
createParams.Admin = *isAdmin
|
||||||
} else {
|
} else {
|
||||||
createParams.Admin = false
|
createParams.Admin = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Password
|
// Parse Password
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*createRequest.Password)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create hashed password")
|
return fmt.Errorf("unable to create hashed password")
|
||||||
@ -830,25 +847,22 @@ func (api *API) createUser(createRequest requestAdminUpdateUser) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) updateUser(updateRequest requestAdminUpdateUser) error {
|
func (api *API) updateUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||||
// Validate Necessary Parameters
|
// Validate Necessary Parameters
|
||||||
if updateRequest.User == "" {
|
if rawPassword == nil && isAdmin == nil {
|
||||||
return fmt.Errorf("username can't be empty")
|
|
||||||
}
|
|
||||||
if updateRequest.Password == nil && updateRequest.IsAdmin == nil {
|
|
||||||
return fmt.Errorf("nothing to update")
|
return fmt.Errorf("nothing to update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base Params
|
// Base Params
|
||||||
updateParams := database.UpdateUserParams{
|
updateParams := database.UpdateUserParams{
|
||||||
UserID: updateRequest.User,
|
UserID: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Admin (Update or Existing)
|
// Handle Admin (Update or Existing)
|
||||||
if updateRequest.IsAdmin != nil {
|
if isAdmin != nil {
|
||||||
updateParams.Admin = *updateRequest.IsAdmin == "true"
|
updateParams.Admin = *isAdmin
|
||||||
} else {
|
} else {
|
||||||
user, err := api.db.Queries.GetUser(api.db.Ctx, updateRequest.User)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||||
}
|
}
|
||||||
@ -856,20 +870,20 @@ func (api *API) updateUser(updateRequest requestAdminUpdateUser) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Admins
|
// Check Admins
|
||||||
if isLast, err := api.isLastAdmin(updateRequest.User); err != nil {
|
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if isLast {
|
} else if isLast {
|
||||||
return fmt.Errorf("unable to demote %s - last admin", updateRequest.User)
|
return fmt.Errorf("unable to demote %s - last admin", user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Password
|
// Handle Password
|
||||||
if updateRequest.Password != nil {
|
if rawPassword != nil {
|
||||||
if *updateRequest.Password == "" {
|
if *rawPassword == "" {
|
||||||
return fmt.Errorf("password can't be empty")
|
return fmt.Errorf("password can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Password
|
// Parse Password
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*updateRequest.Password)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create hashed password")
|
return fmt.Errorf("unable to create hashed password")
|
||||||
@ -894,32 +908,34 @@ func (api *API) updateUser(updateRequest requestAdminUpdateUser) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) deleteUser(updateRequest requestAdminUpdateUser) error {
|
func (api *API) deleteUser(user string) error {
|
||||||
// Check Admins
|
// Check Admins
|
||||||
if isLast, err := api.isLastAdmin(updateRequest.User); err != nil {
|
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if isLast {
|
} else if isLast {
|
||||||
return fmt.Errorf("unable to demote %s - last admin", updateRequest.User)
|
return fmt.Errorf("unable to delete %s - last admin", user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - Implementation
|
// Create Backup File
|
||||||
|
backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
|
||||||
return errors.New("unimplemented")
|
backupFile, err := os.Create(backupFilePath)
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) isLastAdmin(userID string) (bool, error) {
|
|
||||||
allUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
|
return err
|
||||||
|
}
|
||||||
|
defer backupFile.Close()
|
||||||
|
|
||||||
|
// Save Backup File (DB Only)
|
||||||
|
w := bufio.NewWriter(backupFile)
|
||||||
|
err = api.createBackup(w, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAdmin := false
|
// Delete User
|
||||||
for _, user := range allUsers {
|
_, err = api.db.Queries.DeleteUser(api.db.Ctx, user)
|
||||||
if user.Admin && user.ID != userID {
|
if err != nil {
|
||||||
hasAdmin = true
|
return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err))
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !hasAdmin, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -30,6 +30,9 @@ INSERT INTO users (id, pass, auth_hash, admin)
|
|||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- name: DeleteUser :execrows
|
||||||
|
DELETE FROM users WHERE id = $id;
|
||||||
|
|
||||||
-- name: DeleteDocument :execrows
|
-- name: DeleteDocument :execrows
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET
|
SET
|
||||||
|
@ -153,6 +153,18 @@ func (q *Queries) DeleteDocument(ctx context.Context, id string) (int64, error)
|
|||||||
return result.RowsAffected()
|
return result.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteUser = `-- name: DeleteUser :execrows
|
||||||
|
DELETE FROM users WHERE id = ?1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteUser(ctx context.Context, id string) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteUser, id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
const getActivity = `-- name: GetActivity :many
|
const getActivity = `-- name: GetActivity :many
|
||||||
WITH filtered_activity AS (
|
WITH filtered_activity AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -189,3 +189,11 @@ UPDATE documents
|
|||||||
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||||
WHERE id = old.id;
|
WHERE id = old.id;
|
||||||
END;
|
END;
|
||||||
|
|
||||||
|
-- Delete User
|
||||||
|
CREATE TRIGGER IF NOT EXISTS user_deleted
|
||||||
|
BEFORE DELETE ON users BEGIN
|
||||||
|
DELETE FROM activity WHERE activity.user_id=OLD.id;
|
||||||
|
DELETE FROM devices WHERE devices.user_id=OLD.id;
|
||||||
|
DELETE FROM document_progress WHERE document_progress.user_id=OLD.id;
|
||||||
|
END;
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
type="submit">Create</button>
|
type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-full overflow-hidden rounded shadow">
|
<div class="min-w-full overflow-scroll rounded shadow">
|
||||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
@ -50,12 +50,27 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $user := .Data }}
|
{{ range $user := .Data }}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer">
|
<!-- User Deletion -->
|
||||||
{{ template "svg/delete" }}
|
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
|
||||||
|
<label for="delete-{{ $user.ID }}-button" class="cursor-pointer">{{ template "svg/delete" }}</label>
|
||||||
|
<input type="checkbox"
|
||||||
|
id="delete-{{ $user.ID }}-button"
|
||||||
|
class="hidden css-button" />
|
||||||
|
<div class="absolute z-30 top-1.5 left-10 p-1.5 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||||
|
<form method="POST"
|
||||||
|
action="./users"
|
||||||
|
class="text-black dark:text-white text-sm w-40">
|
||||||
|
<input type="hidden" id="operation" name="operation" value="DELETE" />
|
||||||
|
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
||||||
|
{{ template "component/button" (dict "Title" (printf "Delete (%s)" $user.ID )) }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<!-- User ID -->
|
||||||
<td class="p-3 border-b border-gray-200">
|
<td class="p-3 border-b border-gray-200">
|
||||||
<p>{{ $user.ID }}</p>
|
<p>{{ $user.ID }}</p>
|
||||||
</td>
|
</td>
|
||||||
|
<!-- User Password Change -->
|
||||||
<td class="border-b border-gray-200 relative px-3">
|
<td class="border-b border-gray-200 relative px-3">
|
||||||
<label for="edit-{{ $user.ID }}-button" class="cursor-pointer">
|
<label for="edit-{{ $user.ID }}-button" class="cursor-pointer">
|
||||||
<span class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
<span class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||||
@ -64,7 +79,7 @@
|
|||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="edit-{{ $user.ID }}-button"
|
id="edit-{{ $user.ID }}-button"
|
||||||
class="hidden css-button" />
|
class="hidden css-button" />
|
||||||
<div class="absolute z-30 -bottom-1.5 left-16 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
<div class="absolute z-30 top-1 left-16 ml-2 p-1.5 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="./users"
|
action="./users"
|
||||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
class="flex flex gap-2 text-black dark:text-white text-sm">
|
||||||
@ -73,13 +88,14 @@
|
|||||||
<input type="password"
|
<input type="password"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
placeholder="{{ printf "Password (%s)" $user.ID }}"
|
||||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" />
|
class="p-1.5 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" />
|
||||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||||
type="submit">Change</button>
|
type="submit">Change</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<!-- User Role -->
|
||||||
<td class="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
<td class="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
||||||
<!-- Set Admin & User Styles -->
|
<!-- Set Admin & User Styles -->
|
||||||
{{ $adminStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
{{ $adminStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
||||||
|
Loading…
Reference in New Issue
Block a user