恢复 #12 from LiteyukiStudio/revert-11-main
Some checks failed
Deploy VitePress site to Spage / build (push) Failing after 49s
Build & Publish All-in-One / Build Frontend (push) Failing after 1m27s
Build & Publish All-in-One / Build Backend (386, freebsd, 386) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm, 5, linux, armv5) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm, 6, linux, armv6) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm, 7, linux, armv7) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm, freebsd, arm) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm64, darwin, arm64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm64, freebsd, arm64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm64, linux, arm64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (arm64, windows, arm64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (loong64, linux, loong64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (mips, linux, mips) (push) Has been skipped
Build & Publish All-in-One / Build Backend (mips64, linux, mips64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (mips64le, linux, mips64le) (push) Has been skipped
Build & Publish All-in-One / Build Backend (mipsle, linux, mipsle) (push) Has been skipped
Build & Publish All-in-One / Build Backend (ppc64, linux, ppc64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (ppc64le, linux, ppc64le) (push) Has been skipped
Build & Publish All-in-One / Build Backend (riscv64, linux, riscv64) (push) Has been skipped
Build & Publish All-in-One / Build Backend (s390x, linux, s390x) (push) Has been skipped
Build & Publish All-in-One / Build Backend (sse2, 386, linux, 386-sse2) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v1, amd64, darwin, amd64-v1) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v1, amd64, freebsd, amd64-v1) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v1, amd64, linux, amd64-v1) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v1, amd64, windows, amd64-v1) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v3, amd64, darwin, amd64-v3) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v3, amd64, freebsd, amd64-v3) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v3, amd64, linux, amd64-v3) (push) Has been skipped
Build & Publish All-in-One / Build Backend (v3, amd64, windows, amd64-v3) (push) Has been skipped
Build & Publish All-in-One / Build & Publish Container Images (push) Has been skipped
Build & Publish All-in-One / Publish GitHub Release (push) Has been skipped

 恢复 "🗑️ 删除不必要的英文注释"
This commit is contained in:
Nanaloveyuki
2025-06-18 18:40:43 +08:00
committed by GitHub
32 changed files with 348 additions and 212 deletions

View File

