mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
⚡️ feat: add main page layout with navigation and footer
feat: create random labels page feat: implement login page with OpenID Connect support feat: add Gravatar component for user avatars feat: create Navbar component with navigation menu chore: create Sidebar component placeholder feat: implement login form with OIDC and email/password options feat: add reusable button component feat: create card component for structured content display feat: implement input component for forms feat: create label component for form labels feat: add navigation menu component for site navigation chore: add configuration file for site metadata feat: implement device context for responsive design feat: add utility functions for class name management feat: define OIDC configuration model feat: define base response model for API responses feat: define user model for user data feat: implement i18n for internationalization support feat: add English and Chinese translations for login chore: create index for locale resources chore: add blog home view placeholder
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"github.com/snowykami/neo-blog/internal/service"
|
||||
"github.com/snowykami/neo-blog/pkg/errs"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AdminController struct {
|
||||
@ -22,7 +23,7 @@ func NewAdminController() *AdminController {
|
||||
func (cc *AdminController) CreateOidc(ctx context.Context, c *app.RequestContext) {
|
||||
var adminCreateOidcReq dto.AdminOidcConfigDto
|
||||
if err := c.BindAndValidate(&adminCreateOidcReq); err != nil {
|
||||
c.JSON(400, map[string]string{"error": "Invalid parameters"})
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
err := cc.service.CreateOidcConfig(&adminCreateOidcReq)
|
||||
@ -77,12 +78,23 @@ func (cc *AdminController) ListOidc(ctx context.Context, c *app.RequestContext)
|
||||
}
|
||||
|
||||
func (cc *AdminController) UpdateOidc(ctx context.Context, c *app.RequestContext) {
|
||||
var adminUpdateOidcReq dto.AdminOidcConfigDto
|
||||
if err := c.BindAndValidate(&adminUpdateOidcReq); err != nil {
|
||||
c.JSON(400, map[string]string{"error": "Invalid parameters"})
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
err := cc.service.UpdateOidcConfig(&adminUpdateOidcReq)
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil || idInt <= 0 {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
var adminUpdateOidcReq dto.AdminOidcConfigDto
|
||||
if err := c.BindAndValidate(&adminUpdateOidcReq); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
adminUpdateOidcReq.ID = uint(idInt)
|
||||
err = cc.service.UpdateOidcConfig(&adminUpdateOidcReq)
|
||||
if err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||
|
@ -3,10 +3,14 @@ package v1
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/internal/service"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/pkg/errs"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PostController struct {
|
||||
@ -24,7 +28,7 @@ func (p *PostController) Create(ctx context.Context, c *app.RequestContext) {
|
||||
if err := c.BindAndValidate(&req); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
}
|
||||
if err := p.service.CreatePost(&req); err != nil {
|
||||
if err := p.service.CreatePost(ctx, &req); err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||
return
|
||||
@ -47,13 +51,63 @@ func (p *PostController) Delete(ctx context.Context, c *app.RequestContext) {
|
||||
}
|
||||
|
||||
func (p *PostController) Get(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: Impl
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
post, err := p.service.GetPost(ctx, id)
|
||||
if err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||
return
|
||||
}
|
||||
if post == nil {
|
||||
resps.NotFound(c, resps.ErrNotFound)
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, post)
|
||||
}
|
||||
|
||||
func (p *PostController) Update(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: Impl
|
||||
var req dto.CreateOrUpdatePostReq
|
||||
if err := c.BindAndValidate(&req); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
if err := p.service.UpdatePost(ctx, id, &req); err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, nil)
|
||||
}
|
||||
|
||||
func (p *PostController) List(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: Impl
|
||||
pagination := ctxutils.GetPaginationParams(c)
|
||||
if pagination.OrderedBy != "" && !slices.Contains(constant.OrderedByEnumPost, pagination.OrderedBy) {
|
||||
resps.BadRequest(c, "无效的排序字段")
|
||||
return
|
||||
}
|
||||
keywords := c.Query("keywords")
|
||||
keywordsArray := strings.Split(keywords, ",")
|
||||
req := &dto.ListPostReq{
|
||||
Keywords: keywordsArray,
|
||||
Page: pagination.Page,
|
||||
Size: pagination.Size,
|
||||
OrderedBy: pagination.OrderedBy,
|
||||
Reverse: pagination.Reverse,
|
||||
}
|
||||
resp, err := p.service.ListPosts(ctx, req)
|
||||
if err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, resp)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/cloudwego/hertz/pkg/common/utils"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
@ -82,8 +83,13 @@ func (u *UserController) OidcList(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) {
|
||||
name := c.Param("name")
|
||||
code := c.Param("code")
|
||||
state := c.Param("state")
|
||||
code := c.Query("code")
|
||||
state := c.Query("state")
|
||||
redirectUri := c.Query("redirect_back") // 前端路由登录前的重定向地址
|
||||
if redirectUri == "" {
|
||||
redirectUri = "/"
|
||||
}
|
||||
fmt.Println("redirectBack:", redirectUri)
|
||||
oidcLoginReq := &dto.OidcLoginReq{
|
||||
Name: name,
|
||||
Code: code,
|
||||
@ -96,22 +102,14 @@ func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) {
|
||||
return
|
||||
}
|
||||
ctxutils.SetTokenAndRefreshTokenCookie(c, resp.Token, resp.RefreshToken)
|
||||
resps.Ok(c, resps.Success, map[string]any{
|
||||
"token": resp.Token,
|
||||
"user": resp.User,
|
||||
})
|
||||
resps.Redirect(c, redirectUri) // 重定向到前端路由
|
||||
}
|
||||
|
||||
func (u *UserController) GetUser(ctx context.Context, c *app.RequestContext) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
userIDInt, err := strconv.Atoi(userID)
|
||||
if err != nil || userIDInt <= 0 {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
userIDInt = int(ctxutils.GetCurrentUserID(ctx))
|
||||
}
|
||||
|
||||
resp, err := u.service.GetUser(&dto.GetUserReq{UserID: uint(userIDInt)})
|
||||
@ -142,7 +140,7 @@ func (u *UserController) UpdateUser(ctx context.Context, c *app.RequestContext)
|
||||
updateUserReq.ID = uint(userIDInt)
|
||||
currentUser := ctxutils.GetCurrentUser(ctx)
|
||||
if currentUser == nil {
|
||||
resps.UnAuthorized(c, resps.ErrUnauthorized)
|
||||
resps.Unauthorized(c, resps.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
if currentUser.ID != updateUserReq.ID {
|
||||
|
@ -1 +1,46 @@
|
||||
package ctxutils
|
||||
|
||||
import (
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type PaginationParams struct {
|
||||
Page uint64
|
||||
Size uint64
|
||||
OrderedBy string
|
||||
Reverse bool // 默认是从大值到小值
|
||||
}
|
||||
|
||||
func GetPaginationParams(c *app.RequestContext) *PaginationParams {
|
||||
page := c.Query("page")
|
||||
size := c.Query("size")
|
||||
orderedBy := c.Query("ordered_by")
|
||||
reverse := c.Query("reverse")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
if size == "" {
|
||||
size = "10"
|
||||
}
|
||||
var reverseBool bool
|
||||
if reverse == "" || reverse == "false" || reverse == "0" {
|
||||
reverseBool = false
|
||||
} else {
|
||||
reverseBool = true
|
||||
}
|
||||
pageNum, err := strconv.ParseUint(page, 10, 64)
|
||||
if err != nil || pageNum < 1 {
|
||||
pageNum = 1
|
||||
}
|
||||
sizeNum, err := strconv.ParseUint(size, 10, 64)
|
||||
if err != nil || sizeNum < 1 {
|
||||
sizeNum = 10
|
||||
}
|
||||
return &PaginationParams{
|
||||
Page: pageNum,
|
||||
Size: sizeNum,
|
||||
OrderedBy: orderedBy,
|
||||
Reverse: reverseBool,
|
||||
}
|
||||
}
|
||||
|
@ -17,3 +17,12 @@ func GetCurrentUser(ctx context.Context) *model.User {
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// GetCurrentUserID 获取当前用户ID,如果未认证则返回0
|
||||
func GetCurrentUserID(ctx context.Context) uint {
|
||||
user := GetCurrentUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
|
@ -8,5 +8,6 @@ type AdminOidcConfigDto struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
OidcDiscoveryUrl string `json:"oidc_discovery_url"`
|
||||
Type string `json:"type"` // oauth2 or misskey
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
package dto
|
||||
|
||||
type PostDto struct {
|
||||
UserID uint `json:"user_id"` // 发布者的用户ID
|
||||
Title string `json:"title"` // 帖子标题
|
||||
Content string `json:"content"`
|
||||
Labels []LabelDto `json:"labels"` // 关联的标签
|
||||
IsPrivate bool `json:"is_private"` // 是否为私密帖子
|
||||
ID uint `json:"id"` // 帖子ID
|
||||
UserID uint `json:"user_id"` // 发布者的用户ID
|
||||
Title string `json:"title"` // 帖子标题
|
||||
Content string `json:"content"`
|
||||
Labels []LabelDto `json:"labels"` // 关联的标签
|
||||
IsPrivate bool `json:"is_private"` // 是否为私密帖子
|
||||
LikeCount uint64 `json:"like_count"` // 点赞数
|
||||
CommentCount uint64 `json:"comment_count"` // 评论数
|
||||
ViewCount uint64 `json:"view_count"` // 浏览数
|
||||
Heat uint64 `json:"heat"` // 热度
|
||||
}
|
||||
|
||||
type CreateOrUpdatePostReq struct {
|
||||
@ -14,3 +19,18 @@ type CreateOrUpdatePostReq struct {
|
||||
IsPrivate bool `json:"is_private"`
|
||||
Labels []uint `json:"labels"` // 标签ID列表
|
||||
}
|
||||
|
||||
type ListPostReq struct {
|
||||
Keywords []string `json:"keywords"` // 关键词列表
|
||||
OrderedBy string `json:"ordered_by"` // 排序方式
|
||||
Page uint64 `json:"page"` // 页码
|
||||
Size uint64 `json:"size"`
|
||||
Reverse bool `json:"reverse"`
|
||||
}
|
||||
|
||||
type ListPostResp struct {
|
||||
Posts []PostDto `json:"posts"`
|
||||
Total uint64 `json:"total"` // 总数
|
||||
OrderedBy string `json:"ordered_by"` // 排序方式
|
||||
Reverse bool `json:"reverse"`
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ func UseAuth(block bool) app.HandlerFunc {
|
||||
// 所有认证方式都失败
|
||||
if block {
|
||||
// 若需要阻断,返回未授权错误并中止请求
|
||||
resps.UnAuthorized(c, resps.ErrUnauthorized)
|
||||
resps.Unauthorized(c, resps.ErrUnauthorized)
|
||||
c.Abort()
|
||||
} else {
|
||||
// 若不需要阻断,继续请求但不设置用户ID
|
||||
|
@ -1 +1,9 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Category struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"type:text;not null"`
|
||||
Description string `gorm:"type:text;not null"` // 分类描述
|
||||
}
|
||||
|
@ -1,11 +1,24 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
import (
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
gorm.Model
|
||||
Key string `gorm:"uniqueIndex"` // 标签键,唯一标识
|
||||
Value string `gorm:"type:text"` // 标签值,描述标签的内容
|
||||
Color string `gorm:"type:text"` // 前端可用颜色代码
|
||||
TailwindClassName string `gorm:"type:text"` // Tailwind CSS 的类名,用于前端样式
|
||||
Key string `gorm:"uniqueIndex"` // 标签键,唯一标识
|
||||
Value string `gorm:"type:text"` // 标签值,描述标签的内容
|
||||
Color string `gorm:"type:text"` // 前端可用颜色代码
|
||||
TailwindClassName string `gorm:"type:text"` // Tailwind CSS 的类名,用于前端样式
|
||||
Posts []Post `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的帖子
|
||||
}
|
||||
|
||||
func (l *Label) ToDto() dto.LabelDto {
|
||||
return dto.LabelDto{
|
||||
Key: l.Key,
|
||||
Value: l.Value,
|
||||
Color: l.Color,
|
||||
TailwindClassName: l.TailwindClassName,
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"gorm.io/gorm"
|
||||
"resty.dev/v3"
|
||||
@ -17,6 +18,7 @@ type OidcConfig struct {
|
||||
Icon string // 图标url,为空则使用内置默认图标
|
||||
OidcDiscoveryUrl string // OpenID自动发现URL,例如 :https://pass.liteyuki.icu/.well-known/openid-configuration
|
||||
Enabled bool `gorm:"default:true"` // 是否启用
|
||||
Type string `gorm:"oauth2"` // OIDC类型,默认为oauth2,也可以为misskey
|
||||
// 以下字段为自动获取字段,每次更新配置时自动填充
|
||||
Issuer string
|
||||
AuthorizationEndpoint string
|
||||
@ -42,7 +44,7 @@ type oidcDiscoveryResp struct {
|
||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||
}
|
||||
|
||||
func updateOidcConfigFromUrl(url string) (*oidcDiscoveryResp, error) {
|
||||
func updateOidcConfigFromUrl(url string, typ string) (*oidcDiscoveryResp, error) {
|
||||
client := resty.New()
|
||||
client.SetTimeout(10 * time.Second) // 设置超时时间
|
||||
var discovery oidcDiscoveryResp
|
||||
@ -57,6 +59,11 @@ func updateOidcConfigFromUrl(url string) (*oidcDiscoveryResp, error) {
|
||||
return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode())
|
||||
}
|
||||
// 验证必要字段
|
||||
if typ == "misskey" {
|
||||
discovery.UserInfoEndpoint = discovery.Issuer + "/api/users/me" // Misskey的用户信息端点
|
||||
discovery.JwksUri = discovery.Issuer + "/api/jwks"
|
||||
}
|
||||
fmt.Println(discovery)
|
||||
if discovery.Issuer == "" ||
|
||||
discovery.AuthorizationEndpoint == "" ||
|
||||
discovery.TokenEndpoint == "" ||
|
||||
@ -69,10 +76,12 @@ func updateOidcConfigFromUrl(url string) (*oidcDiscoveryResp, error) {
|
||||
|
||||
func (o *OidcConfig) BeforeSave(tx *gorm.DB) (err error) {
|
||||
// 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息
|
||||
if tx.Statement.Changed("OidcDiscoveryUrl") {
|
||||
discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl)
|
||||
if tx.Statement.Changed("OidcDiscoveryUrl") || o.ID == 0 {
|
||||
logrus.Infof("Updating OIDC config for %s, OidcDiscoveryUrl: %s", o.Name, o.OidcDiscoveryUrl)
|
||||
discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl, o.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新OIDC配置失败: %w", err)
|
||||
logrus.Error("Updating OIDC config failed: ", err)
|
||||
return fmt.Errorf("updating OIDC config failed: %w", err)
|
||||
}
|
||||
o.Issuer = discoveryResp.Issuer
|
||||
o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint
|
||||
|
@ -1,16 +1,57 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
import (
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"` // 发布者的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
Title string `gorm:"type:text;not null"` // 帖子标题
|
||||
Content string `gorm:"type:text;not null"` // 帖子内容
|
||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||
IsPrivate bool `gorm:"default:false"` // 是否为私密帖子
|
||||
UserID uint `gorm:"index"` // 发布者的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
Title string `gorm:"type:text;not null"` // 帖子标题
|
||||
Content string `gorm:"type:text;not null"` // 帖子内容
|
||||
CategoryID uint `gorm:"index"` // 帖子分类ID
|
||||
Category Category `gorm:"foreignKey:CategoryID;references:ID"` // 关联的分类
|
||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||
IsPrivate bool `gorm:"default:false"` // 是否为私密帖子
|
||||
LikeCount uint64
|
||||
CommentCount uint64
|
||||
VisitorCount uint64
|
||||
ViewCount uint64
|
||||
Heat uint64 `gorm:"default:0"`
|
||||
}
|
||||
|
||||
// CalculateHeat 热度计算
|
||||
func (p *Post) CalculateHeat() float64 {
|
||||
return float64(
|
||||
p.LikeCount*constant.HeatFactorLikeWeight +
|
||||
p.CommentCount*constant.HeatFactorCommentWeight +
|
||||
p.ViewCount*constant.HeatFactorViewWeight,
|
||||
)
|
||||
}
|
||||
|
||||
// AfterUpdate 热度指标更新后更新热度
|
||||
func (p *Post) AfterUpdate(tx *gorm.DB) (err error) {
|
||||
if tx.Statement.Changed("LikeCount") || tx.Statement.Changed("CommentCount") || tx.Statement.Changed("ViewCount") {
|
||||
p.Heat = uint64(p.CalculateHeat())
|
||||
if err := tx.Model(p).Update("heat", p.Heat).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) ToDto() dto.PostDto {
|
||||
return dto.PostDto{
|
||||
ID: p.ID,
|
||||
UserID: p.UserID,
|
||||
Title: p.Title,
|
||||
Content: p.Content,
|
||||
IsPrivate: p.IsPrivate,
|
||||
LikeCount: p.LikeCount,
|
||||
CommentCount: p.CommentCount,
|
||||
ViewCount: p.ViewCount,
|
||||
Heat: p.Heat,
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,7 @@ func initSQLite(path string, gormConfig *gorm.Config) (*gorm.DB, error) {
|
||||
|
||||
func migrate() error {
|
||||
return GetDB().AutoMigrate(
|
||||
&model.Category{},
|
||||
&model.Comment{},
|
||||
&model.Label{},
|
||||
&model.Like{},
|
||||
|
@ -62,7 +62,7 @@ func (o *oidcRepo) UpdateOidcConfig(oidcConfig *model.OidcConfig) error {
|
||||
if oidcConfig.ID == 0 {
|
||||
return errs.New(http.StatusBadRequest, "invalid OIDC config ID", nil)
|
||||
}
|
||||
if err := GetDB().Updates(oidcConfig).Error; err != nil {
|
||||
if err := GetDB().Select("Enabled").Updates(oidcConfig).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -2,8 +2,10 @@ package repo
|
||||
|
||||
import (
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/pkg/errs"
|
||||
"net/http"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type postRepo struct{}
|
||||
@ -45,9 +47,34 @@ func (p *postRepo) UpdatePost(post *model.Post) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *postRepo) ListPosts(limit, offset int) ([]model.Post, error) {
|
||||
func (p *postRepo) ListPosts(currentUserID uint, keywords []string, page, size uint64, orderedBy string, reverse bool) ([]model.Post, error) {
|
||||
var posts []model.Post
|
||||
if err := GetDB().Limit(limit).Offset(offset).Find(&posts).Error; err != nil {
|
||||
if !slices.Contains(constant.OrderedByEnumPost, orderedBy) {
|
||||
return nil, errs.New(http.StatusBadRequest, "invalid ordered_by parameter", nil)
|
||||
}
|
||||
order := orderedBy
|
||||
if reverse {
|
||||
order += " ASC"
|
||||
} else {
|
||||
order += " DESC"
|
||||
}
|
||||
query := GetDB().Model(&model.Post{}).Preload("User")
|
||||
if currentUserID > 0 {
|
||||
query = query.Where("is_private = ? OR (is_private = ? AND user_id = ?)", false, true, currentUserID)
|
||||
} else {
|
||||
query = query.Where("is_private = ?", false)
|
||||
}
|
||||
if len(keywords) > 0 {
|
||||
for _, keyword := range keywords {
|
||||
if keyword != "" {
|
||||
// 使用LIKE进行模糊匹配,搜索标题、内容和标签
|
||||
query = query.Where("title LIKE ? OR content LIKE ? OR tags LIKE ?",
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
}
|
||||
}
|
||||
query = query.Order(order).Offset(int((page - 1) * size)).Limit(int(size))
|
||||
if err := query.Find(&posts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return posts, nil
|
||||
|
@ -18,6 +18,7 @@ func registerUserRoutes(group *route.RouterGroup) {
|
||||
userGroupWithoutAuth.GET("/oidc/list", userController.OidcList)
|
||||
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
||||
userGroupWithoutAuth.GET("/u/:id", userController.GetUser)
|
||||
userGroup.GET("/u", userController.GetUser)
|
||||
userGroup.POST("/logout", userController.Logout)
|
||||
userGroup.PUT("/u/:id", userController.UpdateUser)
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ func (c *AdminService) CreateOidcConfig(req *dto.AdminOidcConfigDto) error {
|
||||
ClientSecret: req.ClientSecret,
|
||||
OidcDiscoveryUrl: req.OidcDiscoveryUrl,
|
||||
Enabled: req.Enabled,
|
||||
Type: req.Type,
|
||||
}
|
||||
return repo.Oidc.CreateOidcConfig(oidcConfig)
|
||||
}
|
||||
@ -70,6 +71,7 @@ func (c *AdminService) UpdateOidcConfig(req *dto.AdminOidcConfigDto) error {
|
||||
ClientSecret: req.ClientSecret,
|
||||
OidcDiscoveryUrl: req.OidcDiscoveryUrl,
|
||||
Enabled: req.Enabled,
|
||||
Type: req.Type,
|
||||
}
|
||||
return repo.Oidc.UpdateOidcConfig(oidcConfig)
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/errs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PostService struct{}
|
||||
@ -21,15 +20,22 @@ func (p *PostService) CreatePost(ctx context.Context, req *dto.CreateOrUpdatePos
|
||||
if currentUser == nil {
|
||||
return errs.ErrUnauthorized
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
UserID: currentUser.ID,
|
||||
Labels: req.Labels,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
UserID: currentUser.ID,
|
||||
Labels: func() []model.Label {
|
||||
labelModels := make([]model.Label, 0)
|
||||
for _, labelID := range req.Labels {
|
||||
labelModel, err := repo.Label.GetLabelByID(labelID)
|
||||
if err == nil {
|
||||
labelModels = append(labelModels, *labelModel)
|
||||
}
|
||||
}
|
||||
return labelModels
|
||||
}(),
|
||||
IsPrivate: req.IsPrivate,
|
||||
}
|
||||
|
||||
if err := repo.Post.CreatePost(post); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -37,10 +43,103 @@ func (p *PostService) CreatePost(ctx context.Context, req *dto.CreateOrUpdatePos
|
||||
}
|
||||
|
||||
func (p *PostService) DeletePost(ctx context.Context, id string) error {
|
||||
currentUser := ctxutils.GetCurrentUser(ctx)
|
||||
if currentUser == nil {
|
||||
return errs.ErrUnauthorized
|
||||
}
|
||||
if id == "" {
|
||||
return errs.ErrBadRequest
|
||||
}
|
||||
post, err := repo.Post.GetPostByID(id)
|
||||
if err != nil {
|
||||
return errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||
}
|
||||
if post.UserID != currentUser.ID {
|
||||
return errs.ErrForbidden
|
||||
}
|
||||
if err := repo.Post.DeletePost(id); err != nil {
|
||||
return errs.ErrInternalServer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PostService) GetPost(ctx context.Context, id string) (*model.Post, error) {}
|
||||
func (p *PostService) GetPost(ctx context.Context, id string) (*dto.PostDto, error) {
|
||||
currentUser := ctxutils.GetCurrentUser(ctx)
|
||||
if currentUser == nil {
|
||||
return nil, errs.ErrUnauthorized
|
||||
}
|
||||
if id == "" {
|
||||
return nil, errs.ErrBadRequest
|
||||
}
|
||||
post, err := repo.Post.GetPostByID(id)
|
||||
if err != nil {
|
||||
return nil, errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||
}
|
||||
if post.IsPrivate && post.UserID != currentUser.ID {
|
||||
return nil, errs.ErrForbidden
|
||||
}
|
||||
return &dto.PostDto{
|
||||
UserID: post.UserID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Labels: func() []dto.LabelDto {
|
||||
labelDtos := make([]dto.LabelDto, 0)
|
||||
for _, label := range post.Labels {
|
||||
labelDtos = append(labelDtos, label.ToDto())
|
||||
}
|
||||
return labelDtos
|
||||
}(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PostService) UpdatePost(req *dto.CreateOrUpdatePostReq) error {}
|
||||
func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.CreateOrUpdatePostReq) error {
|
||||
currentUser := ctxutils.GetCurrentUser(ctx)
|
||||
if currentUser == nil {
|
||||
return errs.ErrUnauthorized
|
||||
}
|
||||
if id == "" {
|
||||
return errs.ErrBadRequest
|
||||
}
|
||||
post, err := repo.Post.GetPostByID(id)
|
||||
if err != nil {
|
||||
return errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||
}
|
||||
if post.UserID != currentUser.ID {
|
||||
return errs.ErrForbidden
|
||||
}
|
||||
post.Title = req.Title
|
||||
post.Content = req.Content
|
||||
post.IsPrivate = req.IsPrivate
|
||||
post.Labels = func() []model.Label {
|
||||
labelModels := make([]model.Label, len(req.Labels))
|
||||
for _, labelID := range req.Labels {
|
||||
labelModel, err := repo.Label.GetLabelByID(labelID)
|
||||
if err == nil {
|
||||
labelModels = append(labelModels, *labelModel)
|
||||
}
|
||||
}
|
||||
return labelModels
|
||||
}()
|
||||
if err := repo.Post.UpdatePost(post); err != nil {
|
||||
return errs.ErrInternalServer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PostService) ListPosts() {}
|
||||
func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) (*dto.ListPostResp, error) {
|
||||
postDtos := make([]dto.PostDto, 0)
|
||||
currentUserID := ctxutils.GetCurrentUserID(ctx)
|
||||
posts, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Page, req.Size, req.OrderedBy, req.Reverse)
|
||||
if err != nil {
|
||||
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list posts", err)
|
||||
}
|
||||
for _, post := range posts {
|
||||
postDtos = append(postDtos, post.ToDto())
|
||||
}
|
||||
return &dto.ListPostResp{
|
||||
Posts: postDtos,
|
||||
Total: uint64(len(posts)),
|
||||
OrderedBy: req.OrderedBy,
|
||||
Reverse: req.Reverse,
|
||||
}, nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
@ -25,6 +26,10 @@ func NewUserService() *UserService {
|
||||
func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, error) {
|
||||
user, err := repo.User.GetUserByUsernameOrEmail(req.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logrus.Warnf("User not found: %s", req.Username)
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
if user == nil {
|
||||
@ -92,6 +97,15 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
// 创建默认管理员账户
|
||||
if newUser.ID == 1 {
|
||||
newUser.Role = constant.RoleAdmin
|
||||
err = repo.User.UpdateUser(newUser)
|
||||
if err != nil {
|
||||
logrus.Errorln("Failed to update user role to admin:", err)
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
}
|
||||
// 生成访问令牌和刷新令牌
|
||||
token, refreshToken, err := s.generate2Token(newUser.ID)
|
||||
if err != nil {
|
||||
@ -137,17 +151,40 @@ func (s *UserService) ListOidcConfigs() (*dto.ListOidcConfigResp, error) {
|
||||
state := utils.Strings.GenerateRandomString(32)
|
||||
kvStore := utils.KV.GetInstance()
|
||||
kvStore.Set(constant.KVKeyOidcState+state, oidcConfig.Name, 5*time.Minute)
|
||||
loginUrl := utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{
|
||||
"client_id": oidcConfig.ClientID,
|
||||
"redirect_uri": fmt.Sprintf("%s%s%s/%sREDIRECT_BACK", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的
|
||||
strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"),
|
||||
constant.ApiSuffix,
|
||||
constant.OidcUri,
|
||||
oidcConfig.Name,
|
||||
),
|
||||
"response_type": "code",
|
||||
"scope": "openid email profile",
|
||||
"state": state,
|
||||
})
|
||||
|
||||
if oidcConfig.Type == constant.OidcProviderTypeMisskey {
|
||||
// Misskey OIDC 特殊处理
|
||||
loginUrl = utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{
|
||||
"client_id": oidcConfig.ClientID,
|
||||
"redirect_uri": fmt.Sprintf("%s%s%s/%s", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的
|
||||
strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"),
|
||||
constant.ApiSuffix,
|
||||
constant.OidcUri,
|
||||
oidcConfig.Name,
|
||||
),
|
||||
"response_type": "code",
|
||||
"scope": "read:account",
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
oidcConfigsDtos = append(oidcConfigsDtos, dto.UserOidcConfigDto{
|
||||
Name: oidcConfig.Name,
|
||||
DisplayName: oidcConfig.DisplayName,
|
||||
Icon: oidcConfig.Icon,
|
||||
LoginUrl: utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{
|
||||
"client_id": oidcConfig.ClientID,
|
||||
"redirect_uri": strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/") + constant.OidcUri + oidcConfig.Name,
|
||||
"response_type": "code",
|
||||
"scope": "openid email profile",
|
||||
"state": state,
|
||||
}),
|
||||
LoginUrl: loginUrl,
|
||||
})
|
||||
}
|
||||
return &dto.ListOidcConfigResp{
|
||||
@ -190,7 +227,7 @@ func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, erro
|
||||
|
||||
// 绑定过登录
|
||||
userOpenID, err := repo.User.GetUserOpenIDByIssuerAndSub(oidcConfig.Issuer, userInfo.Sub)
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
if userOpenID != nil {
|
||||
@ -212,7 +249,7 @@ func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, erro
|
||||
} else {
|
||||
// 若没有绑定过登录,则先通过邮箱查找用户,若没有再创建新用户
|
||||
user, err := repo.User.GetUserByEmail(userInfo.Email)
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logrus.Errorln("Failed to get user by email:", err)
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
package tasks
|
||||
|
||||
// ClearSessionDaemon 定时任务:清理过期会话
|
||||
func ClearSessionDaemon() {}
|
||||
|
@ -1 +1,6 @@
|
||||
package tasks
|
||||
|
||||
func RunTaskManager() {
|
||||
// 启动任务管理器
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user