Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/auto-add-to-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
issues:
types: [opened]
pull_request_target:
types: [opened]
types: [opened, edited]

permissions: {}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-sprint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ name: Check Sprint

on:
pull_request_target:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, edited]
branches: [main]

permissions: {}
Expand Down
27 changes: 0 additions & 27 deletions .github/workflows/octo-pr-feed.yml

This file was deleted.

43 changes: 43 additions & 0 deletions .github/workflows/octo-pr-result-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Octo PR Result Notify

on:
pull_request_target:
types: [closed, reopened]
pull_request_review:
types: [submitted]

permissions: {}

jobs:
pr-result:
if: github.event_name == 'pull_request_target'
uses: Mininglamp-OSS/.github/.github/workflows/octo-pr-result-notify.yml@main
with:
repo_name: ${{ github.event.repository.name }}
pr_number: ${{ github.event.pull_request.number }}
pr_title: ${{ github.event.pull_request.title }}
pr_url: ${{ github.event.pull_request.html_url }}
pr_author: ${{ github.event.pull_request.user.login }}
event_kind: ${{ github.event.action == 'closed' && github.event.pull_request.merged == true && 'pr_merged' || github.event.action == 'closed' && 'pr_closed' || 'pr_reopened' }}
pr_additions: ${{ github.event.pull_request.additions }}
pr_deletions: ${{ github.event.pull_request.deletions }}
pr_changed_files: ${{ github.event.pull_request.changed_files }}
secrets:
OCTO_BOT_TOKEN: ${{ secrets.OCTO_BOT_TOKEN }}

review-result:
if: >-
github.event_name == 'pull_request_review' &&
(github.event.review.state == 'approved' || github.event.review.state == 'changes_requested') &&
github.event.pull_request.head.repo.full_name == github.repository
uses: Mininglamp-OSS/.github/.github/workflows/octo-pr-result-notify.yml@main
with:
repo_name: ${{ github.event.repository.name }}
pr_number: ${{ github.event.pull_request.number }}
pr_title: ${{ github.event.pull_request.title }}
pr_url: ${{ github.event.pull_request.html_url }}
pr_author: ${{ github.event.pull_request.user.login }}
event_kind: ${{ github.event.review.state == 'approved' && 'review_approved' || 'review_changes_requested' }}
reviewer: ${{ github.event.review.user.login }}
secrets:
OCTO_BOT_TOKEN: ${{ secrets.OCTO_BOT_TOKEN }}
22 changes: 22 additions & 0 deletions .github/workflows/octo-pr-review-feed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Octo PR Review Feed

on:
pull_request_target:
types: [opened, ready_for_review, review_requested, synchronize]

permissions: {}

jobs:
notify:
if: ${{ !github.event.pull_request.draft }}
uses: Mininglamp-OSS/.github/.github/workflows/octo-pr-review-feed.yml@main
with:
repo_name: ${{ github.event.repository.name }}
pr_number: ${{ github.event.pull_request.number }}
pr_title: ${{ github.event.pull_request.title }}
pr_url: ${{ github.event.pull_request.html_url }}
pr_author: ${{ github.event.pull_request.user.login }}
event_action: ${{ github.event.action }}
project_group_id: '9ea115c7462b4b45b8c85d07d07e0dde'
secrets:
OCTO_BOT_TOKEN: ${{ secrets.OCTO_BOT_TOKEN }}
46 changes: 45 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,48 @@ stop-dev:
echo " https://github.com/Mininglamp-OSS/octo-deployment"; \
exit 1
env-test:
docker-compose -f ./testenv/docker-compose.yaml up -d
docker-compose -f ./testenv/docker-compose.yaml up -d

