Skip to content
Merged

Dev #141

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/momshell/backend/internal/model"
"github.com/momshell/backend/internal/repository"
"github.com/momshell/backend/internal/router"
"github.com/momshell/backend/internal/scheduler"
"github.com/momshell/backend/internal/service"
"github.com/momshell/backend/pkg/firecrawl"
"github.com/momshell/backend/pkg/openai"
Expand Down Expand Up @@ -75,7 +76,7 @@ func main() {

chatService := service.NewChatService(chatClient, chatRepo, firecrawlClient)
echoService := service.NewEchoService(chatClient, echoRepo, userRepo)
photoService := service.NewPhotoService(photoRepo, chatClient, cfg.ImageModel)
photoService := service.NewPhotoService(photoRepo, userRepo, chatClient, cfg.ImageModel)
whisperService := service.NewWhisperService(whisperRepo, userRepo, chatClient)
taskService := service.NewTaskService(taskRepo, userRepo)

Expand All @@ -97,10 +98,13 @@ func main() {

// Initialize admin layer
adminRepo := repository.NewAdminRepo(db)
adminService := service.NewAdminService(cfg, adminRepo, userRepo)
adminService := service.NewAdminService(cfg, adminRepo, userRepo, photoRepo)

// Start background schedulers
scheduler.StartPhotoCleanup(photoRepo)

// Initialize handlers
authHandler := handler.NewAuthHandler(authService)
authHandler := handler.NewAuthHandler(authService, cfg)
questionHandler := handler.NewQuestionHandler(communityService, authService, communityAIService)
answerHandler := handler.NewAnswerHandler(communityService, authService, communityAIService)
commentHandler := handler.NewCommentHandler(communityService, authService, communityAIService)
Expand All @@ -116,13 +120,16 @@ func main() {

// Setup Gin
r := gin.New()
_ = r.SetTrustedProxies(nil) // Don't trust any proxy headers by default
r.Use(middleware.Recovery())
r.Use(middleware.CORS())
r.Use(middleware.SecurityHeaders())
r.Use(middleware.CORS(cfg))
r.Use(gin.Logger())

// Register routes
router.Setup(
r, cfg,
adminHandler.IsAdmin,
authHandler, questionHandler, answerHandler,
commentHandler, interactionHandler, tagHandler,
chatHandler, echoHandler, userHandler, adminHandler,
Expand Down
191 changes: 187 additions & 4 deletions backend/internal/admin/admin.html

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type Config struct {
// Server
Port string

// CORS
CORSOrigins string

// Logging
DBLogLevel string

// Admin
AdminUsername string
AdminEmail string
Expand All @@ -53,6 +59,8 @@ func Load() *Config {
FirecrawlAPIKey: getEnv("FIRECRAWL_API_KEY", ""),
ImageModel: getEnv("IMAGE_MODEL", ""),
Port: getEnv("PORT", "8000"),
CORSOrigins: getEnv("CORS_ORIGINS", "http://localhost:5173,http://localhost:8000,http://localhost:3000"),
DBLogLevel: getEnv("DB_LOG_LEVEL", "warn"),
AdminUsername: getEnv("ADMIN_USERNAME", ""),
AdminEmail: getEnv("ADMIN_EMAIL", ""),
AdminPassword: getEnv("ADMIN_PASSWORD", ""),
Expand Down
15 changes: 14 additions & 1 deletion backend/internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package database

import (
"fmt"
"strings"

"github.com/momshell/backend/internal/config"
"gorm.io/driver/postgres"
Expand All @@ -10,8 +11,20 @@ import (
)

func Connect(cfg *config.Config) (*gorm.DB, error) {
logLevel := logger.Warn
switch strings.ToLower(cfg.DBLogLevel) {
case "silent":
logLevel = logger.Silent
case "error":
logLevel = logger.Error
case "warn":
logLevel = logger.Warn
case "info":
logLevel = logger.Info
}

db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
Logger: logger.Default.LogMode(logLevel),
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
Expand Down
23 changes: 23 additions & 0 deletions backend/internal/dto/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ type DashboardStats struct {
TotalQuestions int64 `json:"total_questions"`
TotalAnswers int64 `json:"total_answers"`
TotalCertifications int64 `json:"total_certifications"`
TotalPhotos int64 `json:"total_photos"`
WallPhotos int64 `json:"wall_photos"`
}

// ConfigItem represents a single configuration item
Expand All @@ -90,3 +92,24 @@ type ConfigItem struct {
type ConfigUpdateRequest struct {
Items map[string]string `json:"items" binding:"required"`
}

// AdminPhotoListParams holds query parameters for admin photo listing
type AdminPhotoListParams struct {
PaginationParams
Search string `form:"search"`
UserID string `form:"user_id"`
Source string `form:"source"`
OnWall string `form:"on_wall"`
}

// AdminPhotoListItem is a photo row in admin photo list
type AdminPhotoListItem struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Username string `json:"username"`
Title string `json:"title"`
ImageURL string `json:"image_url"`
IsOnWall bool `json:"is_on_wall"`
Source string `json:"source"`
CreatedAt time.Time `json:"created_at"`
}
4 changes: 2 additions & 2 deletions backend/internal/dto/answer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import "time"

// AnswerCreate is the request body for creating an answer
type AnswerCreate struct {
Content string `json:"content" binding:"required,min=1"`
Content string `json:"content" binding:"required,min=1,max=50000"`
ImageURLs []string `json:"image_urls"`
}

// AnswerUpdate is the request body for updating an answer
type AnswerUpdate struct {
Content *string `json:"content" binding:"omitempty,min=1"`
Content *string `json:"content" binding:"omitempty,min=1,max=50000"`
}

// AnswerListParams holds query parameters for listing answers
Expand Down
14 changes: 10 additions & 4 deletions backend/internal/dto/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "time"
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Password string `json:"password" binding:"required,min=8"`
Nickname string `json:"nickname" binding:"required,min=1,max=50"`
Role string `json:"role" binding:"omitempty,oneof=mom dad family"`
}
Expand All @@ -17,13 +17,19 @@ type LoginRequest struct {
Password string `json:"password" binding:"required"`
}

// TokenResponse is the response body for login/refresh
// TokenResponse is the response body for login/refresh (internal use)
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"` // seconds
}

// AccessTokenResponse is the response body sent to clients (refresh token in cookie only)
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"` // seconds
}

// RefreshRequest is the request body for token refresh
type RefreshRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
Expand All @@ -48,7 +54,7 @@ type UserResponse struct {
// ChangePasswordRequest is the request body for changing password
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}

// ForgotPasswordRequest is the request body for forgot password
Expand All @@ -59,7 +65,7 @@ type ForgotPasswordRequest struct {
// ResetPasswordRequest is the request body for resetting password
type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}

// UpdateRoleRequest is the request body for updating user role
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/dto/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "time"

// CommentCreate is the request body for creating a comment
type CommentCreate struct {
Content string `json:"content" binding:"required,min=1"`
Content string `json:"content" binding:"required,min=1,max=10000"`
ParentID *string `json:"parent_id"`
}

Expand Down
4 changes: 2 additions & 2 deletions backend/internal/dto/question.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "time"
// QuestionCreate is the request body for creating a question
type QuestionCreate struct {
Title string `json:"title" binding:"required,min=1,max=200"`
Content string `json:"content" binding:"required,min=1"`
Content string `json:"content" binding:"required,min=1,max=50000"`
Channel string `json:"channel" binding:"required,oneof=professional experience"`
ImageURLs []string `json:"image_urls"`
TagIDs []string `json:"tag_ids"`
Expand All @@ -14,7 +14,7 @@ type QuestionCreate struct {
// QuestionUpdate is the request body for updating a question
type QuestionUpdate struct {
Title *string `json:"title" binding:"omitempty,min=1,max=200"`
Content *string `json:"content" binding:"omitempty,min=1"`
Content *string `json:"content" binding:"omitempty,min=1,max=50000"`
TagIDs []string `json:"tag_ids"`
}

Expand Down
45 changes: 45 additions & 0 deletions backend/internal/handler/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ func NewAdminHandler(adminService *service.AdminService, authService *service.Au
}
}

// IsAdmin checks if the given user is an admin. Used as middleware.AdminChecker.
func (h *AdminHandler) IsAdmin(userID string) bool {
user, err := h.authService.GetUserByID(userID)
if err != nil {
return false
}
return user.IsAdmin
}

// requireAdmin checks if the current user is an admin
func (h *AdminHandler) requireAdmin(c *gin.Context) (string, bool) {
userID := middleware.GetUserID(c)
Expand Down Expand Up @@ -190,3 +199,39 @@ func (h *AdminHandler) UpdateConfig(c *gin.Context) {

c.JSON(http.StatusOK, gin.H{"message": "配置已更新"})
}

// ListPhotos returns paginated photo list for admin
func (h *AdminHandler) ListPhotos(c *gin.Context) {
if _, ok := h.requireAdmin(c); !ok {
return
}
Comment on lines +204 to +207
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AdminRequired middleware (used in the router) now performs a database lookup via isAdmin(userID) to verify admin status. However, every admin handler method (e.g., ListPhotos, DeletePhoto) also calls h.requireAdmin(c) which performs a second identical database lookup. This results in two DB queries per admin API request to check the same thing. Since the middleware already guarantees the user is an admin, you can remove the requireAdmin calls from the handler methods that are behind the AdminRequired middleware.

Copilot uses AI. Check for mistakes.

var params dto.AdminPhotoListParams
if err := c.ShouldBindQuery(&params); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}

resp, err := h.adminService.ListPhotos(params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// DeletePhoto deletes a photo by ID (admin)
func (h *AdminHandler) DeletePhoto(c *gin.Context) {
if _, ok := h.requireAdmin(c); !ok {
return
}

id := c.Param("id")
if err := h.adminService.DeletePhoto(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "照片已删除"})
}
Loading