Compare commits

..

2 Commits

Author SHA1 Message Date
7991c80176 Allow admin to create user
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 5m22s
2026-03-10 12:32:59 +00:00
bad44a6ddd Separate admin protected endpoints from non-admin endpoints 2026-03-10 12:32:47 +00:00
5 changed files with 62 additions and 46 deletions

View File

@@ -5,6 +5,7 @@ import (
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -26,6 +27,28 @@ func (store *Store) AuthMiddlewear(ctx *gin.Context) {
ctx.Next() ctx.Next()
} }
func (store *Store) AdminMiddleware(ctx *gin.Context) {
claims, exists := ctx.Get("userClaims")
if !exists {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
mapClaims, ok := claims.(*jwt.MapClaims)
if !ok {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid claims"})
return
}
admin, ok := (*mapClaims)["admin"].(bool)
if !ok || !admin {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin access required"})
return
}
ctx.Next()
}
func (store *Store) CheckToken(ctx *gin.Context) { func (store *Store) CheckToken(ctx *gin.Context) {
access_token, err := ctx.Cookie("access_token") access_token, err := ctx.Cookie("access_token")
if err != nil { if err != nil {

View File

@@ -67,11 +67,6 @@ func (store *Store) CreatePost(ctx *gin.Context) {
} }
userID := uint(userIDF) userID := uint(userIDF)
if !(*claims)["admin"].(bool) {
ctx.JSON(http.StatusForbidden, gin.H{"error": "you are not admin :("})
return
}
// Create post // Create post
post := models.Post{Title: input.Title, Content: input.Content, AuthorID: userID} post := models.Post{Title: input.Title, Content: input.Content, AuthorID: userID}
tx := store.DB.Create(&post) tx := store.DB.Create(&post)

View File

@@ -19,21 +19,6 @@ type SetAdminInput struct {
} }
func (store *Store) CreateUser(ctx *gin.Context) { func (store *Store) CreateUser(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
}
var input UserCredentials var input UserCredentials
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil { if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
ctx.JSON(http.StatusBadRequest, err.Error()) ctx.JSON(http.StatusBadRequest, err.Error())
@@ -116,11 +101,6 @@ func (store *Store) SetUserAdmin(ctx *gin.Context) {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"}) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"})
return return
} }
if !(*claims)["admin"].(bool) {
ctx.JSON(http.StatusForbidden, gin.H{"error": "admin access required"})
return
}
callerIDF, ok := (*claims)["id"].(float64) callerIDF, ok := (*claims)["id"].(float64)
if !ok { if !ok {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"}) ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})

View File

@@ -71,33 +71,34 @@ func main() {
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes} store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes}
protected := r.Group("/", store.AuthMiddlewear) protected := r.Group("/", store.AuthMiddlewear)
admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware)
// FAVORITES // FAVORITES
r.GET("/favorites", store.GetFavorites) r.GET("/favorites", store.GetFavorites)
protected.POST("/favorites", store.CreateFavorite) admin.POST("/favorites", store.CreateFavorite)
// ROWING // ROWING
r.GET("/rowing", store.GetRowing) r.GET("/rowing", store.GetRowing)
protected.POST("/rowing", store.CreateRowing) admin.POST("/rowing", store.CreateRowing)
// ACTIVITIES // ACTIVITIES
r.GET("/activity", store.GetActivity) r.GET("/activity", store.GetActivity)
protected.POST("/activity", store.CreateActivity) admin.POST("/activity", store.CreateActivity)
// POSTS // POSTS
r.GET("/posts", store.GetPosts) r.GET("/posts", store.GetPosts)
protected.POST("/posts", store.CreatePost) admin.POST("/posts", store.CreatePost)
r.GET("/posts/:id", store.GetPost) r.GET("/posts/:id", store.GetPost)
protected.PUT("/posts/:id", store.UpdatePost) admin.PUT("/posts/:id", store.UpdatePost)
protected.DELETE("/posts/:id", store.DeletePost) admin.DELETE("/posts/:id", store.DeletePost)
// USERS // USERS
r.GET("/user/:id", store.GetUser) r.GET("/user/:id", store.GetUser)
protected.PUT("/user/:id", store.UpdateUser) admin.PUT("/user/:id", store.UpdateUser)
protected.DELETE("/user/:id", store.DeleteUser) admin.DELETE("/user/:id", store.DeleteUser)
r.GET("/user", store.GetUsers) r.GET("/user", store.GetUsers)
protected.POST("/user", store.CreateUser) admin.POST("/user", store.CreateUser)
protected.PATCH("/user/:id/admin", store.SetUserAdmin) admin.PATCH("/user/:id/admin", store.SetUserAdmin)
// AUTH // AUTH
r.POST("/auth/login", store.Login) r.POST("/auth/login", store.Login)

View File

@@ -1,28 +1,45 @@
<script setup> <script setup>
import Button from "@/components/input/Button.vue"; import Button from "@/components/input/Button.vue";
import { ref, onMounted, computed } from "vue"; import { ref } from "vue";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import axios from "axios";
const auth = useAuthStore(); const auth = useAuthStore();
const username = ref(""); const username = ref("");
const password = ref(""); const password = ref("");
const message = ref("");
const error = ref("");
function handleLogin() { async function handleCreate() {
auth.createUser(username.value, password.value); message.value = "";
error.value = "";
try {
const res = await axios.post("/api/user", {
username: username.value,
password: password.value,
});
message.value = `User "${res.data.username}" created successfully.`;
username.value = "";
password.value = "";
} catch (err) {
error.value = err.response?.data?.message || "Failed to create user.";
}
} }
</script> </script>
<template> <template>
<div v-if="auth.loggedIn" class="flex flex-col"> <div v-if="auth.loggedIn && auth.user.admin" class="flex flex-col">
<h1>Logged in</h1>
<p>{{ auth.user.id }}</p>
<p>{{ auth.user.username }}</p>
<p>{{ auth.user.admin }}</p>
</div>
<div v-else class="flex flex-col">
<h1>Create User</h1> <h1>Create User</h1>
<p v-if="message" class="text-green-500">{{ message }}</p>
<p v-if="error" class="text-red-500">{{ error }}</p>
<input type="text" v-model="username" placeholder="Username" /> <input type="text" v-model="username" placeholder="Username" />
<input type="password" v-model="password" placeholder="Password" /> <input type="password" v-model="password" placeholder="Password" />
<Button @click="handleLogin">Create Account</Button> <Button @click="handleCreate">Create Account</Button>
</div>
<div v-else-if="auth.loggedIn" class="flex flex-col">
<p>You do not have permission to create users.</p>
</div>
<div v-else class="flex flex-col">
<p>You must be logged in as an admin to create users.</p>
</div> </div>
</template> </template>