# ---- i18n message marker pipeline (TODOS §0.8 / D18) ----------------------
#
# i18n-extract : regenerate tools/i18nmarkers/{shared,server}/active.en-US.toml
# from codes.Register / errcode.register AST call sites.
# i18n-extract-check : CI guard — fails (exit 3) when on-disk markers diverge
# from what extraction would emit. Wired up alongside
# the rest of the 0.10 lint suite.
# i18n-merge : optional convenience target that feeds BOTH generated marker
# files (shared + server) into the upstream `goi18n` CLI to
# produce translate.<lang>.toml stubs for new keys. Requires:
# go install github.com/nicksnyder/go-i18n/v2/goi18n@v2.6.1
# First-run side effect: goi18n rewrites active.<lang>.toml
# into its canonical format (entries sorted by ID, content
# hashes added, top-of-file comments stripped). This is
# expected once translators adopt the goi18n workflow; hashes
# are how goi18n detects source drift requiring re-translation.

.PHONY: i18n-extract i18n-extract-check i18n-merge

i18n-extract:
go run ./pkg/i18n/cmd/octo-i18n-extract

i18n-extract-check:
go run ./pkg/i18n/cmd/octo-i18n-extract -check

i18n-merge: i18n-extract
@command -v goi18n >/dev/null 2>&1 || { \
echo "goi18n not on PATH — install with:"; \
echo " go install github.com/nicksnyder/go-i18n/v2/goi18n@v2.6.1"; \
exit 1; \
}
# CRITICAL: feed BOTH shared and server marker files to goi18n as source
# inputs. With `-sourceLanguage en-US`, goi18n treats the union of source
# files as the canonical message set and rewrites any existing translation
# file (active.zh-CN.toml) to that set — entries not present in the source
# inputs are removed. Omitting the server marker file destructively wipes
# every err.server.* zh-CN translation from active.zh-CN.toml (verified
# against goi18n@v2.6.1 by PR #186 reviewers; preserving server
# translations is the contract this target must hold).
goi18n merge -sourceLanguage en-US -outdir pkg/i18n/locales \
tools/i18nmarkers/shared/active.en-US.toml \
tools/i18nmarkers/server/active.en-US.toml \
pkg/i18n/locales/active.zh-CN.toml
22 changes: 17 additions & 5 deletions modules/common/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,10 @@ func (cn *Common) appConfig(c *wkhttp.Context) {
}
if versionI64 != 0 && int(versionI64) >= appConfigM.Version {
c.JSON(http.StatusOK, &appConfigResp{
Version: appConfigM.Version,
SystemBotUIDs: spacepkg.SystemBotList(),
LocalLoginOff: boolToFlag(cn.systemSettings.LocalLoginOff()),
Version: appConfigM.Version,
SystemBotUIDs: spacepkg.SystemBotList(),
LocalLoginOff: boolToFlag(cn.systemSettings.LocalLoginOff()),
DisableUserCreateSpace: boolToFlag(cn.systemSettings.SpaceDisableUserCreate()),
})
return
}
Expand Down Expand Up @@ -433,8 +434,9 @@ func (cn *Common) appConfig(c *wkhttp.Context) {
OIDCResetPasswordURL: oidcResetPasswordURL(),
OIDCProviders: oidcProviders(),
// YUJ-219-A / GH#1283:单一真源下发系统 Bot UID 列表,替代三端硬编码。
SystemBotUIDs: spacepkg.SystemBotList(),
LocalLoginOff: boolToFlag(cn.systemSettings.LocalLoginOff()),
SystemBotUIDs: spacepkg.SystemBotList(),
LocalLoginOff: boolToFlag(cn.systemSettings.LocalLoginOff()),
DisableUserCreateSpace: boolToFlag(cn.systemSettings.SpaceDisableUserCreate()),
})
}

Expand Down Expand Up @@ -735,6 +737,16 @@ type appConfigResp struct {
// 与 app_config.version 解耦:即使客户端命中 version 短路分支,也必须能拿到
// 最新值,否则 admin 切换开关后老客户端会被本地缓存住。和 SystemBotUIDs 同理。
LocalLoginOff int `json:"local_login_off"`

// DisableUserCreateSpace 控制客户端是否隐藏「创建空间」入口。
// 来源 system_setting space.disable_user_create,回退到 env
// DM_SPACE_DISABLE_USER_CREATE,默认 0(允许创建)。
//
// 与 app_config.version 解耦的原因同 LocalLoginOff:admin 在管理台 toggle
// 后老客户端命中 version 短路分支仍必须看到最新值,否则被本地缓存住失去
// 实时性。后端 POST /v1/space/create 也走同一个 getter 校验,客户端隐藏
// 与服务端拒绝由单一真源驱动,不存在前后端漂移。
DisableUserCreateSpace int `json:"disable_user_create_space"`
}