@ -12,6 +12,7 @@ type AdminApi struct{}
var Admin = AdminApi{}
// CreateUser 创建用户
// Create User
func (AdminApi) CreateUser(ctx context.Context, c *app.RequestContext) {
var userDTO *UserDTO
err := c.BindJSON(&userDTO)

View File

@ -17,6 +17,7 @@ type OrgApi struct{}
var Org = OrgApi{}
// OrganizationDTO 组织信息数据传输对象
// Organization Information Data Transfer Object (DTO)
func getOrg(ctx context.Context) *models.Organization {
org, ok := ctx.Value("userOrg").(*models.Organization)
if !ok {
@ -26,6 +27,7 @@ func getOrg(ctx context.Context) *models.Organization {
}
// UserOrgAuth 组织权限检查
// Organization permission check
func (OrgApi) UserOrgAuth(ctx context.Context, c *app.RequestContext) {
user := middle.Auth.GetUser(ctx, c)
// 当 id 为空的 POST 请求默认为 create
@ -47,7 +49,7 @@ func (OrgApi) UserOrgAuth(ctx context.Context, c *app.RequestContext) {
return
}
// 判断权限 (GET 请求需要用户权限,其他请求需要管理员权限)
// Determine permissions (GET requests require user permissions, other requests require admin permissions)
var authType string
if string(c.Method()) == "GET" {
authType = "member"
@ -65,6 +67,7 @@ func (OrgApi) UserOrgAuth(ctx context.Context, c *app.RequestContext) {
}
// OrganizationDTO 组织信息数据传输对象
// Organization Information Data Transfer Object (DTO)
func (OrgApi) ToDTO(org *models.Organization) OrganizationDTO {
return OrganizationDTO{
ID: org.ID,
@ -78,22 +81,23 @@ func (OrgApi) ToDTO(org *models.Organization) OrganizationDTO {
}
// CreateOrganization 创建组织
// Create Organization
func (OrgApi) CreateOrganization(ctx context.Context, c *app.RequestContext) {
// 绑定参数
// Bind parameters
req := &CreateOrgReq{}
if err := c.BindAndValidate(&req); err != nil {
resps.BadRequest(c, resps.ParameterError)
return
}
// 检验组织名称是否存在
// Check if the organization name already exists
if store.Org.OrgNameIsExist(req.Name) {
resps.BadRequest(c, "organization name already exists")
return
}
// 创建组织
// Create organization
user := middle.Auth.GetUser(ctx, c)
org := models.Organization{
Name: req.Name,
@ -114,6 +118,7 @@ func (OrgApi) CreateOrganization(ctx context.Context, c *app.RequestContext) {
}
// UpdateOrganization 更新组织信息
// Update Organization Information
func (OrgApi) UpdateOrganization(ctx context.Context, c *app.RequestContext) {
req := &UpdateOrgReq{}
if err := c.BindAndValidate(&req); err != nil {
@ -121,7 +126,7 @@ func (OrgApi) UpdateOrganization(ctx context.Context, c *app.RequestContext) {
return
}
org := getOrg(ctx)
// 更新
// 更新 Update
org.DisplayName = req.DisplayName
org.Email = req.Email
org.Description = *req.Description
@ -136,6 +141,7 @@ func (OrgApi) UpdateOrganization(ctx context.Context, c *app.RequestContext) {
}
// GetOrganizationProject 获取组织项目
// Get Organization Projects
func (OrgApi) GetOrganizationProject(ctx context.Context, c *app.RequestContext) {
req := &GetOrgProjectReq{}
if err := c.BindAndValidate(&req); err != nil {
@ -143,7 +149,7 @@ func (OrgApi) GetOrganizationProject(ctx context.Context, c *app.RequestContext)
return
}
org := getOrg(ctx)
// 查询
// 查询 Query
projects, total, err := store.Project.ListByOwner(constants.OwnerTypeOrg, strconv.Itoa(int(org.ID)), req.Page, req.Limit)
if err != nil {
resps.InternalServerError(c, "Failed to get projects")
@ -161,6 +167,7 @@ func (OrgApi) GetOrganizationProject(ctx context.Context, c *app.RequestContext)
}
// DeleteOrganization 删除组织
// Delete Organization
func (OrgApi) DeleteOrganization(ctx context.Context, c *app.RequestContext) {
org := getOrg(ctx)
// 删除组织
@ -172,6 +179,7 @@ func (OrgApi) DeleteOrganization(ctx context.Context, c *app.RequestContext) {
}
// GetOrganization 获取组织信息
// Get Organization Information
func (OrgApi) GetOrganization(ctx context.Context, c *app.RequestContext) {
org := getOrg(ctx)
resps.Ok(c, resps.OK, map[string]any{
@ -180,6 +188,7 @@ func (OrgApi) GetOrganization(ctx context.Context, c *app.RequestContext) {
}
// GetOrganizationUsers 获取组织用户
// Get Organization Users
func (OrgApi) GetOrganizationUsers(ctx context.Context, c *app.RequestContext) {
org := getOrg(ctx)
resps.Ok(c, resps.OK, map[string]any{
@ -199,19 +208,20 @@ func (OrgApi) GetOrganizationUsers(ctx context.Context, c *app.RequestContext) {
}
// AddOrganizationUser 添加组织用户
// Add Organization User
func (OrgApi) AddOrganizationUser(ctx context.Context, c *app.RequestContext) {
req := &OrgUserReq{}
if err := c.BindAndValidate(&req); err != nil {
resps.BadRequest(c, resps.ParameterError)
return
}
// 查询
// 查询 Query
user, err := store.User.GetByID(req.UserID)
if err != nil || user == nil {
resps.NotFound(c, resps.TargetNotFound)
return
}
// 新增
// 新增 Add
org := getOrg(ctx)
if req.Role == "member" {
org.Members = append(org.Members, user)
@ -230,19 +240,20 @@ func (OrgApi) AddOrganizationUser(ctx context.Context, c *app.RequestContext) {
}
// DeleteOrganizationUser 删除组织用户
// Delete Organization User
func (OrgApi) DeleteOrganizationUser(ctx context.Context, c *app.RequestContext) {
req := &OrgUserReq{}
if err := c.BindAndValidate(&req); err != nil {
resps.BadRequest(c, resps.ParameterError)
return
}
// 查询
// 查询 Query
user, err := store.User.GetByID(req.UserID)
if err != nil || user == nil {
resps.NotFound(c, resps.TargetNotFound)
return
}
// 删除
// 删除 Delete
org := getOrg(ctx)
if req.Role == "member" {
for i, member := range org.Members {

View File

@ -1,12 +1,13 @@
package handlers
// OrganizationDTO 组织信息数据传输对象
// Organization Information Data Transfer Object (DTO)
type OrganizationDTO struct {
ID uint `json:"id"` // 组织ID
Name string `json:"name"` // 组织名称
DisplayName *string `json:"display_name"` // 显示名称
Email *string `json:"email"` // 邮箱地址
Description string `json:"description"` // 描述信息
ID uint `json:"id"` // 组织ID Organization ID
Name string `json:"name"` // 组织名称 Organization Name
DisplayName *string `json:"display_name"` // 显示名称 Display Name
Email *string `json:"email"` // 邮箱地址 Email Address
Description string `json:"description"` // 描述信息 Description
AvatarURL *string `json:"avatar_url"` // 头像URL Avatar URL
ProjectLimit int `json:"project_limit"` // 项目数量限制 Project Limit
Members []UserDTO `json:"members"` // 组织成员 Members

View File

@ -18,6 +18,7 @@ type ProjectApi struct {
var Project = ProjectApi{}
// ProjectDTO 项目信息数据传输对象
// Project Information Data Transfer Object (DTO)
func (ProjectApi) toDTO(project *models.Project, full bool) ProjectDTO {
projectDto := ProjectDTO{
Description: project.Description,
@ -40,6 +41,7 @@ func (ProjectApi) toDTO(project *models.Project, full bool) ProjectDTO {
}
// GetProject 获取项目信息
// Get project information
func getProject(ctx context.Context) *models.Project {
project, ok := ctx.Value("userProject").(*models.Project)
if !ok {
@ -49,6 +51,7 @@ func getProject(ctx context.Context) *models.Project {
}
// UserProjectAuth 用户项目权限认证
// User project authorization
func (ProjectApi) UserProjectAuth(ctx context.Context, c *app.RequestContext) {
user := middle.Auth.GetUser(ctx, c)
projectIdStr := c.Param("id")
@ -69,12 +72,12 @@ func (ProjectApi) UserProjectAuth(ctx context.Context, c *app.RequestContext) {
c.Abort()
return
}
// 项目权限判断
// 项目权限判断 Project authorization check
if store.Project.UserIsOwner(project, user.ID) {
context.WithValue(ctx, "userProject", project)
return
}
// 组织权限判断
// 组织权限判断 Organization authorization check
if project.OwnerType == constants.OwnerTypeOrg {
// 组织查询
org, err := store.Org.GetOrgById(project.OwnerID)
@ -106,6 +109,7 @@ func (ProjectApi) UserProjectAuth(ctx context.Context, c *app.RequestContext) {
}
// Create 创建项目
// Create project
func (ProjectApi) Create(ctx context.Context, c *app.RequestContext) {
req := CreateProjectReq{}
if err := c.BindAndValidate(&req); err != nil {
@ -113,10 +117,10 @@ func (ProjectApi) Create(ctx context.Context, c *app.RequestContext) {
return
}
user := middle.Auth.GetUser(ctx, c)
// 校验权限
// 校验权限 Check permissions
if req.OwnerType == constants.OwnerTypeOrg {
// 如果为组织,需要具有组织管理员权限
// If it is an organization, you need to have the organization administrator permission
org, err := store.Org.GetOrgById(req.OwnerID)
if err != nil || org == nil {
resps.InternalServerError(c, resps.ParameterError)
@ -127,7 +131,7 @@ func (ProjectApi) Create(ctx context.Context, c *app.RequestContext) {
}
} else if req.OwnerType == constants.OwnerTypeUser {
// 如果为用户,仅允许为自己添加
// If it is a user, only allow adding for user-self
req.OwnerID = user.ID
} else {
resps.BadRequest(c, resps.ParameterError)
@ -151,6 +155,7 @@ func (ProjectApi) Create(ctx context.Context, c *app.RequestContext) {
}
// Update 更新项目
// Update project
func (ProjectApi) Update(ctx context.Context, c *app.RequestContext) {
req := UpdateProjectReq{}
if err := c.BindAndValidate(&req); err != nil {
@ -162,7 +167,7 @@ func (ProjectApi) Update(ctx context.Context, c *app.RequestContext) {
resps.NotFound(c, resps.TargetNotFound)
return
}
// 更新数据
// 更新数据 Update data
project.Description = *req.Description
project.DisplayName = req.DisplayName
project.Name = *req.Name
@ -176,6 +181,7 @@ func (ProjectApi) Update(ctx context.Context, c *app.RequestContext) {
}
// Delete 删除项目
// Delete project
func (ProjectApi) Delete(ctx context.Context, c *app.RequestContext) {
project := getProject(ctx)
if project == nil {
@ -190,6 +196,7 @@ func (ProjectApi) Delete(ctx context.Context, c *app.RequestContext) {
}
// Info 获取项目信息
// Get project information
func (ProjectApi) Info(ctx context.Context, c *app.RequestContext) {
project := getProject(ctx)
if project == nil {
@ -202,6 +209,7 @@ func (ProjectApi) Info(ctx context.Context, c *app.RequestContext) {
}
// GetOwners 获取项目所有者列表
// Get project owner list
func (ProjectApi) GetOwners(ctx context.Context, c *app.RequestContext) {
project := getProject(ctx)
if project == nil {
@ -219,6 +227,7 @@ func (ProjectApi) GetOwners(ctx context.Context, c *app.RequestContext) {
}
// AddOwner 添加项目所有者
// Add project owner
func (ProjectApi) AddOwner(ctx context.Context, c *app.RequestContext) {
project := getProject(ctx)
if project == nil {
@ -236,14 +245,14 @@ func (ProjectApi) AddOwner(ctx context.Context, c *app.RequestContext) {
resps.NotFound(c, resps.TargetNotFound)
return
}
// 判断用户是否已存在权限列表
// 判断用户是否已存在权限列表 Check if the user already exists in the permission list
for _, owner := range project.Owners {
if owner.ID == user.ID {
resps.BadRequest(c, "User already exists in the permission list")
return
}
}
// 添加用户
// 添加用户 Add user
if err := store.Project.AddOwner(project, user); err != nil {
resps.InternalServerError(c, resps.ParameterError)
return
@ -254,6 +263,7 @@ func (ProjectApi) AddOwner(ctx context.Context, c *app.RequestContext) {
}
// DeleteOwner 删除项目所有者
// Delete project owner
func (ProjectApi) DeleteOwner(ctx context.Context, c *app.RequestContext) {
project := getProject(ctx)
if project == nil {
@ -265,13 +275,13 @@ func (ProjectApi) DeleteOwner(ctx context.Context, c *app.RequestContext) {
resps.BadRequest(c, resps.ParameterError)
return
}
// 查询用户
// 查询用户 Query user
user, err := store.User.GetByID(req.UserID)
if err != nil || user == nil {
resps.NotFound(c, resps.TargetNotFound)
return
}
// 删除用户
// 删除用户 Delete user
if err := store.Project.DeleteOwner(project, user); err != nil {
resps.InternalServerError(c, resps.ParameterError)
return
@ -282,6 +292,7 @@ func (ProjectApi) DeleteOwner(ctx context.Context, c *app.RequestContext) {
}
// GetSites 获取站点列表
// Get site list
func (ProjectApi) GetSites(ctx context.Context, c *app.RequestContext) {
req := GetSiteListReq{}
if err := c.BindAndValidate(&req); err != nil {

View File

@ -1,43 +1,48 @@
package handlers
// ProjectDTO 项目信息数据传输对象
// Project Information Data Transfer Object (DTO)
type ProjectDTO struct {
ID uint `json:"id"` // 项目ID
Name string `json:"name"` // 项目名称
DisplayName *string `json:"display_name"` // 项目显示名称
Description string `json:"description"` // 项目描述
OwnerType string `json:"owner_type"` // 项目拥有者类型
OwnerID uint `json:"owner_id"` // 项目拥有者ID
Owners []UserDTO `json:"owners"` // 项目拥有者列表
SiteLimit int `json:"site_limit"` // 项目站点数量限制
ID uint `json:"id"` // 项目ID Project ID
Name string `json:"name"` // 项目名称 Project Name
DisplayName *string `json:"display_name"` // 项目显示名称 Project Display Name
Description string `json:"description"` // 项目描述 Project Description
OwnerType string `json:"owner_type"` // 项目拥有者类型 Project Owner Type
OwnerID uint `json:"owner_id"` // 项目拥有者ID Project Owner ID
Owners []UserDTO `json:"owners"` // 项目拥有者列表 Project Owner List
SiteLimit int `json:"site_limit"` // 项目站点数量限制 Project Site Limit
}
// CreateProjectReq 创建项目请求参数
// Create Project Request Parameters
type CreateProjectReq struct {
Name string `json:"name" binding:"required"` // 项目名称
DisplayName *string `json:"display_name"` // 项目显示名称
Description string `json:"description"` // 项目描述
OwnerType string `json:"owner_type" binding:"required" vd:"in($,'user','organization')"` // 项目拥有者类型
OwnerID uint `json:"owner_id" binding:"required"` // 项目拥有者ID
Name string `json:"name" binding:"required"` // 项目名称 Project Name
DisplayName *string `json:"display_name"` // 项目显示名称 Project Display Name
Description string `json:"description"` // 项目描述 Project Description
OwnerType string `json:"owner_type" binding:"required" vd:"in($,'user','organization')"` // 项目拥有者类型 Project Owner Type
OwnerID uint `json:"owner_id" binding:"required"` // 项目拥有者ID Project Owner ID
}
// UpdateProjectReq 更新项目请求参数
// Update Project Request Parameters
type UpdateProjectReq struct {
Name *string `json:"name"` // 项目名称
DisplayName *string `json:"display_name"` // 项目显示名称
Description *string `json:"description"` // 项目描述
Name *string `json:"name"` // 项目名称 Project Name
DisplayName *string `json:"display_name"` // 项目显示名称 Project Display Name
Description *string `json:"description"` // 项目描述 Project Description
}
// ProjectUserReq 项目用户请求参数
// Project User Request Parameters
type ProjectUserReq struct {
UserID uint `json:"user_id" binding:"required"` // 用户ID
UserID uint `json:"user_id" binding:"required"` // 用户ID User ID
}
// GetProjectListReq 获取项目列表请求参数
// Get Project List Request Parameters
type GetSiteListReq struct {
Page int `form:"page" binding:"required"` // 页码
Limit int `form:"limit" binding:"required"` // 每页数量
Project string `form:"project" binding:"required"` // 项目名称
SiteName string `form:"site_name"` // 站点名称
Page int `form:"page" binding:"required"` // 页码 Page
Limit int `form:"limit" binding:"required"` // 每页数量 Page Limit
Project string `form:"project" binding:"required"` // 项目名称 Project Name
SiteName string `form:"site_name"` // 站点名称 Site Name
}

View File

@ -2,10 +2,9 @@ package handlers
import (
"context"
"strconv"
"github.com/LiteyukiStudio/spage/spage/models"
"github.com/LiteyukiStudio/spage/spage/store"
"strconv"
"github.com/LiteyukiStudio/spage/resps"
"github.com/cloudwego/hertz/pkg/app"
@ -17,6 +16,7 @@ type SiteApi struct {
var Site = SiteApi{}
// ToDTO 站点信息数据传输对象
// Site Information Data Transfer Object (DTO)
func (SiteApi) ToDTO(site *models.Site, full bool) SiteDTO {
siteDTO := SiteDTO{
Description: site.Description,
@ -60,6 +60,7 @@ func (SiteApi) SiteAuth(ctx context.Context, c *app.RequestContext) {
}
// Create 创建站点
// Create Site
func (SiteApi) Create(ctx context.Context, c *app.RequestContext) {
req := CreateSiteReq{}
if err := c.BindAndValidate(&req); err != nil {

View File

@ -1,28 +1,30 @@
package handlers
// SiteDTO 网站详情
// Site Detail
type SiteDTO struct {
ID uint `json:"id"` // 网站ID
Name string `json:"name"` // 网站名称
Description string `json:"description"` // 网站描述
ProjectID uint `json:"project_id"` // 项目ID
Project ProjectDTO `json:"project"` // 项目详情
SubDomain *string `json:"sub_domain"` // 子域名
Domains []string `json:"domains"` // 域名
ID uint `json:"id"` // 网站ID WebSiteID
Name string `json:"name"` // 网站名称 WebSiteName
Description string `json:"description"` // 网站描述 WebSiteDescription
ProjectID uint `json:"project_id"` // 项目ID ProjectID
Project ProjectDTO `json:"project"` // 项目详情 ProjectDetail
SubDomain *string `json:"sub_domain"` // 子域名 SubDomain
Domains []string `json:"domains"` // 域名 Domains
}
// CreateSiteReq 创建网站请求参数
// Create Site Request Parameters
type CreateSiteReq struct {
Name string `json:"name" binding:"required"` // 网站名称
Description string `json:"description"` // 网站描述
ProjectID uint `json:"project_id" binding:"required"` // 项目ID
SubDomain *string `json:"sub_domain"` // 子域名
Domains []string `json:"domains"` // 域名
Name string `json:"name" binding:"required"` // 网站名称 WebSiteName
Description string `json:"description"` // 网站描述 WebSiteDescription
ProjectID uint `json:"project_id" binding:"required"` // 项目ID ProjectID
SubDomain *string `json:"sub_domain"` // 子域名 SubDomain
Domains []string `json:"domains"` // 域名 Domains
}
type UpdateSiteReq struct {
Name *string `json:"name"` // 网站名称
Description *string `json:"description"` // 网站描述
SubDomain *string `json:"sub_domain"` // 子域名
Domains []string `json:"domains"` // 域名
Name *string `json:"name"` // 网站名称 WebSiteName
Description *string `json:"description"` // 网站描述 WebSiteDescription
SubDomain *string `json:"sub_domain"` // 子域名 SubDomain
Domains []string `json:"domains"` // 域名 Domains
}

View File

@ -21,6 +21,7 @@ type UserApi struct{}
var User = UserApi{}
// UserDTO 用户信息数据传输对象
// User Information Data Transfer Object (DTO)
func (UserApi) ToDTO(user *models.User, self bool) UserDTO {
userDTO := UserDTO{
ID: user.ID,
@ -38,6 +39,7 @@ func (UserApi) ToDTO(user *models.User, self bool) UserDTO {
}
// Login 用户登录
// User login
func (UserApi) Login(ctx context.Context, c *app.RequestContext) {
loginReq := &LoginReq{}
// TODO: 这里需要验证验证码
@ -90,6 +92,7 @@ func (UserApi) Login(ctx context.Context, c *app.RequestContext) {
}
// Logout 用户登出
// User logout
func (UserApi) Logout(ctx context.Context, c *app.RequestContext) {
// 删除cookie
c.SetCookie("token", "", -1, "/", "", protocol.CookieSameSiteLaxMode, true, true)
@ -98,6 +101,7 @@ func (UserApi) Logout(ctx context.Context, c *app.RequestContext) {
}
// GetCaptcha 获取验证码
// Get captcha
func (UserApi) GetCaptcha(ctx context.Context, c *app.RequestContext) {
resps.Ok(c, "ok", map[string]any{
"provider": config.CaptchaType,
@ -107,6 +111,7 @@ func (UserApi) GetCaptcha(ctx context.Context, c *app.RequestContext) {
}
// GetUserOrgs 获取用户的组织
// Get user organizations
func (UserApi) GetOrgs(ctx context.Context, c *app.RequestContext) {
userID := c.Param("id")
crtUser := middle.Auth.GetUser(ctx, c)
@ -133,6 +138,7 @@ func (UserApi) GetOrgs(ctx context.Context, c *app.RequestContext) {
}
// GetUserProjects 获取用户的项目
// Get user projects
func (UserApi) GetProjects(ctx context.Context, c *app.RequestContext) {
userID := c.Param("id")
crtUser := middle.Auth.GetUser(ctx, c)
@ -157,6 +163,7 @@ func (UserApi) GetProjects(ctx context.Context, c *app.RequestContext) {
}
// GetUser 获取用户信息
// Get user information
func (UserApi) GetUser(ctx context.Context, c *app.RequestContext) {
userID := c.Param("id")
crtUser := middle.Auth.GetUser(ctx, c)
@ -177,6 +184,7 @@ func (UserApi) GetUser(ctx context.Context, c *app.RequestContext) {
}
// Register 用户注册
// User registration
func (UserApi) Register(ctx context.Context, c *app.RequestContext) {
// 接收参数
request := &RegisterReq{}
@ -221,6 +229,7 @@ func (UserApi) Register(ctx context.Context, c *app.RequestContext) {
}
// UpdateUser 更新用户信息
// Update user information
func (UserApi) UpdateUser(ctx context.Context, c *app.RequestContext) {
userDTO := &UserDTO{}
if err := c.BindJSON(userDTO); err != nil {

View File

@ -1,30 +1,33 @@
package handlers
// RegisterReq 注册请求结构体
// Registration request structure
type RegisterReq struct {
Username string `json:"username" binding:"required"` // 用户名
Password string `json:"password" binding:"required"` // 密码
Email string `json:"email" binding:"required"` // 邮箱
Username string `json:"username" binding:"required"` // 用户名 Username
Password string `json:"password" binding:"required"` // 密码 Password
Email string `json:"email" binding:"required"` // 邮箱 Email
}
// LoginReq 登录请求结构体
// Login request structure
type LoginReq struct {
Username string `json:"username" binding:"required"` // 用户名
Password string `json:"password" binding:"required"` // 密码
CaptchaToken string `json:"captcha_token" binding:"required"` // 验证码
Username string `json:"username" binding:"required"` // 用户名 Username
Password string `json:"password" binding:"required"` // 密码 Password
CaptchaToken string `json:"captcha_token" binding:"required"` // 验证码 Token
}
// OrganizationDTO 组织信息数据传输对象
// Organization Information Data Transfer Object (DTO)
type UserDTO struct {
ID uint `json:"id"` // 用户ID
Name string `json:"name"` // 用户名
DisplayName *string `json:"display_name"` // 显示名称
Email *string `json:"email"` // 邮箱
Description string `json:"description"` // 描述
Avatar *string `json:"avatar_url"` // 头像
Role string `json:"role"` // 角色
Organizations []OrganizationDTO `json:"organizations"` // 组织
Language string `json:"language"` // 语言
//Password string `json:"password"` // 密码
ID uint `json:"id"` // 用户ID User ID
Name string `json:"name"` // 用户名 Username
DisplayName *string `json:"display_name"` // 显示名称 DisplayName
Email *string `json:"email"` // 邮箱 Email
Description string `json:"description"` // 描述 Description
Avatar *string `json:"avatar_url"` // 头像 Avatar URL
Role string `json:"role"` // 角色 Role
Organizations []OrganizationDTO `json:"organizations"` // 组织 Organizations
Language string `json:"language"` // 语言 Language
//Password string `json:"password"` // 密码 Password
}

View File

@ -11,6 +11,7 @@ import (
)
// getMimeType 根据文件扩展名返回相应的 MIME 类型
// as the file extension returns the corresponding MIME type
func getMimeType(path string) string {
// 这里可以根据文件扩展名返回相应的 MIME 类型
ext := filepath.Ext(path)
@ -18,6 +19,7 @@ func getMimeType(path string) string {
}
// WebHandler 处理静态文件请求的 Handler
// Handler for static file requests
func WebHandler(ctx context.Context, c *app.RequestContext) {
path := "dist" + string(c.Path())
file, err := static.WebFS.Open(path)

View File

@ -3,14 +3,13 @@ package middle
import (
"context"
"fmt"
"strings"
"time"
"github.com/LiteyukiStudio/spage/config"
"github.com/LiteyukiStudio/spage/constants"
models "github.com/LiteyukiStudio/spage/spage/models"
store "github.com/LiteyukiStudio/spage/spage/store"
"github.com/LiteyukiStudio/spage/utils"
"strings"
"time"
"github.com/LiteyukiStudio/spage/resps"
"github.com/cloudwego/hertz/pkg/app"
@ -22,6 +21,7 @@ type authType struct{}
var Auth = authType{}
// PersistentHandler 持久化处理函数,使用依赖注入到 utils 中防止循环引用
// Persistent Handler Function, using dependency injection to prevent circular references
func PersistentHandler(userID uint) (*models.Token, error) {
token, err := store.JWT.CreateToken(userID)
if err != nil {
@ -31,25 +31,31 @@ func PersistentHandler(userID uint) (*models.Token, error) {
}
// RevokeChecker 令牌撤销检查器,使用依赖注入到 utils 中防止循环引用
// Token Revocation Checker, using dependency injection to prevent circular references
func RevokeChecker(tokenID uint) bool {
return store.JWT.IsTokenRevoked(tokenID)
}
// UseAuth 中间件函数
// Middleware function for authentication
func (authType) UseAuth() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
// 1. 检查认证方式
// 1. Check authentication method
authHeader := string(c.GetHeader("Authorization"))
// 认证方式1使用 Authorization Header
// Authentication method 1: Use Authorization Header
if authHeader != "" {
// 检查 token 是否以 "Bearer " 开头,如果是,则去掉前缀
// Check if the token starts with "Bearer ", if so, remove the prefix
token := authHeader
if strings.HasPrefix(token, "Bearer ") {
token = strings.TrimPrefix(token, "Bearer ")
}
// 验证令牌
// Verify token
claims, err := utils.Token.ParseToken(token, RevokeChecker)
if err != nil {
resps.Unauthorized(c, "Invalid token")
@ -58,15 +64,18 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 将用户信息存储到上下文中
// Store user information in the context
c.Set("user", claims.UserID)
c.Next(ctx)
return
}
// 认证方式2使用 Cookie启用无感刷新
// Authentication method 2: Use Cookie (Enable silent refresh)
token := string(c.Cookie("token"))
if token == "" {
// 尝试通过 refresh_token 刷新
// Try to refresh by refresh_token
refreshToken := string(c.Cookie("refresh_token"))
if refreshToken == "" {
resps.BadRequest(c, "Refresh token not found 1")
@ -75,6 +84,7 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 验证刷新令牌
// Verify refresh token
refreshClaims, err := utils.Token.ParseToken(refreshToken, RevokeChecker)
if err != nil {
resps.Unauthorized(c, "Refresh token expired or invalid 2")
@ -83,6 +93,7 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 生成新的访问令牌
// Generate new access token
newToken, err := utils.Token.CreateToken(refreshClaims.UserID, time.Duration(config.TokenExpireTime)*time.Second, false, PersistentHandler)
if err != nil {
resps.InternalServerError(c, "Create access token failed 3")
@ -91,18 +102,22 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 设置新的访问令牌
// Set new access token
c.SetCookie("token", newToken, config.TokenExpireTime, "/", "", protocol.CookieSameSiteLaxMode, true, true)
// 保存用户信息并继续请求
// Save user information and continue request
c.Set("user", refreshClaims.UserID)
c.Next(ctx)
return
}
// Cookie 中存在 token验证其有效性
// Cookie contains token, verify its validity
claims, err := utils.Token.ParseToken(token, RevokeChecker)
if err != nil {
// token 无效,尝试刷新
// Token is invalid, try to refresh
refreshToken := string(c.Cookie("refresh_token"))
if refreshToken == "" {
resps.Unauthorized(c, "Refresh token not found 4")
@ -111,6 +126,7 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 验证刷新令牌
// Verify refresh token
refreshClaims, err := utils.Token.ParseToken(refreshToken, RevokeChecker)
if err != nil {
fmt.Println(err)
@ -120,6 +136,7 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 生成新的访问令牌
// Generate new access token
newToken, err := utils.Token.CreateToken(refreshClaims.UserID, time.Duration(config.TokenExpireTime)*time.Second, false, PersistentHandler)
if err != nil {
resps.InternalServerError(c, "Create access token failed 6")
@ -128,21 +145,25 @@ func (authType) UseAuth() app.HandlerFunc {
}
// 设置新的访问令牌
// Set new access token
c.SetCookie("token", newToken, config.TokenExpireTime, "/", "", protocol.CookieSameSiteLaxMode, true, true)
// 保存用户信息并继续请求
// Save user information and continue request
c.Set("user", refreshClaims.UserID)
c.Next(ctx)
return
}
// token 有效,继续请求
// Token is valid, continue request
ctx = context.WithValue(ctx, "user", claims.UserID)
c.Next(ctx)
}
}
// IsAdmin 是一个中间件,用于检查用户是否为管理员
// IsAdmin is a middleware that checks if the user is an admin
func (authType) IsAdmin() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
user := Auth.GetUser(ctx, c)
@ -156,6 +177,7 @@ func (authType) IsAdmin() app.HandlerFunc {
}
// GetUser 从已认证的上下文中获取用户信息,如果用户不存在则终止请求并返回
// GetUser retrieves user information from the authenticated context, if the user does not exist it terminates the request and returns
func (authType) GetUser(ctx context.Context, c *app.RequestContext) *models.User {
userID := ctx.Value("user").(uint)
if userID == 0 {

View File

@ -2,7 +2,6 @@ package middle
import (
"context"
"github.com/LiteyukiStudio/spage/config"
"github.com/LiteyukiStudio/spage/constants"
"github.com/LiteyukiStudio/spage/utils"
@ -22,6 +21,7 @@ type CaptchaReq struct {
}
// UseCaptcha 中间件函数,用于验证验证码
// Middleware function for captcha verification
func (captchaType) UseCaptcha() app.HandlerFunc {
captchaConfig := &utils.CaptchaConfig{
Type: config.CaptchaType,
@ -38,6 +38,7 @@ func (captchaType) UseCaptcha() app.HandlerFunc {
}
if config.Mode == constants.ModeDev && req.CaptchaToken == constants.CaptchaDevPasscode {
// 开发模式密钥
// Dev mode passkey
c.Next(ctx)
return
}
@ -56,6 +57,7 @@ func (captchaType) UseCaptcha() app.HandlerFunc {
return
}
c.Next(ctx) // 如果验证码验证成功,则继续下一个处理程序
// If captcha verification is successful, continue to the next handler
return
}

View File

@ -13,6 +13,7 @@ type corsType struct{}
var Cors = corsType{}
// UseCors 跨域中间件
// Cross-domain middleware
func (corsType) UseCors() app.HandlerFunc {
var allowedOrigins []string
if config.Mode == constants.ModeDev {

View File

@ -14,6 +14,7 @@ type traceType struct{}
var Trace = traceType{}
// UseTrace 中间件函数,用于记录请求日志
// Middleware function for request logging
func (traceType) UseTrace() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
start := time.Now()

View File

@ -5,56 +5,61 @@ import "gorm.io/gorm"
// User 用户模型
type User struct {
gorm.Model
Name string `gorm:"not null;unique"` // 用户的唯一名称
DisplayName *string `gorm:"column:display_name"` // 用户的显示名称
Email *string `gorm:"unique"` // 用户的电子邮件地址,只有用户的电子邮件地址是唯一的(用于 oidc 身份验证)
Description string `gorm:"default:'No description.'"` // 用户描述
AvatarURL *string `gorm:"column:avatar_url"` // 留空以使用
Role string `gorm:"not null;default:member"` // 用户的全局角色
Organizations []*Organization `gorm:"many2many:organization_members;"` // 隶属于许多组织
ProjectLimit int `gorm:"default:-1"` // 用户的项目限制0 表示无限制
Language string `gorm:"default:'zh-cn'"` // 用户的语言,默认为英语
Flag string `gorm:"default:'0'"` // system_admin 的另一面旗帜
Password *string `gorm:"column:password"` // 用户的密码(经过哈希处理),仅用于本地身份验证
Name string `gorm:"not null;unique"` // 用户的唯一名称 User's unique name
DisplayName *string `gorm:"column:display_name"` // 用户的显示名称 User's display name
Email *string `gorm:"unique"` // 用户的电子邮件地址,只有用户的电子邮件地址是唯一的(用于 oidc 身份验证) User's email address, only the user's email address is unique (used for oidc authentication)
Description string `gorm:"default:'No description.'"` // 用户描述 User description
AvatarURL *string `gorm:"column:avatar_url"` // 留空以使用 Gravatar Leave blank to use Gravatar
Role string `gorm:"not null;default:member"` // 用户的全局角色 User's global role
Organizations []*Organization `gorm:"many2many:organization_members;"` // 隶属于许多组织 Many organizations the user belongs to
ProjectLimit int `gorm:"default:-1"` // 用户的项目限制0 表示无限制 User's project limit, 0 means no limit
Language string `gorm:"default:'zh-cn'"` // 用户的语言,默认为英语 User's language, default to English
Flag string `gorm:"default:'0'"` // system_admin 的另一面旗帜 The other side of system_admin flag
Password *string `gorm:"column:password"` // 用户的密码(经过哈希处理),仅用于本地身份验证 User's password (hashed), only used for local authentication
}
// TableName 用户
// User
func (User) TableName() string {
return "users"
}
// Organization 组织模型
// Organization Model
type Organization struct {
gorm.Model
Name string `gorm:"not null;unique"` // 组织的唯一名称
DisplayName *string `gorm:"column:display_name"` // 组织的显示名称
Email *string `gorm:"column:email"` // 组织的电子邮件地址
Description string `gorm:"default:'No description.'"` // 组织描述
AvatarURL *string `gorm:"column:avatar_url"` // 留空以使用
Members []*User `gorm:"many2many:organization_members;"` // 组织的成员包含创建者
Owners []User `gorm:"many2many:organization_owners;"` // 组织的所有者(无反向关系)包含创建者
ProjectLimit int `gorm:"default:0"` // 组织的项目限制0遵循策略-1无限制
Name string `gorm:"not null;unique"` // 组织的唯一名称 Organization's unique name
DisplayName *string `gorm:"column:display_name"` // 组织的显示名称 Organization's display name
Email *string `gorm:"column:email"` // 组织的电子邮件地址 Organization's email address
Description string `gorm:"default:'No description.'"` // 组织描述 Organization description
AvatarURL *string `gorm:"column:avatar_url"` // 留空以使用 Gravatar Leave blank to use Gravatar
Members []*User `gorm:"many2many:organization_members;"` // 组织的成员包含创建者 (including the creator)
Owners []User `gorm:"many2many:organization_owners;"` // 组织的所有者(无反向关系)包含创建者 (including the creator)
ProjectLimit int `gorm:"default:0"` // 组织的项目限制0遵循策略-1无限制 Organization's project limit, 0: follow the policy, -1: unlimited
}
// TableName 组织
// Organization
func (Organization) TableName() string {
return "organizations"
}
// Project 项目模型
// Project Model
type Project struct {
gorm.Model
Name string `gorm:"not null;unique"` // 项目的唯一名称
DisplayName *string `gorm:"column:display_name"` // 项目的显示名称
Description string `gorm:"default:'No description.'"` // 项目描述
OwnerID uint `gorm:"not null"` // 所有者 ID用户 ID 或组织 ID
OwnerType string `gorm:"not null"` // 所有者类型,可以是用户或组织
Owners []User `gorm:"many2many:project_owners;"` // 项目的所有者,无反向关系
Members []*User `gorm:"many2many:project_members;"` // 项目的成员
SiteLimit int `gorm:"default:0"` // 项目的站点限制0遵循策略-1无限制
Name string `gorm:"not null;unique"` // 项目的唯一名称 Project's unique name
DisplayName *string `gorm:"column:display_name"` // 项目的显示名称 Project's display name
Description string `gorm:"default:'No description.'"` // 项目描述 Project description
OwnerID uint `gorm:"not null"` // 所有者 ID用户 ID 或组织 ID Owner ID (user ID or organization ID)
OwnerType string `gorm:"not null"` // 所有者类型,可以是用户或组织 Owner type, can be user or organization
Owners []User `gorm:"many2many:project_owners;"` // 项目的所有者,无反向关系 Project's owners, no reverse relation
Members []*User `gorm:"many2many:project_members;"` // 项目的成员 Project's members
SiteLimit int `gorm:"default:0"` // 项目的站点限制0遵循策略-1无限制 Project's site limit, 0: follow the policy, -1: unlimited
}
// TableName 项目
// Project
func (Project) TableName() string {
return "projects"
}

View File

@ -4,12 +4,12 @@ import "gorm.io/gorm"
type File struct {
gorm.Model
ID uint `gorm:"primaryKey" json:"id"` // 文件ID
Path string `gorm:"not null" json:"path"` // 文件路径,相较于根目录的相对路径
MD5 string `gorm:"not null" json:"md5"` // 文件哈希值
ID uint `gorm:"primaryKey" json:"id"` // 文件ID File ID
Path string `gorm:"not null" json:"path"` // 文件路径,相较于根目录的相对路径 File path, relative to the root directory
MD5 string `gorm:"not null" json:"md5"` // 文件哈希值 File hash
}
// TableName 自定义表名
// TableName 自定义表名 Custom table name
func (File) TableName() string {
return "projects"
}

View File

@ -3,6 +3,7 @@ package models
import "gorm.io/gorm"
// Migrate 迁移模型通过依赖注入的方式使用gorm.DB进行数据库操作
// Migrate models, using gorm.DB for database operations through dependency injection
func Migrate(db *gorm.DB) error {
if err := db.AutoMigrate(
// entity.go

View File

@ -5,16 +5,25 @@ import "gorm.io/gorm"
type OIDCConfig struct {
gorm.Model
AdminGroups []string `gorm:"type:json;column:admin_groups;default:'[]'"` // 平台管理员组,默认为:[]string{}*为匹配所有组,储存为逗号分隔的字符串
// Admin groups, default is: []string{}, * matches all groups, stored as a comma-separated string
AllowedGroups []string `gorm:"type:json;column:allowed_groups;default:'[\"*\"]'"` // 允许登录的组,默认为:[]string{"*"}*为匹配所有组,储存为逗号分隔的字符串
// Allowed groups for login, default is: []string{"*"}, * matches all groups, stored as a comma-separated string
ClientID string `gorm:"column:client_id"` // 客户端ID
// Client ID
ClientSecret string `gorm:"column:client_secret"` // 客户端密钥
// Client Secret
DisplayName string `gorm:"column:display_name"` // 显示名称,例如:轻雪通行证
// Display name, e.g., Light Snow Passport
GroupsClaim *string `gorm:"default:groups"` // 组声明,默认为:"groups"
// Groups claim, default is: "groups"
Icon *string `gorm:"column:icon"` // 图标url为空则使用内置默认图标
// Icon URL, if empty use the built-in default icon
OidcDiscoveryURL string `gorm:"column:oidc_discovery_url"` // OpenID自动发现URL例如 https://pass.liteyuki.icu/.well-known/openid-configuration
// OpenID auto-discovery URL, e.g., https://pass.liteyuki.icu/.well-known/openid-configuration
}
// TableName 重写表名
// Rewrite table name
func (OIDCConfig) TableName() string {
return "oidc_configs"
}

View File

@ -6,30 +6,30 @@ import (
type Site struct {
gorm.Model
Name string `gorm:"unique"` // 站点名称
Description string `gorm:"size:255"` // 站点描述
ProjectID uint `gorm:"not null"` // 项目ID
Project Project `gorm:"foreignKey:ProjectID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` // 项目
SubDomain string `gorm:"unique;size:255"` // 子域前缀
Domains []string `gorm:"type:json;default:'[]'"` // 允许的域名json格式
Name string `gorm:"unique"` // 站点名称 Site name
Description string `gorm:"size:255"` // 站点描述 Site description
ProjectID uint `gorm:"not null"` // 项目ID Project ID
Project Project `gorm:"foreignKey:ProjectID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` // 项目 Project
SubDomain string `gorm:"unique;size:255"` // 子域前缀 Subdomain prefix
Domains []string `gorm:"type:json;default:'[]'"` // 允许的域名json格式 Allowed domains, json format
}
// 站点表名
// 站点表名 Site table name
func (Site) TableName() string {
return "sites"
}
// 站点发布表
// 站点发布表 Site release table
type SiteRelease struct {
gorm.Model
SiteID uint `gorm:"not null"` // 站点ID
SiteID uint `gorm:"not null"` // 站点ID Site ID
Site Site `gorm:"foreignKey:SiteID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Tag string `gorm:"not null"` // 版本标签
FileID uint `gorm:"not null"` // 版本文件ID
File File `gorm:"foreignKey:FileID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` // 版本文件
Tag string `gorm:"not null"` // 版本标签 Version tag
FileID uint `gorm:"not null"` // 版本文件ID Version file ID
File File `gorm:"foreignKey:FileID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` // 版本文件 Version file
}
// 站点发布表名
// 站点发布表名 Site release table name
func (SiteRelease) TableName() string {
return "site_releases"
}

View File

@ -8,77 +8,78 @@ import (
)
// Run 运行路由服务
// Run router service
func Run() error {
// 运行路由
// 运行路由 Run router
H := server.New(server.WithHostPorts(":"+config.ServerPort), server.WithMaxRequestBodySize(config.FileMaxSize))
H.Use(middle.Cors.UseCors(), middle.Trace.UseTrace())
apiV1 := H.Group("/api/v1")
apiV1.Use(middle.Auth.UseAuth())
apiV1WithoutAuth := H.Group("/api/v1")
apiV1WithoutAuthAndCaptcha := H.Group("/api/v1") // 不需要登录和验证码的路由
apiV1WithoutAuthAndCaptcha := H.Group("/api/v1") // 不需要登录和验证码的路由 Group without auth and captcha
{
apiV1WithoutAuthAndCaptcha.GET("/user/captcha", handlers.User.GetCaptcha) // 取验证码
apiV1WithoutAuthAndCaptcha.GET("/user/captcha", handlers.User.GetCaptcha) // 取验证码 Get captcha
apiV1WithoutAuth.POST("/user/logout", handlers.User.Logout)
apiV1WithoutAuth.POST("/user/register", handlers.User.Register).Use(middle.Captcha.UseCaptcha()) // 注册
apiV1WithoutAuth.POST("/user/register", handlers.User.Register).Use(middle.Captcha.UseCaptcha()) // 注册 Register
apiV1WithoutAuth.POST("/user/login", handlers.User.Login).Use(middle.Captcha.UseCaptcha())
userGroup := apiV1.Group("/user")
{
userGroup.PUT("", handlers.User.UpdateUser) // 更新用户信息
userGroup.GET("", handlers.User.GetUser) // 获取用户信息
userGroup.GET("/:id", handlers.User.GetUser) // 获取用户信息
userGroup.GET("/:id/projects", handlers.User.GetProjects) // 获取用户项目
userGroup.GET("/:id/orgs", handlers.User.GetOrgs) // 获取用户组织
userGroup.PUT("", handlers.User.UpdateUser) // 更新用户信息 Update user info
userGroup.GET("", handlers.User.GetUser) // 获取用户信息 Get user info
userGroup.GET("/:id", handlers.User.GetUser) // 获取用户信息 Get user info
userGroup.GET("/:id/projects", handlers.User.GetProjects) // 获取用户项目 Get user projects
userGroup.GET("/:id/orgs", handlers.User.GetOrgs) // 获取用户组织 Get user orgs
}
orgGroup := apiV1.Group("/org", handlers.Org.UserOrgAuth)
{
orgGroup.POST("", handlers.Org.CreateOrganization) // 创建组织
orgGroup.PUT("/:id", handlers.Org.UpdateOrganization) // 更新组织
orgGroup.DELETE("/:id", handlers.Org.DeleteOrganization) // 删除组织
orgGroup.GET("/:id", handlers.Org.GetOrganization) // 获取组织信息
orgGroup.GET("/:id/projects", handlers.Org.GetOrganizationProject) // 获取组织项目
orgGroup.GET("/:id/users", handlers.Org.GetOrganizationUsers) // 获取组织所有成员和所有者
orgGroup.PUT("/:id/users", handlers.Org.AddOrganizationUser) // 添加组织成员或所有者
orgGroup.DELETE("/:id/users", handlers.Org.DeleteOrganizationUser) // 删除组织成员或所有者
orgGroup.POST("", handlers.Org.CreateOrganization) // 创建组织 Create organization
orgGroup.PUT("/:id", handlers.Org.UpdateOrganization) // 更新组织 Update organization
orgGroup.DELETE("/:id", handlers.Org.DeleteOrganization) // 删除组织 Delete organization
orgGroup.GET("/:id", handlers.Org.GetOrganization) // 获取组织信息 Get organization info
orgGroup.GET("/:id/projects", handlers.Org.GetOrganizationProject) // 获取组织项目 Get organization projects
orgGroup.GET("/:id/users", handlers.Org.GetOrganizationUsers) // 获取组织所有成员和所有者 Get organization users
orgGroup.PUT("/:id/users", handlers.Org.AddOrganizationUser) // 添加组织成员或所有者 Add organization user
orgGroup.DELETE("/:id/users", handlers.Org.DeleteOrganizationUser) // 删除组织成员或所有者 Delete organization user
}
projectGroup := apiV1.Group("/project", handlers.Project.UserProjectAuth)
{
projectGroup.POST("", handlers.Project.Create) // 创建项目
projectGroup.PUT("/:id", handlers.Project.Update) // 更新项目
projectGroup.DELETE("/:id", handlers.Project.Delete) // 删除项目
projectGroup.GET("/:id", handlers.Project.Info) // 获取项目信息
projectGroup.GET("/:id/owners", handlers.Project.GetOwners) // 获取项目所有者
projectGroup.PUT("/:id/owner", handlers.Project.AddOwner) // 更新项目所有者
projectGroup.DELETE("/:id/owner", handlers.Project.DeleteOwner) // 删除项目所有者
projectGroup.GET("/:id/sites", handlers.Project.GetSites) // 获取项目站点
projectGroup.POST("", handlers.Project.Create) // 创建项目 Create project
projectGroup.PUT("/:id", handlers.Project.Update) // 更新项目 Update project
projectGroup.DELETE("/:id", handlers.Project.Delete) // 删除项目 Delete project
projectGroup.GET("/:id", handlers.Project.Info) // 获取项目信息 Get project info
projectGroup.GET("/:id/owners", handlers.Project.GetOwners) // 获取项目所有者 Get project owners
projectGroup.PUT("/:id/owner", handlers.Project.AddOwner) // 更新项目所有者 Add project owner
projectGroup.DELETE("/:id/owner", handlers.Project.DeleteOwner) // 删除项目所有者 Delete project owner
projectGroup.GET("/:id/sites", handlers.Project.GetSites) // 获取项目站点 Get project sites
siteGroup := projectGroup.Group("/:id/site", handlers.Site.SiteAuth)
{
siteGroup.POST("", handlers.Site.Create) // 创建站点
siteGroup.PUT("/:site_id", handlers.Site.Update) // 更新站点
siteGroup.DELETE("/:site_id", handlers.Site.Delete) // 删除站点
siteGroup.GET("/:site_id", handlers.Site.Info) // 获取网站信息
siteGroup.POST("", handlers.Site.Create) // 创建站点 Create site
siteGroup.PUT("/:site_id", handlers.Site.Update) // 更新站点 Update site
siteGroup.DELETE("/:site_id", handlers.Site.Delete) // 删除站点 Delete site
siteGroup.GET("/:site_id", handlers.Site.Info) // 获取网站信息 Get site info
siteGroup.GET("/:site_id/releases", handlers.Release.ReleaseList) // 获取站点 release 列表
siteRelease := siteGroup.Group("/:site_id/release")
{
siteRelease.POST("", handlers.Release.Create) // 创建站点发布
siteRelease.DELETE("", handlers.Release.Delete) // 删除站点版本
siteRelease.POST("", handlers.Release.Create) // 创建站点发布 Create site release
siteRelease.DELETE("", handlers.Release.Delete) // 删除站点版本 Delete site release
siteRelease.POST("/activation", handlers.Release.Activation) // 指定使用该站点版本
}
}
}
fileGroup := apiV1.Group("/file")
{
fileGroup.POST("", handlers.File.UploadFileStream) // 上传文件
fileGroup.GET("") // 下载文件
fileGroup.DELETE("") // 删除文件
fileGroup.POST("", handlers.File.UploadFileStream) // 上传文件 Upload file
fileGroup.GET("") // 下载文件 Download file
fileGroup.DELETE("") // 删除文件 Delete file
}
adminGroup := apiV1.Group("/admin") // 管理员路由
adminGroup.Use(middle.Auth.IsAdmin())
{
adminUser := adminGroup.Group("/user")
{
adminUser.POST("", handlers.Admin.CreateUser) // 创建用户
adminUser.POST("", handlers.Admin.CreateUser) // 创建用户 Create user
}
adminNode := adminGroup.Group("/node")
{
@ -95,21 +96,21 @@ func Run() error {
}
// 设置静态文件目录
// 设置静态文件目录 Set static file directory
web := H.Group("")
{
web.GET("/*any", handlers.WebHandler)
}
// 运行服务
// 运行服务 Run service
if config.Mode == "dev" {
// 开发模式
// 开发模式 Development mode
err := H.Run()
if err != nil {
return err
}
} else {
// 生产模式
// 生产模式 Production mode
H.Spin()
}
return nil

View File

@ -9,6 +9,7 @@ type JWTType struct{}
var JWT = JWTType{}
// CreateToken 创建令牌
// Create Token
func (JWTType) CreateToken(userID uint) (*models.Token, error) {
token := &models.Token{
UserID: userID,
@ -20,11 +21,14 @@ func (JWTType) CreateToken(userID uint) (*models.Token, error) {
}
// IsTokenRevoked 检查令牌是否被撤销
// Check if a token has been revoked
func (JWTType) IsTokenRevoked(tokenID uint) bool {
var count int64
// 查询是否存在该令牌(未被删除的)
// Check if the token exists (not deleted)
err := DB.Model(&models.Token{}).Where("id = ?", tokenID).Count(&count).Error
// 如果查询出错或找不到令牌,默认视为已撤销(安全优先)
// If the query fails or no token is found, assume it's revoked (safety first)
if err != nil || count == 0 {
return true
}
@ -32,6 +36,7 @@ func (JWTType) IsTokenRevoked(tokenID uint) bool {
}
// RevokeTokenByID 撤销令牌
// Revoke Token
func (JWTType) RevokeTokenByID(id uint) error {
if err := DB.Where("id = ?", id).Delete(&models.Token{}).Error; err != nil {
return err
@ -40,6 +45,7 @@ func (JWTType) RevokeTokenByID(id uint) error {
}
// RevokeTokenByUserID 撤销用户的所有令牌
// Revoke all tokens for a user
func (JWTType) RevokeTokenByUserID(userID uint) error {
if err := DB.Where("user_id = ?", userID).Delete(&models.Token{}).Error; err != nil {
return err

View File

@ -10,6 +10,7 @@ type orgType struct {
var Org = orgType{}
// ListByUserID 通过UserID获取用户组织支持分页和预加载关系
// Get Organizations by UserID, support pagination and preload relationships
func (o *orgType) ListByUserID(userID string, page, limit int) (orgs []models.Organization, err error) {
// 使用连接查询
query := DB.Joins("JOIN organization_members ON organizations.id = organization_members.organization_id").
@ -27,12 +28,14 @@ func (o *orgType) ListByUserID(userID string, page, limit int) (orgs []models.Or
}
// GetOrgById 通过ID获取组织
// Get Organization by ID
func (o *orgType) GetOrgById(id uint) (org *models.Organization, err error) {
err = DB.Model(&models.Organization{}).Where("id = ?", id).Preload("Members").Preload("Owners").First(&org).Error
return
}
// OrgNameIsExist 判断组织名称是否存在
// Check if the organization name exists
func (o *orgType) OrgNameIsExist(name string) bool {
var count int64
DB.Model(&models.Organization{}).Where("name = ?", name).Count(&count)
@ -40,6 +43,7 @@ func (o *orgType) OrgNameIsExist(name string) bool {
}
// GetUserAuth 获取用户在组织中的权限
// Get User's Authority in Organization
func (o *orgType) GetUserAuth(org *models.Organization, userID uint) (auth string) {
for _, owner := range org.Owners {
if owner.ID == userID {

View File

@ -2,7 +2,6 @@ package store
import (
"fmt"
"github.com/LiteyukiStudio/spage/constants"
"github.com/LiteyukiStudio/spage/spage/models"
)
@ -13,17 +12,20 @@ type projectType struct {
var Project = projectType{}
// Create 创建项目
// Create a project
func (p *projectType) Create(project *models.Project) (err error) {
return DB.Create(project).Error
}
// GetByID 通过项目ID获取项目
// Get Project by ID
func (p *projectType) GetByID(id uint) (project *models.Project, err error) {
err = DB.First(&project, id).Preload("Owners").Error
return
}
// UserIsOwner 判断用户是否是项目的所有者
// Check if a user is the owner of a project
func (p *projectType) UserIsOwner(project *models.Project, userID uint) bool {
if project.OwnerType == constants.OwnerTypeUser && project.OwnerID == userID {
return true
@ -37,6 +39,7 @@ func (p *projectType) UserIsOwner(project *models.Project, userID uint) bool {
}
// ListByOwner 通过用户ID获取项目列表支持分页和从新到旧排序
// Get Project List by UserID, support pagination and new to old sorting
func (p *projectType) ListByOwner(ownerType, ownerID string, page, limit int) (projects []models.Project, total int64, err error) {
tableName := ""
switch ownerType {
@ -71,16 +74,19 @@ func (p *projectType) Delete(project *models.Project) (err error) {
}
// AddOwner 为项目添加所有者
// Add Owner to a project
func (p *projectType) AddOwner(project *models.Project, user *models.User) (err error) {
return DB.Model(project).Association("Owners").Append(user)
}
// DeleteOwner 从项目删除所有者
// Delete Owner from a project
func (p *projectType) DeleteOwner(project *models.Project, user *models.User) (err error) {
return DB.Model(project).Association("Owners").Delete(user)
}
// GetSiteList 获取项目下的站点列表
// Get Site List of a project
func (p *projectType) GetSiteList(project *models.Project, page, limit int) (sites []models.Site, total int64, err error) {
sites, total, err = Paginate[models.Site](
DB,

View File

@ -10,11 +10,13 @@ type SiteType struct {
var Site = SiteType{}
// Create 创建站点
// Create Site
func (s *SiteType) Create(site *models.Site) (err error) {
return DB.Create(site).Error
}
// GetByID 根据id获取站点信息
// Get Site Info by ID
func (s *SiteType) GetByID(id uint) (site *models.Site, err error) {
site = &models.Site{}
err = DB.Where("id = ?", id).Preload("Project").First(site).Error

View File

@ -3,9 +3,6 @@ package store
import (
"errors"
"fmt"
"plugin"
"runtime"
"github.com/LiteyukiStudio/spage/config"
"github.com/LiteyukiStudio/spage/constants"
"github.com/LiteyukiStudio/spage/spage/models"
@ -14,6 +11,8 @@ import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"plugin"
"runtime"
)
var DB *gorm.DB
@ -21,13 +20,13 @@ var DB *gorm.DB
// DBConfig 数据库配置结构体
type DBConfig struct {
Driver string // 数据库驱动类型,例如 "sqlite" 或 "postgres" Database driver type, e.g., "sqlite" or "postgres"
Path string // 路径
Host string // PostgreSQL 主机名
Port int // PostgreSQL 端口
User string // PostgreSQL 用户名
Password string // PostgreSQL 密码
DBName string // PostgreSQL 数据库名
SSLMode string // PostgreSQL SSL 模式
Path string // SQLite 路径 SQLite path
Host string // PostgreSQL 主机名 PostgreSQL hostname
Port int // PostgreSQL 端口 PostgreSQL port
User string // PostgreSQL 用户名 PostgreSQL username
Password string // PostgreSQL 密码 PostgreSQL password
DBName string // PostgreSQL 数据库名 PostgreSQL database name
SSLMode string // PostgreSQL SSL 模式 PostgreSQL SSL mode
}
// loadDBConfig 从配置文件加载数据库配置

View File

@ -2,7 +2,6 @@ package store
import (
"errors"
"github.com/LiteyukiStudio/spage/constants"
"github.com/LiteyukiStudio/spage/spage/models"
@ -21,10 +20,10 @@ func (u *userType) Create(user *models.User) (err error) {
// GetByName 根据名称获取用户
func (u *userType) GetByName(name string) (user *models.User, err error) {
user = &models.User{} // 初始化指针
user = &models.User{} // 初始化指针 // Initialize pointer
err = DB.Where("name = ?", name).First(user).Error
if err != nil {
return nil, err // 出错时返回
return nil, err // 出错时返回nil When an error occurs, return nil
}
return user, nil
}
@ -74,26 +73,27 @@ func (u *userType) DeleteByID(id uint) (err error) {
}
// UpdateSystemAdmin 更新系统管理员用户,不存在则创建
// Update System Admin User, create if not exist
func (u *userType) UpdateSystemAdmin(user *models.User) (err error) {
// 设置该用户为系统管理员
// 设置该用户为系统管理员 Set this user as a system admin
user.Flag = constants.FlagSystemAdmin
user.Role = constants.RoleAdmin
// 尝试查找系统管理员
// 尝试查找系统管理员 Try to find system admin
existingAdmin := models.User{}
result := DB.Where("flag = ?", constants.FlagSystemAdmin).First(&existingAdmin)
if result.Error != nil {
// 如果不存在系统管理员(记录未找到),则创建一个
// 如果不存在系统管理员(记录未找到),则创建一个 If there is no system admin (record not found), create one
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 创建新的系统管理员
// 创建新的系统管理员 Create new system admin
return DB.Create(user).Error
}
// 其他错误则直接返回
// 其他错误则直接返回 Other errors are returned directly
return result.Error
}
// 系统管理员已存在,更新信息
// 保留ID更新其他字段
// 系统管理员已存在,更新信息 System admin exists, update information
// 保留ID更新其他字段 Keep ID, update other fields
user.ID = existingAdmin.ID
return DB.Model(&existingAdmin).Updates(user).Error
}

View File

@ -7,6 +7,8 @@ import (
// Paginate 封装通用的分页查询逻辑
// T 是任意数据模型类型
// Paginate generic pagination query logic
// T is any data model type
func Paginate[T any](db *gorm.DB, page, limit int, conditions ...any) (items []T, total int64, err error) {
// 查询总记录数
countDB := db
@ -19,11 +21,13 @@ func Paginate[T any](db *gorm.DB, page, limit int, conditions ...any) (items []T
}
// 确保分页参数有效
// Ensure pagination parameters are valid
if limit <= 0 {
limit = config.PageLimit
}
// 执行分页查询
// Execute pagination query
queryDB := db
if len(conditions) > 0 {
queryDB = queryDB.Where(conditions[0], conditions[1:]...)
@ -39,6 +43,7 @@ func Paginate[T any](db *gorm.DB, page, limit int, conditions ...any) (items []T
}
// WithPreloads 添加预加载关系的辅助函数
// Add a helper function to add preloaded relationships
func WithPreloads(db *gorm.DB, preloads ...string) *gorm.DB {
for _, preload := range preloads {
db = db.Preload(preload)

View File

@ -2,7 +2,6 @@ package utils
import (
"fmt"
"github.com/LiteyukiStudio/spage/constants"
"github.com/go-resty/resty/v2"
)
@ -18,6 +17,7 @@ type CaptchaConfig struct {
}
// VerifyCaptcha 根据提供的配置和令牌验证验证码
// Verify captcha based on provided configuration and token
func (captchaType) VerifyCaptcha(restyClient *resty.Client, captchaConfig *CaptchaConfig, captchaToken string) (bool, error) {
switch captchaConfig.Type {
case constants.CaptchaTypeDisable:

View File

@ -14,16 +14,17 @@ type EmailType struct{}
var Email = EmailType{}
type EmailConfig struct {
Enable bool // 邮箱启用状态
Username string // 邮箱用户名
Address string // 邮箱地址
Host string // 邮箱服务器地址
Port string // 邮箱服务器端口
Password string // 邮箱密码
SSL bool // 是否使用SSL
Enable bool // 邮箱启用状态 Email enable status
Username string // 邮箱用户名 Email username
Address string // 邮箱地址 Email address
Host string // 邮箱服务器地址 Email server address
Port string // 邮箱服务器端口 Email server port
Password string // 邮箱密码 Email password
SSL bool // 是否使用SSL Email use SSL
}
// SendTemplate 发送HTML模板从配置文件中读取邮箱配置
// Send HTML template, read email configuration from the configuration file
func SendTemplate(emailConfig *EmailConfig, target, htmlTemplate string, placeholders map[string]string) error {
for placeholder, value := range placeholders {
htmlTemplate = strings.ReplaceAll(htmlTemplate, placeholder, value)
@ -36,8 +37,10 @@ func SendTemplate(emailConfig *EmailConfig, target, htmlTemplate string, placeho
}
// SendEmail 发送邮件
// Send Email
func SendEmail(emailConfig *EmailConfig, target, content string, isHTML bool) error {
// 如果配置未启用则直接返回nil
// If the configuration is not enabled, return nil directly
if !emailConfig.Enable {
return nil
}
@ -58,6 +61,7 @@ func SendEmail(emailConfig *EmailConfig, target, content string, isHTML bool) er
}
// 在函数退出时关闭连接
// Close the connection when the function exits
defer func(conn net.Conn) {
err := conn.Close()
if err != nil {
@ -70,40 +74,47 @@ func SendEmail(emailConfig *EmailConfig, target, content string, isHTML bool) er
if err != nil {
return err
// todo: 处理连接时的错误
// todo: Handle errors during connection
}
defer func(client *smtp.Client) {
err := client.Quit()
if err != nil {
// todo: 处理关闭连接时的错误
// todo: Handle errors when closing the connection
}
}(client)
if err = client.Auth(auth); err != nil {
return err
// todo: 处理身份验证时的错误
// todo: Handle errors during authentication
}
if err = client.Mail(emailConfig.Address); err != nil {
return err
// todo: 处理发件人时的错误
// todo: Handle errors when processing the sender
}
if err = client.Rcpt(target); err != nil {
return err
// todo: 处理收件人时的错误
// todo: Handle errors when processing recipients
}
writer, err := client.Data()
if err != nil {
return err
// todo: 处理数据写入器创建时的错误
// todo: Handle errors when creating the data writer
}
defer func(writer io.WriteCloser) {
err := writer.Close()
if err != nil {
// todo: 处理关闭写入器时的错误
// todo: Handle errors when closing the writer
}
}(writer)

View File

@ -1,9 +1,8 @@
package utils
import (
"strconv"
"github.com/LiteyukiStudio/spage/config"
"strconv"
"github.com/cloudwego/hertz/pkg/app"
)
@ -13,12 +12,13 @@ type ctxType struct{}
var Ctx = ctxType{}
// GetPageLimit 封装从上下文中的query获取查询参数并转换为整数的函数出错返回默认值
// packing from the query in the context and converting it to an integer, returning the default value on error
func (ctxType) GetPageLimit(c *app.RequestContext) (page, limit int) {
pageString := c.Query("page")
limitString := c.Query("limit")
page, err := strconv.Atoi(pageString)
if err != nil || page <= 0 {
page = 1 // 默认第一页
page = 1 // 默认第一页 Default to the first page
}
limit, err = strconv.Atoi(limitString)

View File

@ -14,6 +14,7 @@ type PasswordType struct {
var Password = PasswordType{}
// HashPassword 密码哈希函数
// hash password function
func (u *PasswordType) HashPassword(password string, salt string) (string, error) {
saltedPassword := Password.addSalt(password, salt)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
@ -24,6 +25,8 @@ func (u *PasswordType) HashPassword(password string, salt string) (string, error
}
// VerifyPassword 验证密码
// verify password
func (u *PasswordType) VerifyPassword(password, hashedPassword string, salt string) bool {
saltedPassword := Password.addSalt(password, salt)
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(saltedPassword))
@ -34,6 +37,10 @@ func (u *PasswordType) VerifyPassword(password, hashedPassword string, salt stri
// password: 原始密码
// salt: 盐值
// 返回值: 加盐后的密码
// Add salt function
// password: original password
// salt: salt value
// return value: salted password
func (u *PasswordType) addSalt(password string, salt string) string {
combined := password + salt
hash := sha256.New()
@ -45,12 +52,17 @@ func (u *PasswordType) addSalt(password string, salt string) string {
// password: 待检查的密码
// level: 复杂度级别(1-4)
// 返回值: 是否满足复杂度要求
// Check password complexity based on the specified level
// password: the password to be checked
// level: complexity level (1-4)
// return value: whether it meets the complexity requirements
func (u *PasswordType) CheckPasswordComplexity(password string, level int) bool {
if len(password) <= 8 {
return false
}
// 定义各种字符类型的检查标志
// Define flags for checking various character types
var (
hasLower bool
hasUpper bool

View File

@ -1,10 +1,9 @@
package utils
import (
"time"
"github.com/LiteyukiStudio/spage/config"
"github.com/LiteyukiStudio/spage/spage/models"
"time"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
@ -16,13 +15,15 @@ var Token = TokenType{}
type Claims struct {
jwt.RegisteredClaims
UserID uint `json:"user_id"` // 用户ID用于身份验证
TokenID uint `json:"token_id"` // 令牌ID用于服务端会话维持
Stateful bool `json:"stateful"` // 是否为有状态Token
UserID uint `json:"user_id"` // 用户ID用于身份验证 Verify user identity using the User ID
TokenID uint `json:"token_id"` // 令牌ID用于服务端会话维持 Keep the token ID for server-side session maintenance
Stateful bool `json:"stateful"` // 是否为有状态Token Whether it is a stateful Token
}
// CreateToken 生成用户会话令牌默认24小时有效
// stateful=false的无状态Token不会做持久化在实例重启后失效
// Create a user session token (default 24 hours valid)
// stateful=false tokens are not persistent, and they will expire after the instance restarts
func (TokenType) CreateToken(userID uint, duration time.Duration, stateful bool, persistentHandler func(uint) (*models.Token, error)) (string, error) {
var tokenModel *models.Token
var err error
@ -53,6 +54,7 @@ func (TokenType) CreateToken(userID uint, duration time.Duration, stateful bool,
}
// ParseToken 解析JWT令牌
// Parse JWT token
func (TokenType) ParseToken(tokenString string, revokeChecker func(uint) bool) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) {
@ -68,6 +70,7 @@ func (TokenType) ParseToken(tokenString string, revokeChecker func(uint) bool) (
}
// 有状态token被吊销也视为过期
// Revoked stateful tokens are considered expired
if claims.Stateful {
if revokeChecker(claims.UserID) {
return nil, jwt.ErrTokenExpired