新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。
作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
@@ -27,13 +28,46 @@ const (
|
||||
SettingSearchKeywordMax = "search_keyword_max"
|
||||
|
||||
SettingPageSizeDefault = "page_size_default"
|
||||
SettingPageSizeMax = "page_size_max"
|
||||
|
||||
SettingFeedMaxPages = "feed_max_pages"
|
||||
SettingFeedMaxItems = "feed_max_items"
|
||||
|
||||
SettingPasswordMinLen = "password_min_len"
|
||||
SettingAvatarMaxMB = "avatar_max_mb"
|
||||
|
||||
SettingOpenPostsInNewTab = "open_posts_in_new_tab"
|
||||
SettingOpenContentLinksInNewTab = "open_content_links_in_new_tab"
|
||||
|
||||
SettingSMTPEnabled = "smtp_enabled"
|
||||
SettingSMTPHost = "smtp_host"
|
||||
SettingSMTPPort = "smtp_port"
|
||||
SettingSMTPUsername = "smtp_username"
|
||||
SettingSMTPPassword = "smtp_password"
|
||||
SettingSMTPFrom = "smtp_from"
|
||||
SettingSMTPFromName = "smtp_from_name"
|
||||
SettingSMTPEncryption = "smtp_encryption"
|
||||
|
||||
SettingOIDCEnabled = "oidc_enabled"
|
||||
SettingOIDCRootURL = "oidc_root_url"
|
||||
SettingOIDCGroupClaim = "oidc_group_claim"
|
||||
SettingOIDCAdminGroup = "oidc_admin_group"
|
||||
SettingOIDCUserGroup = "oidc_user_group"
|
||||
// 遗留单客户端字段(仅用于迁移到 oauth_clients)
|
||||
SettingOAuthClientID = "oauth_client_id"
|
||||
SettingOAuthClientSecret = "oauth_client_secret"
|
||||
SettingOAuthRedirectURIs = "oauth_redirect_uris"
|
||||
|
||||
SettingGiteaSyncEnabled = "gitea_sync_enabled"
|
||||
SettingGiteaBaseURL = "gitea_base_url"
|
||||
SettingGiteaToken = "gitea_token"
|
||||
SettingGiteaSyncIntervalMin = "gitea_sync_interval_min"
|
||||
|
||||
SettingSiteName = "site_name"
|
||||
SettingSiteNameEN = "site_name_en"
|
||||
SettingSiteSlogan = "site_slogan"
|
||||
SettingSiteLogoMark = "site_logo_mark"
|
||||
SettingSiteLogo = "site_logo"
|
||||
SettingSiteFavicon = "site_favicon"
|
||||
|
||||
// pageSizeAPIMax 单次列表请求条数硬上限(防客户端传超大 size),非后台可配项
|
||||
pageSizeAPIMax = 100
|
||||
)
|
||||
|
||||
// ForumLimits 论坛可配置限制(API 传输结构)
|
||||
@@ -56,28 +90,28 @@ type ForumLimits struct {
|
||||
SearchKeywordMax int `json:"search_keyword_max"`
|
||||
|
||||
PageSizeDefault int `json:"page_size_default"`
|
||||
PageSizeMax int `json:"page_size_max"`
|
||||
|
||||
FeedMaxPages int `json:"feed_max_pages"`
|
||||
FeedMaxItems int `json:"feed_max_items"`
|
||||
|
||||
PasswordMinLen int `json:"password_min_len"`
|
||||
AvatarMaxMB int `json:"avatar_max_mb"`
|
||||
|
||||
OpenPostsInNewTab bool `json:"open_posts_in_new_tab"`
|
||||
OpenContentLinksInNewTab bool `json:"open_content_links_in_new_tab"`
|
||||
}
|
||||
|
||||
// ForumLimitsPublic 前台可见的限制(不含限流等内部配置)
|
||||
type ForumLimitsPublic struct {
|
||||
PostTitleMax int `json:"post_title_max"`
|
||||
PostTagsMax int `json:"post_tags_max"`
|
||||
PostContentMax int `json:"post_content_max"`
|
||||
CommentMax int `json:"comment_max"`
|
||||
PostTitleMax int `json:"post_title_max"`
|
||||
PostTagsMax int `json:"post_tags_max"`
|
||||
PostContentMax int `json:"post_content_max"`
|
||||
CommentMax int `json:"comment_max"`
|
||||
SearchKeywordMin int `json:"search_keyword_min"`
|
||||
SearchKeywordMax int `json:"search_keyword_max"`
|
||||
PageSizeDefault int `json:"page_size_default"`
|
||||
FeedMaxPages int `json:"feed_max_pages"`
|
||||
FeedMaxItems int `json:"feed_max_items"`
|
||||
PasswordMinLen int `json:"password_min_len"`
|
||||
AvatarMaxMB int `json:"avatar_max_mb"`
|
||||
PasswordMinLen int `json:"password_min_len"`
|
||||
AvatarMaxMB int `json:"avatar_max_mb"`
|
||||
|
||||
OpenPostsInNewTab bool `json:"open_posts_in_new_tab"`
|
||||
OpenContentLinksInNewTab bool `json:"open_content_links_in_new_tab"`
|
||||
}
|
||||
|
||||
type settingDef struct {
|
||||
@@ -105,14 +139,86 @@ var forumSettingDefs = []settingDef{
|
||||
{SettingSearchKeywordMin, "1", 0, 100},
|
||||
{SettingSearchKeywordMax, "50", 1, 200},
|
||||
|
||||
{SettingPageSizeDefault, "30", 1, 200},
|
||||
{SettingPageSizeMax, "50", 1, 200},
|
||||
|
||||
{SettingFeedMaxPages, "10", 1, 100},
|
||||
{SettingFeedMaxItems, "300", 1, 5000},
|
||||
{SettingPageSizeDefault, "30", 1, pageSizeAPIMax},
|
||||
|
||||
{SettingPasswordMinLen, "6", 4, 128},
|
||||
{SettingAvatarMaxMB, "2", 1, 20},
|
||||
|
||||
{SettingOpenPostsInNewTab, "1", 0, 1},
|
||||
{SettingOpenContentLinksInNewTab, "1", 0, 1},
|
||||
}
|
||||
|
||||
var mailSettingDefaults = map[string]string{
|
||||
SettingSMTPEnabled: "0",
|
||||
SettingSMTPHost: "",
|
||||
SettingSMTPPort: "465",
|
||||
SettingSMTPUsername: "",
|
||||
SettingSMTPPassword: "",
|
||||
SettingSMTPFrom: "",
|
||||
SettingSMTPFromName: "姜十三论坛",
|
||||
SettingSMTPEncryption: "ssl",
|
||||
}
|
||||
|
||||
var oidcSettingDefaults = map[string]string{
|
||||
SettingOIDCEnabled: "0",
|
||||
SettingOIDCRootURL: "",
|
||||
SettingOIDCGroupClaim: "groups",
|
||||
SettingOIDCAdminGroup: "gitea-admin",
|
||||
SettingOIDCUserGroup: "gitea-users",
|
||||
SettingOAuthClientID: "",
|
||||
SettingOAuthClientSecret: "",
|
||||
SettingOAuthRedirectURIs: "",
|
||||
}
|
||||
|
||||
var giteaSettingDefaults = map[string]string{
|
||||
SettingGiteaSyncEnabled: "0",
|
||||
SettingGiteaBaseURL: "",
|
||||
SettingGiteaToken: "",
|
||||
SettingGiteaSyncIntervalMin: "60",
|
||||
}
|
||||
|
||||
var siteBrandingDefaults = map[string]string{
|
||||
SettingSiteName: "姜十三论坛",
|
||||
SettingSiteNameEN: "Jiang13 Forum",
|
||||
SettingSiteSlogan: "拾三一隅,自在交流",
|
||||
SettingSiteLogoMark: "姜",
|
||||
SettingSiteLogo: "",
|
||||
SettingSiteFavicon: "",
|
||||
}
|
||||
|
||||
// SiteBranding 站点品牌配置(名称、Logo、Favicon 等)
|
||||
type SiteBranding struct {
|
||||
Name string `json:"name"`
|
||||
NameEN string `json:"name_en"`
|
||||
Slogan string `json:"slogan"`
|
||||
LogoMark string `json:"logo_mark"`
|
||||
Logo string `json:"logo"`
|
||||
Favicon string `json:"favicon"`
|
||||
}
|
||||
|
||||
// GiteaSyncConfig Gitea 仓库同步配置
|
||||
type GiteaSyncConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Token string `json:"token,omitempty"` // 更新时传入;回显时为空
|
||||
HasToken bool `json:"has_token"`
|
||||
SyncIntervalMin int `json:"sync_interval_min"`
|
||||
Ready bool `json:"ready"`
|
||||
RepoCount int64 `json:"repo_count"`
|
||||
}
|
||||
|
||||
// OIDCConfig OIDC Provider 全局配置(应用凭证见 oauth_clients)
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RootURL string `json:"root_url"`
|
||||
Ready bool `json:"ready"`
|
||||
DiscoveryURL string `json:"discovery_url,omitempty"`
|
||||
AuthorizeURL string `json:"authorize_url,omitempty"`
|
||||
LogoutURL string `json:"logout_url,omitempty"`
|
||||
GroupClaim string `json:"group_claim"`
|
||||
AdminGroup string `json:"admin_group"`
|
||||
UserGroup string `json:"user_group"`
|
||||
ClientCount int64 `json:"client_count"`
|
||||
}
|
||||
|
||||
// ForumSettingsService 论坛全局设置
|
||||
@@ -134,6 +240,48 @@ func (s *ForumSettingsService) ensureDefaults() {
|
||||
model.DB.Create(&model.ForumSetting{Key: def.key, Value: def.defaultVal})
|
||||
}
|
||||
}
|
||||
for key, val := range mailSettingDefaults {
|
||||
var count int64
|
||||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||||
if count == 0 {
|
||||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||||
}
|
||||
}
|
||||
for key, val := range oidcSettingDefaults {
|
||||
var count int64
|
||||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||||
if count == 0 {
|
||||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||||
}
|
||||
}
|
||||
for key, val := range giteaSettingDefaults {
|
||||
var count int64
|
||||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||||
if count == 0 {
|
||||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||||
}
|
||||
}
|
||||
for key, val := range siteBrandingDefaults {
|
||||
var count int64
|
||||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||||
if count == 0 {
|
||||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ForumSettingsService) getString(key, fallback string) string {
|
||||
var setting model.ForumSetting
|
||||
if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil {
|
||||
return fallback
|
||||
}
|
||||
return setting.Value
|
||||
}
|
||||
|
||||
func (s *ForumSettingsService) setString(key, value string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return model.DB.Save(&model.ForumSetting{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func (s *ForumSettingsService) getInt(key string, fallback int) int {
|
||||
@@ -186,13 +334,12 @@ func (s *ForumSettingsService) Limits() ForumLimits {
|
||||
SearchKeywordMax: s.SearchKeywordMax(),
|
||||
|
||||
PageSizeDefault: s.PageSizeDefault(),
|
||||
PageSizeMax: s.PageSizeMax(),
|
||||
|
||||
FeedMaxPages: s.FeedMaxPages(),
|
||||
FeedMaxItems: s.FeedMaxItems(),
|
||||
|
||||
PasswordMinLen: s.PasswordMinLen(),
|
||||
AvatarMaxMB: s.AvatarMaxMB(),
|
||||
|
||||
OpenPostsInNewTab: s.OpenPostsInNewTab(),
|
||||
OpenContentLinksInNewTab: s.OpenContentLinksInNewTab(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +353,11 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic {
|
||||
SearchKeywordMin: limits.SearchKeywordMin,
|
||||
SearchKeywordMax: limits.SearchKeywordMax,
|
||||
PageSizeDefault: limits.PageSizeDefault,
|
||||
FeedMaxPages: limits.FeedMaxPages,
|
||||
FeedMaxItems: limits.FeedMaxItems,
|
||||
PasswordMinLen: limits.PasswordMinLen,
|
||||
AvatarMaxMB: limits.AvatarMaxMB,
|
||||
|
||||
OpenPostsInNewTab: limits.OpenPostsInNewTab,
|
||||
OpenContentLinksInNewTab: limits.OpenContentLinksInNewTab,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,23 +376,30 @@ func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
||||
SettingSearchKeywordMin: in.SearchKeywordMin,
|
||||
SettingSearchKeywordMax: in.SearchKeywordMax,
|
||||
SettingPageSizeDefault: in.PageSizeDefault,
|
||||
SettingPageSizeMax: in.PageSizeMax,
|
||||
SettingFeedMaxPages: in.FeedMaxPages,
|
||||
SettingFeedMaxItems: in.FeedMaxItems,
|
||||
SettingPasswordMinLen: in.PasswordMinLen,
|
||||
SettingAvatarMaxMB: in.AvatarMaxMB,
|
||||
}
|
||||
if in.SearchKeywordMax > 0 && in.SearchKeywordMin > in.SearchKeywordMax {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
if in.PageSizeDefault > in.PageSizeMax {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
for key, val := range updates {
|
||||
if err := s.setInt(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
boolUpdates := map[string]bool{
|
||||
SettingOpenPostsInNewTab: in.OpenPostsInNewTab,
|
||||
SettingOpenContentLinksInNewTab: in.OpenContentLinksInNewTab,
|
||||
}
|
||||
for key, on := range boolUpdates {
|
||||
v := "0"
|
||||
if on {
|
||||
v = "1"
|
||||
}
|
||||
if err := s.setString(key, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -280,14 +435,370 @@ func (s *ForumSettingsService) SearchKeywordMin() int { return s.getInt(SettingS
|
||||
func (s *ForumSettingsService) SearchKeywordMax() int { return s.getInt(SettingSearchKeywordMax, 50) }
|
||||
|
||||
func (s *ForumSettingsService) PageSizeDefault() int { return s.getInt(SettingPageSizeDefault, 30) }
|
||||
func (s *ForumSettingsService) PageSizeMax() int { return s.getInt(SettingPageSizeMax, 50) }
|
||||
|
||||
func (s *ForumSettingsService) FeedMaxPages() int { return s.getInt(SettingFeedMaxPages, 10) }
|
||||
func (s *ForumSettingsService) FeedMaxItems() int { return s.getInt(SettingFeedMaxItems, 300) }
|
||||
|
||||
func (s *ForumSettingsService) PasswordMinLen() int { return s.getInt(SettingPasswordMinLen, 6) }
|
||||
func (s *ForumSettingsService) AvatarMaxMB() int { return s.getInt(SettingAvatarMaxMB, 2) }
|
||||
|
||||
func (s *ForumSettingsService) OpenPostsInNewTab() bool {
|
||||
return s.getString(SettingOpenPostsInNewTab, "1") == "1"
|
||||
}
|
||||
|
||||
func (s *ForumSettingsService) OpenContentLinksInNewTab() bool {
|
||||
return s.getString(SettingOpenContentLinksInNewTab, "1") == "1"
|
||||
}
|
||||
|
||||
// MailConfig 读取 SMTP 配置(密码不回显明文)
|
||||
func (s *ForumSettingsService) MailConfig() MailConfig {
|
||||
port, _ := strconv.Atoi(s.getString(SettingSMTPPort, "465"))
|
||||
if port <= 0 {
|
||||
port = 465
|
||||
}
|
||||
password := s.getString(SettingSMTPPassword, "")
|
||||
return MailConfig{
|
||||
Enabled: s.getString(SettingSMTPEnabled, "0") == "1",
|
||||
Host: s.getString(SettingSMTPHost, ""),
|
||||
Port: port,
|
||||
Username: s.getString(SettingSMTPUsername, ""),
|
||||
Password: password,
|
||||
From: s.getString(SettingSMTPFrom, ""),
|
||||
FromName: s.getString(SettingSMTPFromName, "姜十三论坛"),
|
||||
Encryption: normalizeEncryption(s.getString(SettingSMTPEncryption, "ssl")),
|
||||
HasPassword: password != "",
|
||||
}
|
||||
}
|
||||
|
||||
// MailConfigPublic 管理端回显(不含密码明文)
|
||||
func (s *ForumSettingsService) MailConfigPublic() MailConfig {
|
||||
cfg := s.MailConfig()
|
||||
cfg.Password = ""
|
||||
return cfg
|
||||
}
|
||||
|
||||
// MailReady 邮件服务是否可用于发信
|
||||
func (s *ForumSettingsService) MailReady() bool {
|
||||
cfg := s.MailConfig()
|
||||
return cfg.Enabled &&
|
||||
strings.TrimSpace(cfg.Host) != "" &&
|
||||
cfg.Port > 0 &&
|
||||
strings.TrimSpace(cfg.From) != "" &&
|
||||
strings.TrimSpace(cfg.Username) != "" &&
|
||||
cfg.HasPassword
|
||||
}
|
||||
|
||||
// UpdateMailConfig 更新 SMTP 配置;密码为空表示保持原值
|
||||
func (s *ForumSettingsService) UpdateMailConfig(in MailConfig) error {
|
||||
enc := normalizeEncryption(in.Encryption)
|
||||
if enc != "none" && enc != "starttls" && enc != "ssl" {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
port := in.Port
|
||||
if port <= 0 {
|
||||
port = 465
|
||||
}
|
||||
enabled := "0"
|
||||
if in.Enabled {
|
||||
enabled = "1"
|
||||
}
|
||||
updates := map[string]string{
|
||||
SettingSMTPEnabled: enabled,
|
||||
SettingSMTPHost: strings.TrimSpace(in.Host),
|
||||
SettingSMTPPort: strconv.Itoa(port),
|
||||
SettingSMTPUsername: strings.TrimSpace(in.Username),
|
||||
SettingSMTPFrom: strings.TrimSpace(in.From),
|
||||
SettingSMTPFromName: strings.TrimSpace(in.FromName),
|
||||
SettingSMTPEncryption: enc,
|
||||
}
|
||||
if strings.TrimSpace(in.Password) != "" {
|
||||
updates[SettingSMTPPassword] = in.Password
|
||||
}
|
||||
for key, val := range updates {
|
||||
if err := s.setString(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OIDCConfig 读取 OIDC 全局配置
|
||||
func (s *ForumSettingsService) OIDCConfig() OIDCConfig {
|
||||
root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
|
||||
clientCount := CountEnabledOAuthClients()
|
||||
cfg := OIDCConfig{
|
||||
Enabled: s.getString(SettingOIDCEnabled, "0") == "1",
|
||||
RootURL: root,
|
||||
GroupClaim: strings.TrimSpace(s.getString(SettingOIDCGroupClaim, "groups")),
|
||||
AdminGroup: strings.TrimSpace(s.getString(SettingOIDCAdminGroup, "gitea-admin")),
|
||||
UserGroup: strings.TrimSpace(s.getString(SettingOIDCUserGroup, "gitea-users")),
|
||||
ClientCount: clientCount,
|
||||
}
|
||||
if cfg.GroupClaim == "" {
|
||||
cfg.GroupClaim = "groups"
|
||||
}
|
||||
cfg.Ready = cfg.Enabled && cfg.RootURL != "" && clientCount > 0
|
||||
if root != "" {
|
||||
cfg.DiscoveryURL = root + "/.well-known/openid-configuration"
|
||||
cfg.AuthorizeURL = root + "/oauth/authorize"
|
||||
cfg.LogoutURL = root + "/oauth/logout"
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// OIDCConfigPublic 管理端回显
|
||||
func (s *ForumSettingsService) OIDCConfigPublic() OIDCConfig {
|
||||
return s.OIDCConfig()
|
||||
}
|
||||
|
||||
// UpdateOIDCConfig 更新 OIDC 全局配置(不含 OAuth 应用凭证)
|
||||
func (s *ForumSettingsService) UpdateOIDCConfig(in OIDCConfig) error {
|
||||
root := normalizeRootURL(in.RootURL)
|
||||
if root != "" && !strings.HasPrefix(root, "http://") && !strings.HasPrefix(root, "https://") {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
enabled := "0"
|
||||
if in.Enabled {
|
||||
enabled = "1"
|
||||
}
|
||||
groupClaim := strings.TrimSpace(in.GroupClaim)
|
||||
if groupClaim == "" {
|
||||
groupClaim = "groups"
|
||||
}
|
||||
adminGroup := strings.TrimSpace(in.AdminGroup)
|
||||
userGroup := strings.TrimSpace(in.UserGroup)
|
||||
updates := map[string]string{
|
||||
SettingOIDCEnabled: enabled,
|
||||
SettingOIDCRootURL: root,
|
||||
SettingOIDCGroupClaim: groupClaim,
|
||||
SettingOIDCAdminGroup: adminGroup,
|
||||
SettingOIDCUserGroup: userGroup,
|
||||
}
|
||||
for key, val := range updates {
|
||||
if err := s.setString(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedOIDCFromINI 若库中尚未配置,则用 app.ini 种子一次(便于迁移)
|
||||
func (s *ForumSettingsService) SeedOIDCFromINI(rootURL, clientID, clientSecret, redirectURIsCSV string) {
|
||||
rootURL = normalizeRootURL(rootURL)
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
clientSecret = strings.TrimSpace(clientSecret)
|
||||
uris := normalizeRedirectURIs(redirectURIsCSV)
|
||||
if rootURL == "" && clientID == "" && clientSecret == "" && uris == "" {
|
||||
return
|
||||
}
|
||||
if s.getString(SettingOIDCRootURL, "") == "" && rootURL != "" {
|
||||
_ = s.setString(SettingOIDCRootURL, rootURL)
|
||||
}
|
||||
if s.getString(SettingOAuthClientID, "") == "" && clientID != "" {
|
||||
_ = s.setString(SettingOAuthClientID, clientID)
|
||||
}
|
||||
if s.getString(SettingOAuthClientSecret, "") == "" && clientSecret != "" {
|
||||
_ = s.setString(SettingOAuthClientSecret, clientSecret)
|
||||
}
|
||||
if s.getString(SettingOAuthRedirectURIs, "") == "" && uris != "" {
|
||||
_ = s.setString(SettingOAuthRedirectURIs, uris)
|
||||
}
|
||||
s.MigrateLegacyOIDCClient()
|
||||
if s.getString(SettingOIDCEnabled, "0") == "0" &&
|
||||
s.getString(SettingOIDCRootURL, "") != "" &&
|
||||
CountEnabledOAuthClients() > 0 {
|
||||
_ = s.setString(SettingOIDCEnabled, "1")
|
||||
}
|
||||
}
|
||||
|
||||
// GiteaSyncConfig 读取 Gitea 同步配置(含 Token 明文,供服务内部使用)
|
||||
func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig {
|
||||
interval, _ := strconv.Atoi(s.getString(SettingGiteaSyncIntervalMin, "60"))
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
}
|
||||
if interval > 24*60 {
|
||||
interval = 24 * 60
|
||||
}
|
||||
token := s.getString(SettingGiteaToken, "")
|
||||
base := normalizeRootURL(s.getString(SettingGiteaBaseURL, ""))
|
||||
cfg := GiteaSyncConfig{
|
||||
Enabled: s.getString(SettingGiteaSyncEnabled, "0") == "1",
|
||||
BaseURL: base,
|
||||
Token: token,
|
||||
HasToken: token != "",
|
||||
SyncIntervalMin: interval,
|
||||
}
|
||||
cfg.Ready = cfg.Enabled && base != "" && cfg.HasToken
|
||||
var n int64
|
||||
model.DB.Model(&model.GiteaRepo{}).Where("private = ?", false).Count(&n)
|
||||
cfg.RepoCount = n
|
||||
return cfg
|
||||
}
|
||||
|
||||
// GiteaSyncConfigPublic 管理端回显(不含 Token 明文)
|
||||
func (s *ForumSettingsService) GiteaSyncConfigPublic() GiteaSyncConfig {
|
||||
cfg := s.GiteaSyncConfig()
|
||||
cfg.Token = ""
|
||||
return cfg
|
||||
}
|
||||
|
||||
// UpdateGiteaSyncConfig 更新同步配置;Token 为空表示保持原值
|
||||
func (s *ForumSettingsService) UpdateGiteaSyncConfig(in GiteaSyncConfig) error {
|
||||
base := normalizeRootURL(in.BaseURL)
|
||||
if base != "" && !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
interval := in.SyncIntervalMin
|
||||
if interval <= 0 {
|
||||
interval = 60
|
||||
}
|
||||
if interval < 5 {
|
||||
interval = 5
|
||||
}
|
||||
if interval > 24*60 {
|
||||
interval = 24 * 60
|
||||
}
|
||||
enabled := "0"
|
||||
if in.Enabled {
|
||||
enabled = "1"
|
||||
}
|
||||
updates := map[string]string{
|
||||
SettingGiteaSyncEnabled: enabled,
|
||||
SettingGiteaBaseURL: base,
|
||||
SettingGiteaSyncIntervalMin: strconv.Itoa(interval),
|
||||
}
|
||||
if strings.TrimSpace(in.Token) != "" {
|
||||
updates[SettingGiteaToken] = strings.TrimSpace(in.Token)
|
||||
}
|
||||
for key, val := range updates {
|
||||
if err := s.setString(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedGiteaFromINI 若库中尚未配置,则用 app.ini 种子一次
|
||||
func (s *ForumSettingsService) SeedGiteaFromINI(baseURL, token string, enabled bool) {
|
||||
baseURL = normalizeRootURL(baseURL)
|
||||
token = strings.TrimSpace(token)
|
||||
if baseURL == "" && token == "" && !enabled {
|
||||
return
|
||||
}
|
||||
if s.getString(SettingGiteaBaseURL, "") == "" && baseURL != "" {
|
||||
_ = s.setString(SettingGiteaBaseURL, baseURL)
|
||||
}
|
||||
if s.getString(SettingGiteaToken, "") == "" && token != "" {
|
||||
_ = s.setString(SettingGiteaToken, token)
|
||||
}
|
||||
if enabled && s.getString(SettingGiteaSyncEnabled, "0") == "0" &&
|
||||
s.getString(SettingGiteaBaseURL, "") != "" &&
|
||||
s.getString(SettingGiteaToken, "") != "" {
|
||||
_ = s.setString(SettingGiteaSyncEnabled, "1")
|
||||
}
|
||||
}
|
||||
|
||||
// SiteBranding 读取站点品牌配置
|
||||
func (s *ForumSettingsService) SiteBranding() SiteBranding {
|
||||
name := strings.TrimSpace(s.getString(SettingSiteName, siteBrandingDefaults[SettingSiteName]))
|
||||
if name == "" {
|
||||
name = siteBrandingDefaults[SettingSiteName]
|
||||
}
|
||||
mark := strings.TrimSpace(s.getString(SettingSiteLogoMark, siteBrandingDefaults[SettingSiteLogoMark]))
|
||||
if mark == "" {
|
||||
mark = siteBrandingDefaults[SettingSiteLogoMark]
|
||||
}
|
||||
// 字标取首个字符(支持中文)
|
||||
runes := []rune(mark)
|
||||
if len(runes) > 1 {
|
||||
mark = string(runes[0])
|
||||
}
|
||||
return SiteBranding{
|
||||
Name: name,
|
||||
NameEN: strings.TrimSpace(s.getString(SettingSiteNameEN, siteBrandingDefaults[SettingSiteNameEN])),
|
||||
Slogan: strings.TrimSpace(s.getString(SettingSiteSlogan, siteBrandingDefaults[SettingSiteSlogan])),
|
||||
LogoMark: mark,
|
||||
Logo: strings.TrimSpace(s.getString(SettingSiteLogo, "")),
|
||||
Favicon: strings.TrimSpace(s.getString(SettingSiteFavicon, "")),
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSiteBranding 更新品牌文案;Logo/Favicon URL 由上传接口单独写入
|
||||
func (s *ForumSettingsService) UpdateSiteBranding(in SiteBranding) error {
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
if len([]rune(name)) > 64 {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
mark := strings.TrimSpace(in.LogoMark)
|
||||
if mark == "" {
|
||||
mark = siteBrandingDefaults[SettingSiteLogoMark]
|
||||
}
|
||||
runes := []rune(mark)
|
||||
if len(runes) > 1 {
|
||||
mark = string(runes[0])
|
||||
}
|
||||
nameEN := strings.TrimSpace(in.NameEN)
|
||||
if len([]rune(nameEN)) > 64 {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
slogan := strings.TrimSpace(in.Slogan)
|
||||
if len([]rune(slogan)) > 200 {
|
||||
return ErrInvalidSetting
|
||||
}
|
||||
updates := map[string]string{
|
||||
SettingSiteName: name,
|
||||
SettingSiteNameEN: nameEN,
|
||||
SettingSiteSlogan: slogan,
|
||||
SettingSiteLogoMark: mark,
|
||||
}
|
||||
for key, val := range updates {
|
||||
if err := s.setString(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSiteLogo 写入 Logo URL(空串表示清除)
|
||||
func (s *ForumSettingsService) SetSiteLogo(url string) error {
|
||||
return s.setString(SettingSiteLogo, strings.TrimSpace(url))
|
||||
}
|
||||
|
||||
// SetSiteFavicon 写入 Favicon URL(空串表示清除)
|
||||
func (s *ForumSettingsService) SetSiteFavicon(url string) error {
|
||||
return s.setString(SettingSiteFavicon, strings.TrimSpace(url))
|
||||
}
|
||||
|
||||
func normalizeRootURL(raw string) string {
|
||||
return strings.TrimRight(strings.TrimSpace(raw), "/")
|
||||
}
|
||||
|
||||
func normalizeRedirectURIs(raw string) string {
|
||||
parts := splitRedirectURIs(raw)
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func splitRedirectURIs(raw string) []string {
|
||||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||||
raw = strings.ReplaceAll(raw, "\n", ",")
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[p]; ok {
|
||||
continue
|
||||
}
|
||||
seen[p] = struct{}{}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NormalizeSearchKeyword 校验并规范化搜索关键词,空字符串表示无搜索
|
||||
func (s *ForumSettingsService) NormalizeSearchKeyword(keyword string) (string, error) {
|
||||
kw := trimRunes(keyword)
|
||||
@@ -311,9 +822,8 @@ func (s *ForumSettingsService) NormalizePageSize(size int) int {
|
||||
if size < 1 {
|
||||
return s.PageSizeDefault()
|
||||
}
|
||||
maxSize := s.PageSizeMax()
|
||||
if maxSize > 0 && size > maxSize {
|
||||
return maxSize
|
||||
if size > pageSizeAPIMax {
|
||||
return pageSizeAPIMax
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user