326 lines
8.9 KiB
Go
Executable File
326 lines
8.9 KiB
Go
Executable File
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
db "git.nochill.in/nochill/hiling_go/db/sqlc"
|
|
"git.nochill.in/nochill/hiling_go/util"
|
|
"git.nochill.in/nochill/hiling_go/util/token"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type createUserRequest struct {
|
|
Username string `json:"username" binding:"required,alphanum"`
|
|
Password string `json:"password" binding:"required,min=7"`
|
|
}
|
|
|
|
type createUserResponse struct {
|
|
ID int32 `json:"id"`
|
|
Username string `json:"username"`
|
|
AvatarPicture string `json:"avatar_picture"` // avatar_url
|
|
BannedAt pgtype.Timestamp `json:"banned_at"`
|
|
BannedUntil pgtype.Timestamp `json:"banned_until"`
|
|
BanReason string `json:"ban_reason"`
|
|
IsPermaban bool `json:"is_permaban"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
IsCritics bool `json:"is_critics"`
|
|
IsVerified bool `json:"is_verified"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func (server *Server) createUser(ctx *gin.Context) {
|
|
var req createUserRequest
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
if err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
}
|
|
|
|
hashedPassword, err := util.HashPassword(req.Password)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
|
|
return
|
|
}
|
|
|
|
arg := db.CreateUserParams{
|
|
Username: req.Username,
|
|
Password: hashedPassword,
|
|
}
|
|
|
|
user, err := server.Store.CreateUser(ctx, arg)
|
|
if err != nil {
|
|
if db.ErrorCode(err) == db.UniqueViolation {
|
|
ctx.JSON(http.StatusConflict, ErrorResponse(err, "Username already used"))
|
|
}
|
|
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
|
|
return
|
|
}
|
|
|
|
accessToken, _, err := server.TokenMaker.CreateToken(
|
|
user.Username,
|
|
int(user.ID),
|
|
server.Config.TokenDuration,
|
|
)
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while creating token"))
|
|
return
|
|
}
|
|
|
|
refreshToken, refreshTokenPayload, err := server.TokenMaker.CreateToken(
|
|
user.Username,
|
|
int(user.ID),
|
|
server.Config.RefreshTokenDuration,
|
|
)
|
|
|
|
_, err = server.Store.CreateSession(ctx, db.CreateSessionParams{
|
|
Username: user.Username,
|
|
RefreshToken: refreshToken,
|
|
UserAgent: ctx.Request.UserAgent(),
|
|
ClientIp: ctx.ClientIP(),
|
|
IsBlocked: false,
|
|
ExpiresAt: pgtype.Timestamp{Valid: true, Time: refreshTokenPayload.ExpiredAt},
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while saving sessions"))
|
|
return
|
|
}
|
|
|
|
res := createUserResponse{
|
|
ID: user.ID,
|
|
Username: user.Username,
|
|
AvatarPicture: user.AvatarPicture.String,
|
|
BannedAt: pgtype.Timestamp{Valid: user.BannedAt.Valid, Time: user.BannedAt.Time},
|
|
BannedUntil: pgtype.Timestamp{Valid: user.BannedUntil.Valid, Time: user.BannedUntil.Time},
|
|
BanReason: user.BanReason.String,
|
|
IsPermaban: user.IsPermaban.Bool,
|
|
IsAdmin: user.IsAdmin.Bool,
|
|
IsCritics: user.IsCritics.Bool,
|
|
IsVerified: user.IsVerified.Bool,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
}
|
|
|
|
ctx.SetCookie(
|
|
"paseto",
|
|
accessToken,
|
|
int(server.Config.CookieDuration),
|
|
"/",
|
|
"localhost",
|
|
false,
|
|
false,
|
|
)
|
|
|
|
ctx.JSON(http.StatusOK, res)
|
|
}
|
|
|
|
func (server *Server) getUserStats(ctx *gin.Context) {
|
|
var scoreDistribution []map[string]int8
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
userStats, err := server.Store.GetUserStats(ctx, int32(authPayload.UserID))
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get user stats"))
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal(userStats.ScoresDistribution, &scoreDistribution)
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong whilet try to parse score distribution"))
|
|
return
|
|
}
|
|
|
|
var userReviews []map[string]any
|
|
if userStats.Reviews != nil {
|
|
err = json.Unmarshal(userStats.Reviews, &userReviews)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something wrong while try to parse user reviwes stats"))
|
|
return
|
|
}
|
|
} else {
|
|
userReviews = make([]map[string]any, 0)
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{
|
|
"reviews": userReviews,
|
|
"scores_distribution": scoreDistribution,
|
|
"user_stats": userStats,
|
|
})
|
|
}
|
|
|
|
type UpdateUserRequest struct {
|
|
About string `json:"about"`
|
|
Website string `json:"website"`
|
|
SocialMedia interface{} `json:"social_media"`
|
|
}
|
|
|
|
func (server *Server) updateUser(ctx *gin.Context) {
|
|
var req UpdateUserRequest
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
user, err := server.Store.UpdateUser(ctx, db.UpdateUserParams{
|
|
About: pgtype.Text{String: req.About, Valid: true},
|
|
SocialMedia: req.SocialMedia,
|
|
Website: pgtype.Text{String: req.Website, Valid: len(req.Website) > 0},
|
|
ID: int32(authPayload.UserID),
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user"))
|
|
return
|
|
}
|
|
|
|
//
|
|
// user.BannedAt = util.ParseNullDateTime()
|
|
|
|
ctx.JSON(http.StatusOK, user)
|
|
}
|
|
|
|
func (server *Server) updateUserAvatar(ctx *gin.Context) {
|
|
file, err := ctx.FormFile("file")
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to parse file"))
|
|
return
|
|
}
|
|
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
fileExt := filepath.Ext(file.Filename)
|
|
now := time.Now()
|
|
dir := fmt.Sprintf("public/upload/images/user/%d/avatar", authPayload.UserID)
|
|
osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt)
|
|
imgPath := fmt.Sprintf("%s/%s", dir, osFilename)
|
|
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
os.Mkdir(dir, 0775)
|
|
}
|
|
|
|
if err := ctx.SaveUploadedFile(file, imgPath); err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Error while try to save thumbnail image"))
|
|
return
|
|
}
|
|
|
|
url, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{
|
|
ID: int32(authPayload.UserID),
|
|
AvatarPicture: pgtype.Text{Valid: true, String: imgPath},
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save image to database"))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"image_url": url.String})
|
|
}
|
|
|
|
func (server *Server) removeAvatar(ctx *gin.Context) {
|
|
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
|
|
|
|
_, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{
|
|
AvatarPicture: pgtype.Text{String: "", Valid: false},
|
|
ID: int32(authPayload.UserID),
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user avatar"))
|
|
return
|
|
}
|
|
|
|
ctx.Writer.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (server *Server) login(ctx *gin.Context) {
|
|
var req createUserRequest
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
|
|
return
|
|
}
|
|
|
|
user, err := server.Store.GetUser(ctx, req.Username)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
ctx.JSON(http.StatusNotFound, ErrorResponse(err, "User not found"))
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong whlie try to get user"))
|
|
return
|
|
}
|
|
|
|
err = util.CheckPassword(req.Password, user.Password)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusUnauthorized, ErrorResponse(err, "Password not match"))
|
|
return
|
|
}
|
|
|
|
accessToken, _, err := server.TokenMaker.CreateToken(user.Username, int(user.ID), server.Config.TokenDuration)
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, "Something went wrong while try to create token")
|
|
return
|
|
}
|
|
|
|
refreshToken, refreshTokenPayload, err := server.TokenMaker.CreateToken(
|
|
user.Username,
|
|
int(user.ID),
|
|
server.Config.RefreshTokenDuration,
|
|
)
|
|
|
|
_, err = server.Store.CreateSession(ctx, db.CreateSessionParams{
|
|
Username: user.Username,
|
|
UserAgent: ctx.Request.UserAgent(),
|
|
ClientIp: ctx.ClientIP(),
|
|
RefreshToken: refreshToken,
|
|
ExpiresAt: pgtype.Timestamp{Valid: true, Time: refreshTokenPayload.ExpiredAt},
|
|
IsBlocked: false,
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to create session"))
|
|
return
|
|
}
|
|
|
|
ctx.SetCookie(
|
|
"paseto",
|
|
accessToken,
|
|
int(server.Config.CookieDuration),
|
|
"/",
|
|
"localhost",
|
|
false,
|
|
false,
|
|
)
|
|
ctx.JSON(http.StatusOK, user)
|
|
}
|
|
|
|
func (server *Server) logout(ctx *gin.Context) {
|
|
ctx.SetCookie(
|
|
"paseto",
|
|
"",
|
|
-1,
|
|
"/",
|
|
"",
|
|
false,
|
|
true,
|
|
)
|
|
ctx.Writer.WriteHeader(http.StatusNoContent)
|
|
}
|