Remove REST handlers superseded by GraphQL resolvers
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Deleted handle_activity.go, handle_favorites.go, handle_post.go, and handle_user.go — all logic already exists in schema.resolvers.go. Removed corresponding REST routes from main.go. Moved UserCredentials struct (used by Login handler) into handle_auth.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,43 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"adam-french.co.uk/backend/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateActivityInput struct {
|
|
||||||
Type string `json:"type" binding:"required"`
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Link *string `json:"link"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetActivity(ctx *gin.Context) {
|
|
||||||
var activitys []models.Activity
|
|
||||||
if err := store.DB.Order("Created_At DESC").Find(&activitys).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, activitys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) CreateActivity(ctx *gin.Context) {
|
|
||||||
var input CreateActivityInput
|
|
||||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activity := models.Activity{Type: input.Type, Name: input.Name, Link: input.Link}
|
|
||||||
tx := store.DB.Create(&activity)
|
|
||||||
if tx.Error != nil {
|
|
||||||
log.Println(tx.Error)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusCreated, activity)
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,11 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserCredentials struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
func (store *Store) AuthMiddlewear(ctx *gin.Context) {
|
func (store *Store) AuthMiddlewear(ctx *gin.Context) {
|
||||||
access_token, err := ctx.Cookie("access_token")
|
access_token, err := ctx.Cookie("access_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"adam-french.co.uk/backend/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateFavoriteInput struct {
|
|
||||||
Type string `json:"type" binding:"required"`
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Link *string `json:"link"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetFavorites(ctx *gin.Context) {
|
|
||||||
var favorites []models.Favorite
|
|
||||||
if err := store.DB.Order("Created_At DESC").Find(&favorites).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, favorites)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) CreateFavorite(ctx *gin.Context) {
|
|
||||||
var input CreateFavoriteInput
|
|
||||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
favorite := models.Favorite{Type: input.Type, Name: input.Name, Link: input.Link}
|
|
||||||
tx := store.DB.Create(&favorite)
|
|
||||||
if tx.Error != nil {
|
|
||||||
log.Println(tx.Error)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusCreated, favorite)
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"adam-french.co.uk/backend/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreatePostInput struct {
|
|
||||||
Title string `json:"title" binding:"required"`
|
|
||||||
Content string `json:"content" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetPosts(ctx *gin.Context) {
|
|
||||||
var posts []models.Post
|
|
||||||
if err := store.DB.Preload("Author").Order("Created_At DESC").Find(&posts).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, posts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetPost(ctx *gin.Context) {
|
|
||||||
postIDStr := ctx.Param("id")
|
|
||||||
|
|
||||||
postID, err := strconv.ParseUint(postIDStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, "invalid id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
post := models.Post{ID: uint(postID)}
|
|
||||||
if err := store.DB.First(&post).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) CreatePost(ctx *gin.Context) {
|
|
||||||
var input CreatePostInput
|
|
||||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claimsVal, ok := ctx.Get("userClaims")
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userIDF, ok := (*claims)["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := uint(userIDF)
|
|
||||||
|
|
||||||
// Create post
|
|
||||||
post := models.Post{Title: input.Title, Content: input.Content, AuthorID: userID}
|
|
||||||
tx := store.DB.Create(&post)
|
|
||||||
if tx.Error != nil {
|
|
||||||
log.Println(tx.Error)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusCreated, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) UpdatePost(ctx *gin.Context) {
|
|
||||||
postID := ctx.Param("id")
|
|
||||||
var post models.Post
|
|
||||||
if err := store.DB.First(&post, postID).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claimsVal, ok := ctx.Get("userClaims")
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userIDF, ok := (*claims)["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := uint(userIDF)
|
|
||||||
|
|
||||||
if !(userID == post.AuthorID) {
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var input CreatePostInput
|
|
||||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
post.Title = input.Title
|
|
||||||
post.Content = input.Content
|
|
||||||
tx := store.DB.Save(&post)
|
|
||||||
if tx.Error != nil {
|
|
||||||
log.Println(tx.Error)
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) DeletePost(ctx *gin.Context) {
|
|
||||||
postID := ctx.Param("id")
|
|
||||||
var post models.Post
|
|
||||||
if err := store.DB.First(&post, postID).Error; err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claimsVal, ok := ctx.Get("userClaims")
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userIDF, ok := (*claims)["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := uint(userIDF)
|
|
||||||
|
|
||||||
if !(userID == post.AuthorID) {
|
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
store.DB.Delete(&post)
|
|
||||||
ctx.JSON(http.StatusOK, post)
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"adam-french.co.uk/backend/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserCredentials struct {
|
|
||||||
Username string `json:"username" binding:"required"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetAdminInput struct {
|
|
||||||
Admin *bool `json:"admin" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) CreateUser(ctx *gin.Context) {
|
|
||||||
var input UserCredentials
|
|
||||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
|
||||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := models.User{Username: input.Username, Password: hashedPassword}
|
|
||||||
tx := store.DB.Create(&user)
|
|
||||||
if tx.Error != nil {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetUser(ctx *gin.Context) {
|
|
||||||
userID := ctx.Param("id")
|
|
||||||
var user models.User
|
|
||||||
if err := store.DB.First(&user, userID).Error; err != nil {
|
|
||||||
ctx.JSON(http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) GetUsers(ctx *gin.Context) {
|
|
||||||
var users []models.User
|
|
||||||
if err := store.DB.Find(&users).Error; err != nil {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, users)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) UpdateUser(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
|
|
||||||
}
|
|
||||||
|
|
||||||
userIDF, ok := (*claims)["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := uint(userIDF)
|
|
||||||
|
|
||||||
var user models.User
|
|
||||||
if err := store.DB.First(&user, userID).Error; err != nil {
|
|
||||||
ctx.JSON(http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
userIDF, ok := (*claims)["id"].(float64)
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := uint(userIDF)
|
|
||||||
|
|
||||||
var user models.User
|
|
||||||
if err := store.DB.First(&user, userID).Error; err != nil {
|
|
||||||
ctx.JSON(http.StatusNotFound, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := store.DB.Delete(&user)
|
|
||||||
if tx.Error != nil {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetSameSite(http.SameSiteLaxMode)
|
|
||||||
ctx.SetCookie(
|
|
||||||
"access_token",
|
|
||||||
"",
|
|
||||||
-1,
|
|
||||||
"/",
|
|
||||||
store.Auth.Config.Domain,
|
|
||||||
true, true,
|
|
||||||
)
|
|
||||||
ctx.SetCookie(
|
|
||||||
"refresh_token",
|
|
||||||
"",
|
|
||||||
-1,
|
|
||||||
"/",
|
|
||||||
store.Auth.Config.Domain,
|
|
||||||
true, true,
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, user)
|
|
||||||
}
|
|
||||||
@@ -90,33 +90,10 @@ func main() {
|
|||||||
protected := r.Group("/", store.AuthMiddlewear)
|
protected := r.Group("/", store.AuthMiddlewear)
|
||||||
admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware)
|
admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware)
|
||||||
|
|
||||||
// FAVORITES
|
|
||||||
r.GET("/favorites", store.GetFavorites)
|
|
||||||
admin.POST("/favorites", store.CreateFavorite)
|
|
||||||
|
|
||||||
// ROWING
|
// ROWING
|
||||||
r.GET("/rowing", store.GetRowing)
|
r.GET("/rowing", store.GetRowing)
|
||||||
admin.POST("/rowing", store.CreateRowing)
|
admin.POST("/rowing", store.CreateRowing)
|
||||||
|
|
||||||
// ACTIVITIES
|
|
||||||
r.GET("/activity", store.GetActivity)
|
|
||||||
admin.POST("/activity", store.CreateActivity)
|
|
||||||
|
|
||||||
// POSTS
|
|
||||||
r.GET("/posts", store.GetPosts)
|
|
||||||
admin.POST("/posts", store.CreatePost)
|
|
||||||
r.GET("/posts/:id", store.GetPost)
|
|
||||||
admin.PUT("/posts/:id", store.UpdatePost)
|
|
||||||
admin.DELETE("/posts/:id", store.DeletePost)
|
|
||||||
|
|
||||||
// USERS
|
|
||||||
r.GET("/user/:id", store.GetUser)
|
|
||||||
admin.PUT("/user/:id", store.UpdateUser)
|
|
||||||
admin.DELETE("/user/:id", store.DeleteUser)
|
|
||||||
r.GET("/user", store.GetUsers)
|
|
||||||
admin.POST("/user", store.CreateUser)
|
|
||||||
admin.PATCH("/user/:id/admin", store.SetUserAdmin)
|
|
||||||
|
|
||||||
// AUTH
|
// AUTH
|
||||||
r.POST("/auth/login", store.Login)
|
r.POST("/auth/login", store.Login)
|
||||||
r.POST("/auth/refresh", store.RefreshToken)
|
r.POST("/auth/refresh", store.RefreshToken)
|
||||||
|
|||||||
Reference in New Issue
Block a user