Add promote / demote user to admin and reintroduce create user dashboard

This commit is contained in:
2026-03-10 12:18:24 +00:00
parent c7dbf5b778
commit cb326ff8bf
5 changed files with 114 additions and 0 deletions

View File

@@ -14,6 +14,10 @@ type UserCredentials struct {
Password string `json:"password" binding:"required"` Password string `json:"password" binding:"required"`
} }
type SetAdminInput struct {
Admin *bool `json:"admin" binding:"required"`
}
func (store *Store) CreateUser(ctx *gin.Context) { func (store *Store) CreateUser(ctx *gin.Context) {
claimsVal, ok := ctx.Get("userClaims") claimsVal, ok := ctx.Get("userClaims")
if !ok { if !ok {
@@ -101,6 +105,57 @@ func (store *Store) UpdateUser(ctx *gin.Context) {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "will be implemented"}) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "will be implemented"})
} }
func (store *Store) SetUserAdmin(ctx *gin.Context) {
claimsVal, ok := ctx.Get("userClaims")
if !ok {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user claims could not be found"})
return
}
claims, ok := claimsVal.(*jwt.MapClaims)
if !ok {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"})
return
}
if !(*claims)["admin"].(bool) {
ctx.JSON(http.StatusForbidden, gin.H{"error": "admin access required"})
return
}
callerIDF, ok := (*claims)["id"].(float64)
if !ok {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
return
}
callerID := uint(callerIDF)
targetID := ctx.Param("id")
var input SetAdminInput
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := store.DB.First(&user, targetID).Error; err != nil {
ctx.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if user.ID == callerID {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "cannot change your own admin status"})
return
}
user.Admin = *input.Admin
if err := store.DB.Save(&user).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, user)
}
func (store *Store) DeleteUser(ctx *gin.Context) { func (store *Store) DeleteUser(ctx *gin.Context) {
claimsVal, ok := ctx.Get("userClaims") claimsVal, ok := ctx.Get("userClaims")
if !ok { if !ok {

View File

@@ -97,6 +97,7 @@ func main() {
protected.DELETE("/user/:id", store.DeleteUser) protected.DELETE("/user/:id", store.DeleteUser)
r.GET("/user", store.GetUsers) r.GET("/user", store.GetUsers)
protected.POST("/user", store.CreateUser) protected.POST("/user", store.CreateUser)
protected.PATCH("/user/:id/admin", store.SetUserAdmin)
// AUTH // AUTH
r.POST("/auth/login", store.Login) r.POST("/auth/login", store.Login)

View File

@@ -59,6 +59,16 @@ export const useAuthStore = defineStore("auth", () => {
} }
} }
async function setUserAdmin(userId, admin) {
try {
const res = await axios.patch(`/api/user/${userId}/admin`, { admin });
return res.data;
} catch (err) {
console.error(err);
throw err;
}
}
return { return {
user, user,
@@ -69,5 +79,6 @@ export const useAuthStore = defineStore("auth", () => {
refreshToken, refreshToken,
logOut, logOut,
createUser, createUser,
setUserAdmin,
}; };
}); });

View File

@@ -8,6 +8,7 @@ import CreatePost from "./CreatePost.vue";
import CreateFavorite from "./CreateFavorite.vue"; import CreateFavorite from "./CreateFavorite.vue";
import CreateActivity from "./CreateActivity.vue"; import CreateActivity from "./CreateActivity.vue";
import CreateRowing from "./CreateRowing.vue"; import CreateRowing from "./CreateRowing.vue";
import ManageUsers from "./ManageUsers.vue";
const auth = useAuthStore(); const auth = useAuthStore();
</script> </script>
@@ -21,6 +22,7 @@ const auth = useAuthStore();
<CreateFavorite class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateFavorite class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
<CreateActivity class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateActivity class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
<CreateRowing class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateRowing class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
<ManageUsers class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
</div> </div>
</main> </main>
</template> </template>

View File

@@ -0,0 +1,45 @@
<script setup>
import Button from "@/components/input/Button.vue";
import { ref, onMounted } from "vue";
import { useAuthStore } from "@/stores/auth";
import axios from "axios";
const auth = useAuthStore();
const users = ref([]);
async function fetchUsers() {
try {
const res = await axios.get("/api/user");
users.value = res.data;
} catch (err) {
console.error(err);
}
}
async function toggleAdmin(user) {
try {
const res = await auth.setUserAdmin(user.id, !user.admin);
user.admin = res.admin;
} catch (err) {
console.error(err);
}
}
onMounted(fetchUsers);
</script>
<template>
<div class="flex flex-col">
<h1>Manage Users</h1>
<div v-for="user in users" :key="user.id" class="flex flex-row items-center gap-2">
<span>{{ user.username }}</span>
<span v-if="user.admin">(admin)</span>
<Button
v-if="user.id !== auth.user.id"
@click="toggleAdmin(user)"
>
{{ user.admin ? "Demote" : "Promote" }}
</Button>
</div>
</div>
</template>