type oidcProviderResp struct {
Expand Down
65 changes: 65 additions & 0 deletions modules/common/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,71 @@ func TestGetAppConfig_SystemBotUIDsOnVersionShortCircuit(t *testing.T) {
assert.Contains(t, body, `"fileHelper"`)
}

// appconfig 必须下发 disable_user_create_space:默认 0(缺 system_setting 行且
// env 未设置)。客户端据此显示/隐藏「创建空间」入口;缺省必须保持开放。
func TestGetAppConfig_DisableUserCreateSpace_DefaultsZero(t *testing.T) {
t.Setenv("DM_SPACE_DISABLE_USER_CREATE", "")
s, ctx := testutil.NewTestServer()
f := New(ctx)
err := testutil.CleanAllTables(ctx)
assert.NoError(t, err)
err = f.appConfigDB.insert(&appConfigModel{})
assert.NoError(t, err)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/v1/common/appconfig", nil)
req.Header.Set("token", testutil.Token)
s.GetRoute().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `"disable_user_create_space":0`)
}

// DB 写入 disable_user_create=1 时 appconfig 必须下发 1,admin 在管理台切换
// 后客户端下次拉配置即可看到入口隐藏 —— 系统级 KV + Reload 路径的实时性保证。
func TestGetAppConfig_DisableUserCreateSpace_DBOverride(t *testing.T) {
t.Setenv("DM_SPACE_DISABLE_USER_CREATE", "")
s, ctx := testutil.NewTestServer()
f := New(ctx)
err := testutil.CleanAllTables(ctx)
assert.NoError(t, err)
err = f.appConfigDB.insert(&appConfigModel{})
assert.NoError(t, err)

settings := EnsureSystemSettings(ctx)
assert.NoError(t, settings.db.upsert("space", "disable_user_create", "1", settingTypeBool, ""))
assert.NoError(t, settings.Reload())

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/v1/common/appconfig", nil)
req.Header.Set("token", testutil.Token)
s.GetRoute().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `"disable_user_create_space":1`)
}

// version 短路分支同样要下发 disable_user_create_space:老客户端命中版本
// 短路也必须看到当前开关,否则被缓存住失去"实时调整"的能力(与 LocalLoginOff
// 同样的"system_setting 与 app_config.version 解耦"约束)。
func TestGetAppConfig_DisableUserCreateSpace_OnVersionShortCircuit(t *testing.T) {
t.Setenv("DM_SPACE_DISABLE_USER_CREATE", "")
s, ctx := testutil.NewTestServer()
f := New(ctx)
err := testutil.CleanAllTables(ctx)
assert.NoError(t, err)
err = f.appConfigDB.insert(&appConfigModel{})
assert.NoError(t, err)

settings := EnsureSystemSettings(ctx)
assert.NoError(t, settings.db.upsert("space", "disable_user_create", "1", settingTypeBool, ""))
assert.NoError(t, settings.Reload())

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/v1/common/appconfig?version=99999999", nil)
req.Header.Set("token", testutil.Token)
s.GetRoute().ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `"disable_user_create_space":1`)
}

