style: 统一代码格式和行尾
This commit is contained in:
@@ -1,79 +1,79 @@
|
||||
package account
|
||||
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
Password string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
RefreshToken string `json:"-"`
|
||||
AvatarURL string `gorm:"type:varchar(512)" json:"avatar_url,omitempty"`
|
||||
Bio string `gorm:"type:varchar(255)" json:"bio,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAccountRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type RenameRequest struct {
|
||||
NewUsername string `json:"new_username"`
|
||||
}
|
||||
|
||||
type FindByIDRequest struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
type FindByIDResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
}
|
||||
|
||||
type FindByUsernameRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type FindByUsernameResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type UpdateProfileRequest struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type GetProfileRequest struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
type GetProfileResponse struct {
|
||||
Account FindByIDResponse `json:"account"`
|
||||
VideoCount int64 `json:"video_count"`
|
||||
TotalLikes int64 `json:"total_likes"`
|
||||
FollowerCount int64 `json:"follower_count"`
|
||||
VloggerCount int64 `json:"vlogger_count"`
|
||||
}
|
||||
package account
|
||||
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique" json:"username"`
|
||||
Password string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
RefreshToken string `json:"-"`
|
||||
AvatarURL string `gorm:"type:varchar(512)" json:"avatar_url,omitempty"`
|
||||
Bio string `gorm:"type:varchar(255)" json:"bio,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAccountRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type RenameRequest struct {
|
||||
NewUsername string `json:"new_username"`
|
||||
}
|
||||
|
||||
type FindByIDRequest struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
type FindByIDResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
}
|
||||
|
||||
type FindByUsernameRequest struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type FindByUsernameResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type UpdateProfileRequest struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type GetProfileRequest struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
type GetProfileResponse struct {
|
||||
Account FindByIDResponse `json:"account"`
|
||||
VideoCount int64 `json:"video_count"`
|
||||
TotalLikes int64 `json:"total_likes"`
|
||||
FollowerCount int64 `json:"follower_count"`
|
||||
VloggerCount int64 `json:"vlogger_count"`
|
||||
}
|
||||
|
||||
@@ -1,257 +1,257 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"feedsystem_video_go/internal/apierror"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountHandler struct {
|
||||
accountService *AccountService
|
||||
}
|
||||
|
||||
func NewAccountHandler(accountService *AccountService) *AccountHandler {
|
||||
return &AccountHandler{accountService: accountService}
|
||||
}
|
||||
func (h *AccountHandler) CreateAccount(c *gin.Context) {
|
||||
var req CreateAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.CreateAccount(c.Request.Context(), &Account{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account created"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Rename(c *gin.Context) {
|
||||
var req RenameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
token, err := h.accountService.Rename(c.Request.Context(), accountID, req.NewUsername)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNewUsernameRequired) {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrUsernameTaken) {
|
||||
c.JSON(409, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(404, gin.H{"error": "account not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.ChangePassword(c.Request.Context(), req.Username, req.OldPassword, req.NewPassword); err != nil {
|
||||
c.JSON(400, gin.H{"error": "unsuccessfully password changed"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "successfully password changed"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByID(c *gin.Context) {
|
||||
var req FindByIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByID(c.Request.Context(), req.ID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByUsername(c *gin.Context) {
|
||||
var req FindByUsernameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accessToken, refreshToken, err := h.accountService.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, LoginResponse{Token: accessToken, RefreshToken: refreshToken, AccountID: account.ID, Username: account.Username})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Logout(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.Logout(c.Request.Context(), accountID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account logged out"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UploadAvatar(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
const maxSize = 10 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp allowed"})
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(".run", "uploads", "avatars", strconv.FormatUint(uint64(accountID), 10))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename, err := randHex(16)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename = filename + ext
|
||||
absPath := filepath.Join(dir, filename)
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
urlPath := path.Join("/static", "avatars", strconv.FormatUint(uint64(accountID), 10), filename)
|
||||
avatarURL := buildAbsoluteURL(c, urlPath)
|
||||
if err := h.accountService.UpdateAvatar(c.Request.Context(), accountID, avatarURL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"avatar_url": avatarURL})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UpdateProfile(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var req UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.UpdateProfile(c.Request.Context(), accountID, &req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "profile updated"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
newToken, accountID, username, err := h.accountService.RefreshAccessToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, LoginResponse{Token: newToken, AccountID: accountID, Username: username})
|
||||
}
|
||||
|
||||
func randHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("rand.Read: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
||||
scheme = xf
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
||||
}
|
||||
|
||||
func getAccountID(c *gin.Context) (uint, error) {
|
||||
value, exists := c.Get("accountID")
|
||||
if !exists {
|
||||
return 0, errors.New("accountID not found")
|
||||
}
|
||||
id, ok := value.(uint)
|
||||
if !ok {
|
||||
return 0, errors.New("accountID has invalid type")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
package account
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"feedsystem_video_go/internal/apierror"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountHandler struct {
|
||||
accountService *AccountService
|
||||
}
|
||||
|
||||
func NewAccountHandler(accountService *AccountService) *AccountHandler {
|
||||
return &AccountHandler{accountService: accountService}
|
||||
}
|
||||
func (h *AccountHandler) CreateAccount(c *gin.Context) {
|
||||
var req CreateAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.CreateAccount(c.Request.Context(), &Account{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account created"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Rename(c *gin.Context) {
|
||||
var req RenameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
token, err := h.accountService.Rename(c.Request.Context(), accountID, req.NewUsername)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNewUsernameRequired) {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrUsernameTaken) {
|
||||
c.JSON(409, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(404, gin.H{"error": "account not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.ChangePassword(c.Request.Context(), req.Username, req.OldPassword, req.NewPassword); err != nil {
|
||||
c.JSON(400, gin.H{"error": "unsuccessfully password changed"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "successfully password changed"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByID(c *gin.Context) {
|
||||
var req FindByIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByID(c.Request.Context(), req.ID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByUsername(c *gin.Context) {
|
||||
var req FindByUsernameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accessToken, refreshToken, err := h.accountService.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, LoginResponse{Token: accessToken, RefreshToken: refreshToken, AccountID: account.ID, Username: account.Username})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Logout(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.Logout(c.Request.Context(), accountID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account logged out"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UploadAvatar(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
const maxSize = 10 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp allowed"})
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(".run", "uploads", "avatars", strconv.FormatUint(uint64(accountID), 10))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename, err := randHex(16)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename = filename + ext
|
||||
absPath := filepath.Join(dir, filename)
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
urlPath := path.Join("/static", "avatars", strconv.FormatUint(uint64(accountID), 10), filename)
|
||||
avatarURL := buildAbsoluteURL(c, urlPath)
|
||||
if err := h.accountService.UpdateAvatar(c.Request.Context(), accountID, avatarURL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"avatar_url": avatarURL})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) UpdateProfile(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var req UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.UpdateProfile(c.Request.Context(), accountID, &req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "profile updated"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Refresh(c *gin.Context) {
|
||||
var req RefreshRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(apierror.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
newToken, accountID, username, err := h.accountService.RefreshAccessToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, LoginResponse{Token: newToken, AccountID: accountID, Username: username})
|
||||
}
|
||||
|
||||
func randHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("rand.Read: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
||||
scheme = xf
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
||||
}
|
||||
|
||||
func getAccountID(c *gin.Context) (uint, error) {
|
||||
value, exists := c.Get("accountID")
|
||||
if !exists {
|
||||
return 0, errors.New("accountID not found")
|
||||
}
|
||||
id, ok := value.(uint)
|
||||
if !ok {
|
||||
return 0, errors.New("accountID has invalid type")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountRepository(db *gorm.DB) *AccountRepository {
|
||||
return &AccountRepository{db: db}
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) CreateAccount(ctx context.Context, account *Account) error {
|
||||
if err := ar.db.WithContext(ctx).Create(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Rename(ctx context.Context, id uint, newUsername string) error {
|
||||
result := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("username", newUsername)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) RenameWithToken(ctx context.Context, id uint, newUsername string, token string) error {
|
||||
return ar.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&Account{}).Where("id = ?", id).Update("username", newUsername)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := tx.Model(&Account{}).Where("id = ?", id).Update("token", token).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) ChangePassword(ctx context.Context, id uint, newPassword string) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("password", newPassword).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindByID(ctx context.Context, id uint) (*Account, error) {
|
||||
var account Account
|
||||
if err := ar.db.WithContext(ctx).First(&account, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindByUsername(ctx context.Context, username string) (*Account, error) {
|
||||
var account Account
|
||||
if err := ar.db.WithContext(ctx).Where("username = ?", username).First(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Login(ctx context.Context, id uint, token, refreshToken string) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(map[string]interface{}{"token": token, "refresh_token": refreshToken}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Logout(ctx context.Context, id uint) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(map[string]interface{}{"token": "", "refresh_token": ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", accountID).Update("avatar_url", avatarURL).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateToken(ctx context.Context, id uint, token string) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("token", token).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateFields(ctx context.Context, id uint, updates map[string]interface{}) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindAll(ctx context.Context) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
if err := ar.db.WithContext(ctx).Find(&accounts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAccountRepository(db *gorm.DB) *AccountRepository {
|
||||
return &AccountRepository{db: db}
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) CreateAccount(ctx context.Context, account *Account) error {
|
||||
if err := ar.db.WithContext(ctx).Create(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Rename(ctx context.Context, id uint, newUsername string) error {
|
||||
result := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("username", newUsername)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) RenameWithToken(ctx context.Context, id uint, newUsername string, token string) error {
|
||||
return ar.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&Account{}).Where("id = ?", id).Update("username", newUsername)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := tx.Model(&Account{}).Where("id = ?", id).Update("token", token).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) ChangePassword(ctx context.Context, id uint, newPassword string) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("password", newPassword).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindByID(ctx context.Context, id uint) (*Account, error) {
|
||||
var account Account
|
||||
if err := ar.db.WithContext(ctx).First(&account, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindByUsername(ctx context.Context, username string) (*Account, error) {
|
||||
var account Account
|
||||
if err := ar.db.WithContext(ctx).Where("username = ?", username).First(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Login(ctx context.Context, id uint, token, refreshToken string) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(map[string]interface{}{"token": token, "refresh_token": refreshToken}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) Logout(ctx context.Context, id uint) error {
|
||||
if err := ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(map[string]interface{}{"token": "", "refresh_token": ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", accountID).Update("avatar_url", avatarURL).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateToken(ctx context.Context, id uint, token string) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Update("token", token).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) UpdateFields(ctx context.Context, id uint, updates map[string]interface{}) error {
|
||||
return ar.db.WithContext(ctx).Model(&Account{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (ar *AccountRepository) FindAll(ctx context.Context) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
if err := ar.db.WithContext(ctx).Find(&accounts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
@@ -1,236 +1,236 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"feedsystem_video_go/internal/auth"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountService struct {
|
||||
accountRepository *AccountRepository
|
||||
cache *rediscache.Client
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUsernameTaken = errors.New("username already exists")
|
||||
ErrNewUsernameRequired = errors.New("new_username is required")
|
||||
)
|
||||
|
||||
func NewAccountService(accountRepository *AccountRepository, cache *rediscache.Client) *AccountService {
|
||||
return &AccountService{accountRepository: accountRepository, cache: cache}
|
||||
}
|
||||
|
||||
func (as *AccountService) CreateAccount(ctx context.Context, account *Account) error {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.Password = string(passwordHash)
|
||||
if err := as.accountRepository.CreateAccount(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AccountService) Rename(ctx context.Context, accountID uint, newUsername string) (string, error) {
|
||||
if newUsername == "" {
|
||||
return "", ErrNewUsernameRequired
|
||||
}
|
||||
|
||||
token, err := auth.GenerateToken(accountID, newUsername)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := as.accountRepository.RenameWithToken(ctx, accountID, newUsername, token); err != nil {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
||||
return "", ErrUsernameTaken
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", accountID), []byte(token), 24*time.Hour); err != nil {
|
||||
log.Printf("failed to set cache: %v", err)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (as *AccountService) ChangePassword(ctx context.Context, username, oldPassword, newPassword string) error {
|
||||
account, err := as.FindByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(oldPassword)); err != nil {
|
||||
return err
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := as.accountRepository.ChangePassword(ctx, account.ID, string(passwordHash)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := as.Logout(ctx, account.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AccountService) FindByID(ctx context.Context, id uint) (*Account, error) {
|
||||
if account, err := as.accountRepository.FindByID(ctx, id); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AccountService) FindByUsername(ctx context.Context, username string) (*Account, error) {
|
||||
if account, err := as.accountRepository.FindByUsername(ctx, username); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AccountService) Login(ctx context.Context, username, password string) (string, string, error) {
|
||||
account, err := as.FindByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
accessToken, err := auth.GenerateToken(account.ID, account.Username)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
refreshToken, err := auth.GenerateRefreshToken(account.ID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := as.accountRepository.Login(ctx, account.ID, accessToken, refreshToken); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(accessToken), 24*time.Hour); err != nil {
|
||||
log.Printf("failed to set cache: %v", err)
|
||||
}
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d:refresh", account.ID), []byte(refreshToken), 7*24*time.Hour); err != nil {
|
||||
log.Printf("failed to set refresh cache: %v", err)
|
||||
}
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("refresh:%s", refreshToken), []byte(strconv.FormatUint(uint64(account.ID), 10)), 7*24*time.Hour); err != nil {
|
||||
log.Printf("failed to set refresh lookup: %v", err)
|
||||
}
|
||||
}
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
func (as *AccountService) Logout(ctx context.Context, accountID uint) error {
|
||||
account, err := as.FindByID(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Token == "" {
|
||||
return nil
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.Del(cacheCtx, as.cache.Key("account:%d", account.ID)); err != nil {
|
||||
log.Printf("failed to del cache: %v", err)
|
||||
}
|
||||
if err := as.cache.Del(cacheCtx, as.cache.Key("account:%d:refresh", account.ID)); err != nil {
|
||||
log.Printf("failed to del refresh cache: %v", err)
|
||||
}
|
||||
if account.RefreshToken != "" {
|
||||
as.cache.Del(cacheCtx, as.cache.Key("refresh:%s", account.RefreshToken))
|
||||
}
|
||||
}
|
||||
return as.accountRepository.Logout(ctx, account.ID)
|
||||
}
|
||||
|
||||
func (as *AccountService) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return as.accountRepository.UpdateAvatar(ctx, accountID, avatarURL)
|
||||
}
|
||||
|
||||
func (as *AccountService) FindAll(ctx context.Context) ([]*Account, error) {
|
||||
return as.accountRepository.FindAll(ctx)
|
||||
}
|
||||
|
||||
func (as *AccountService) UpdateProfile(ctx context.Context, accountID uint, req *UpdateProfileRequest) error {
|
||||
updates := map[string]interface{}{}
|
||||
if req.Bio != "" {
|
||||
updates["bio"] = strings.TrimSpace(req.Bio)
|
||||
}
|
||||
if req.AvatarURL != "" {
|
||||
updates["avatar_url"] = strings.TrimSpace(req.AvatarURL)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return errors.New("nothing to update")
|
||||
}
|
||||
return as.accountRepository.UpdateFields(ctx, accountID, updates)
|
||||
}
|
||||
|
||||
func (as *AccountService) RefreshAccessToken(ctx context.Context, refreshToken string) (string, uint, string, error) {
|
||||
if refreshToken == "" {
|
||||
return "", 0, "", errors.New("refresh token is empty")
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
b, err := as.cache.GetBytes(cacheCtx, as.cache.Key("refresh:%s", refreshToken))
|
||||
if err == nil {
|
||||
idStr := string(b)
|
||||
id, parseErr := strconv.ParseUint(idStr, 10, 64)
|
||||
if parseErr == nil {
|
||||
account, err := as.FindByID(ctx, uint(id))
|
||||
if err == nil && account != nil && account.RefreshToken == refreshToken {
|
||||
newToken, err := auth.GenerateToken(account.ID, account.Username)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
as.accountRepository.UpdateToken(ctx, account.ID, newToken)
|
||||
as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(newToken), 24*time.Hour)
|
||||
return newToken, account.ID, account.Username, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accounts, err := as.FindAll(ctx)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
if acc.RefreshToken == refreshToken {
|
||||
newToken, err := auth.GenerateToken(acc.ID, acc.Username)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
as.accountRepository.UpdateToken(ctx, acc.ID, newToken)
|
||||
return newToken, acc.ID, acc.Username, nil
|
||||
}
|
||||
}
|
||||
return "", 0, "", errors.New("invalid refresh token")
|
||||
}
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"feedsystem_video_go/internal/auth"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountService struct {
|
||||
accountRepository *AccountRepository
|
||||
cache *rediscache.Client
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUsernameTaken = errors.New("username already exists")
|
||||
ErrNewUsernameRequired = errors.New("new_username is required")
|
||||
)
|
||||
|
||||
func NewAccountService(accountRepository *AccountRepository, cache *rediscache.Client) *AccountService {
|
||||
return &AccountService{accountRepository: accountRepository, cache: cache}
|
||||
}
|
||||
|
||||
func (as *AccountService) CreateAccount(ctx context.Context, account *Account) error {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.Password = string(passwordHash)
|
||||
if err := as.accountRepository.CreateAccount(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AccountService) Rename(ctx context.Context, accountID uint, newUsername string) (string, error) {
|
||||
if newUsername == "" {
|
||||
return "", ErrNewUsernameRequired
|
||||
}
|
||||
|
||||
token, err := auth.GenerateToken(accountID, newUsername)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := as.accountRepository.RenameWithToken(ctx, accountID, newUsername, token); err != nil {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
||||
return "", ErrUsernameTaken
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", accountID), []byte(token), 24*time.Hour); err != nil {
|
||||
log.Printf("failed to set cache: %v", err)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (as *AccountService) ChangePassword(ctx context.Context, username, oldPassword, newPassword string) error {
|
||||
account, err := as.FindByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(oldPassword)); err != nil {
|
||||
return err
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := as.accountRepository.ChangePassword(ctx, account.ID, string(passwordHash)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := as.Logout(ctx, account.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AccountService) FindByID(ctx context.Context, id uint) (*Account, error) {
|
||||
if account, err := as.accountRepository.FindByID(ctx, id); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AccountService) FindByUsername(ctx context.Context, username string) (*Account, error) {
|
||||
if account, err := as.accountRepository.FindByUsername(ctx, username); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AccountService) Login(ctx context.Context, username, password string) (string, string, error) {
|
||||
account, err := as.FindByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
accessToken, err := auth.GenerateToken(account.ID, account.Username)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
refreshToken, err := auth.GenerateRefreshToken(account.ID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := as.accountRepository.Login(ctx, account.ID, accessToken, refreshToken); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(accessToken), 24*time.Hour); err != nil {
|
||||
log.Printf("failed to set cache: %v", err)
|
||||
}
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d:refresh", account.ID), []byte(refreshToken), 7*24*time.Hour); err != nil {
|
||||
log.Printf("failed to set refresh cache: %v", err)
|
||||
}
|
||||
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("refresh:%s", refreshToken), []byte(strconv.FormatUint(uint64(account.ID), 10)), 7*24*time.Hour); err != nil {
|
||||
log.Printf("failed to set refresh lookup: %v", err)
|
||||
}
|
||||
}
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
func (as *AccountService) Logout(ctx context.Context, accountID uint) error {
|
||||
account, err := as.FindByID(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Token == "" {
|
||||
return nil
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := as.cache.Del(cacheCtx, as.cache.Key("account:%d", account.ID)); err != nil {
|
||||
log.Printf("failed to del cache: %v", err)
|
||||
}
|
||||
if err := as.cache.Del(cacheCtx, as.cache.Key("account:%d:refresh", account.ID)); err != nil {
|
||||
log.Printf("failed to del refresh cache: %v", err)
|
||||
}
|
||||
if account.RefreshToken != "" {
|
||||
as.cache.Del(cacheCtx, as.cache.Key("refresh:%s", account.RefreshToken))
|
||||
}
|
||||
}
|
||||
return as.accountRepository.Logout(ctx, account.ID)
|
||||
}
|
||||
|
||||
func (as *AccountService) UpdateAvatar(ctx context.Context, accountID uint, avatarURL string) error {
|
||||
return as.accountRepository.UpdateAvatar(ctx, accountID, avatarURL)
|
||||
}
|
||||
|
||||
func (as *AccountService) FindAll(ctx context.Context) ([]*Account, error) {
|
||||
return as.accountRepository.FindAll(ctx)
|
||||
}
|
||||
|
||||
func (as *AccountService) UpdateProfile(ctx context.Context, accountID uint, req *UpdateProfileRequest) error {
|
||||
updates := map[string]interface{}{}
|
||||
if req.Bio != "" {
|
||||
updates["bio"] = strings.TrimSpace(req.Bio)
|
||||
}
|
||||
if req.AvatarURL != "" {
|
||||
updates["avatar_url"] = strings.TrimSpace(req.AvatarURL)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return errors.New("nothing to update")
|
||||
}
|
||||
return as.accountRepository.UpdateFields(ctx, accountID, updates)
|
||||
}
|
||||
|
||||
func (as *AccountService) RefreshAccessToken(ctx context.Context, refreshToken string) (string, uint, string, error) {
|
||||
if refreshToken == "" {
|
||||
return "", 0, "", errors.New("refresh token is empty")
|
||||
}
|
||||
if as.cache != nil {
|
||||
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
b, err := as.cache.GetBytes(cacheCtx, as.cache.Key("refresh:%s", refreshToken))
|
||||
if err == nil {
|
||||
idStr := string(b)
|
||||
id, parseErr := strconv.ParseUint(idStr, 10, 64)
|
||||
if parseErr == nil {
|
||||
account, err := as.FindByID(ctx, uint(id))
|
||||
if err == nil && account != nil && account.RefreshToken == refreshToken {
|
||||
newToken, err := auth.GenerateToken(account.ID, account.Username)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
as.accountRepository.UpdateToken(ctx, account.ID, newToken)
|
||||
as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(newToken), 24*time.Hour)
|
||||
return newToken, account.ID, account.Username, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accounts, err := as.FindAll(ctx)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
if acc.RefreshToken == refreshToken {
|
||||
newToken, err := auth.GenerateToken(acc.ID, acc.Username)
|
||||
if err != nil {
|
||||
return "", 0, "", err
|
||||
}
|
||||
as.accountRepository.UpdateToken(ctx, acc.ID, newToken)
|
||||
return newToken, acc.ID, acc.Username, nil
|
||||
}
|
||||
}
|
||||
return "", 0, "", errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user