// appconfig 必须下发 local_login_off:默认 0(缺 system_setting 行)。
func TestGetAppConfig_LocalLoginOff_DefaultsZero(t *testing.T) {
s, ctx := testutil.NewTestServer()
Expand Down
6 changes: 6 additions & 0 deletions modules/common/system_setting_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ var systemSettingSchema = []settingDef{
{Category: "login", Key: "local_off", Type: settingTypeBool, Description: "是否关闭本地账号登录入口",
Effective: func(s *SystemSettings) string { return boolToCanonical(s.LocalLoginOff()) }},

// Space user-facing creation toggle — admin 关闭后客户端隐藏创建入口,
// 后端 POST /v1/space/create 直接 403。env DM_SPACE_DISABLE_USER_CREATE
// 仍作 fallback,DB 行为单一真源。
{Category: "space", Key: "disable_user_create", Type: settingTypeBool, Description: "是否关闭普通用户创建空间入口",
Effective: func(s *SystemSettings) string { return boolToCanonical(s.SpaceDisableUserCreate()) }},

// Email server config — formerly yaml-only (Support.* in config.go).
{Category: "support", Key: "email", Type: settingTypeString, Description: "技术支持邮箱(发件人)",
Effective: func(s *SystemSettings) string { return s.SupportEmail() }},
Expand Down
55 changes: 55 additions & 0 deletions modules/common/system_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -427,6 +428,60 @@ func (s *SystemSettings) RawLocalLoginOffFromSnapshot() bool {
return s.getBool("login", "local_off", false)
}

// envSpaceDisableUserCreate 与 modules/space/api.go:envDisableUserCreateSpace
// 保持同名,镜像在 common 包以避免反向依赖 (space 已 import common)。新增/修改
// env 解析规则时两处同步,语义就是: 1/true/yes/on (任意大小写,允许前后空格)
// 视为 ON,其余皆 OFF。
const envSpaceDisableUserCreate = "DM_SPACE_DISABLE_USER_CREATE"

// SpaceDisableUserCreate reports whether the user-facing「创建空间」入口应被
// 关闭。完整 fallback 链(按优先级):
//
// 1. DB 行存在且 value 非空 → 走 getBool 解析(1/true/TRUE → true;
// 0/false/FALSE → false; 未知字面量 → false)。**不再回退到 env** —— 与
// 其他 bool 设置一致,未知字面量等同 "admin 不希望关闭"。
// 2. DB 行不存在,或 value="" → env DM_SPACE_DISABLE_USER_CREATE
// 3. 都缺失 → false (保持开放)
//
// 注:manager 写接口对 bool 值已做规范化(只接受 0/1/true/false 及大小写
// 变体),正常路径不会出现未知字面量;此规则覆盖的是有人绕过 API 直接改 DB
// 的边缘场景。
//
// DB 是单一真源:admin 在管理台显式 toggle 立刻生效(Reload 内存快照),
// 多实例 60s 内收敛。env 仅作历史部署兼容入口;新部署应直接走 system_setting。
//
// 与 modules/space/api.go:IsUserCreateDisabled 保持等价语义 —— 后者仍是
// env-only 的低层解析器,留给没有 ctx 的调用方与 yaml 模式;实际请求路径走本
// 方法(modules/space/api.go:createSpace)。
//
// 实现细节:DB 路径委托给 getBool 以与其他 bool 设置共享解析规则,避免双写
// 字面量集合(reviewer H1)。"DB 行是否存在"由独立 lookup 决定,从而区分
// "DB 缺行 → env" 与 "DB 值=0 → 强制 false 压制 env" 两个语义。
func (s *SystemSettings) SpaceDisableUserCreate() bool {
if _, ok := s.lookup("space", "disable_user_create"); ok {
// 走与所有其他 bool 设置一致的字面量解析;未知字面量会落到 fallback=false,
// 与 "DB 显式写了 0" 语义一致 —— 都视为 admin 不希望关闭。
return s.getBool("space", "disable_user_create", false)
}
return parseSpaceDisableUserCreateEnv(os.Getenv(envSpaceDisableUserCreate))
}

// parseSpaceDisableUserCreateEnv 与 modules/space/api.go:IsUserCreateDisabled
// 的解析逻辑保持一致(1/true/yes/on,大小写不敏感,允许前后空格)。两处镜像而
// 非提到 leaf package,理由同 LocalLoginOff/OIDC: 一个 helper 不值得为它引
// 入一层新包。修改任何一处时两边同步,否则同一开关在两个出口语义会漂移。
func parseSpaceDisableUserCreateEnv(v string) bool {
v = strings.TrimSpace(v)
if v == "" {
return false
}
switch strings.ToLower(v) {
case "1", "true", "yes", "on":
return true
}
return false
}

// SupportEmail returns the From address used by the SMTP sender.
func (s *SystemSettings) SupportEmail() string {
return s.getString("support", "email", s.ctx.GetConfig().Support.Email)
Expand Down
Loading
Loading