diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index aff63680..a5d3c025 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -4970,9 +4970,16 @@ const docTemplate = `{ "handler.ResponseSettingGet": { "type": "object", "required": [ + "envs", "yaml" ], "properties": { + "envs": { + "type": "array", + "items": { + "type": "string" + } + }, "yaml": { "type": "string" } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2640220e..eb442a5f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -4963,9 +4963,16 @@ "handler.ResponseSettingGet": { "type": "object", "required": [ + "envs", "yaml" ], "properties": { + "envs": { + "type": "array", + "items": { + "type": "string" + } + }, "yaml": { "type": "string" } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 68391613..59d527cd 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1012,9 +1012,14 @@ definitions: type: object handler.ResponseSettingGet: properties: + envs: + items: + type: string + type: array yaml: type: string required: + - envs - yaml type: object handler.ResponseSettingTemplate: diff --git a/server/handler/setting_get.go b/server/handler/setting_get.go index b6cc79d8..484e24d8 100644 --- a/server/handler/setting_get.go +++ b/server/handler/setting_get.go @@ -2,15 +2,18 @@ package handler import ( "os" + "strings" "github.com/ArtalkJS/Artalk/internal/core" "github.com/ArtalkJS/Artalk/internal/i18n" "github.com/ArtalkJS/Artalk/server/common" "github.com/gofiber/fiber/v2" + "github.com/samber/lo" ) type ResponseSettingGet struct { - Yaml string `json:"yaml"` + Yaml string `json:"yaml"` + Envs []string `json:"envs"` } // @Id GetSettings @@ -30,8 +33,14 @@ func SettingGet(app *core.App, router fiber.Router) { return common.RespError(c, 500, i18n.T("Config file read failed")) } + // get all environment variables which start with ATK_ + envs := lo.Filter(os.Environ(), func(v string, _ int) bool { + return strings.HasPrefix(v, "ATK_") + }) + return common.RespData(c, ResponseSettingGet{ Yaml: string(dat), + Envs: envs, }) })) } diff --git a/ui/artalk-sidebar/src/components/PreferenceArr.vue b/ui/artalk-sidebar/src/components/PreferenceArr.vue index 1ae14c52..143e291f 100644 --- a/ui/artalk-sidebar/src/components/PreferenceArr.vue +++ b/ui/artalk-sidebar/src/components/PreferenceArr.vue @@ -6,6 +6,7 @@ const props = defineProps<{ }>() const customValue = ref([]) +const disabled = ref(false) onMounted(() => { sync() @@ -13,7 +14,14 @@ onMounted(() => { function sync() { const value = settings.get().getCustom(props.node.path) - customValue.value = value && typeof value.toJSON === 'function' ? value.toJSON() : [] + disabled.value = !!settings.get().getEnvByPath(props.node.path) + if (typeof value === 'object' && 'toJSON' in value && typeof value.toJSON === 'function') { + customValue.value = value.toJSON() + } else if (typeof value === 'string') { + customValue.value = value.split(' ') + } else { + customValue.value = [] + } } function save() { @@ -43,21 +51,25 @@ function add() { - + -
+
diff --git a/ui/artalk-sidebar/src/i18n/messages.ts b/ui/artalk-sidebar/src/i18n/messages.ts index 355484c9..e5729691 100644 --- a/ui/artalk-sidebar/src/i18n/messages.ts +++ b/ui/artalk-sidebar/src/i18n/messages.ts @@ -35,6 +35,7 @@ const en = { commentAllowAll: 'Anyone Comment', commentOnlyAdmin: 'Admin Comment Only', config: 'Config', + envVarControlHint: 'Referenced by the environment variable {key}', userAdminHint: 'Admin user', userInConfHint: 'This user is defined in config file', edit: 'Edit', @@ -118,6 +119,7 @@ const zhCN: typeof en = { commentAllowAll: '所有人可评', commentOnlyAdmin: '仅管理员可评', config: '配置文件', + envVarControlHint: '由环境变量 {key} 控制', userAdminHint: '该用户具有管理员权限', userInConfHint: '该用户存在于配置文件中', edit: '编辑', @@ -201,6 +203,7 @@ const zhTW: typeof en = { commentAllowAll: '允許任何人評論', commentOnlyAdmin: '僅允許管理員評論', config: '配置文件', + envVarControlHint: '由環境變數 {key} 參照', userAdminHint: '該用戶具有管理員權限', userInConfHint: '該用戶存在於配置文件中', edit: '編輯', diff --git a/ui/artalk-sidebar/src/lib/settings.ts b/ui/artalk-sidebar/src/lib/settings.ts index 1ec8c1b1..04ec79b2 100644 --- a/ui/artalk-sidebar/src/lib/settings.ts +++ b/ui/artalk-sidebar/src/lib/settings.ts @@ -5,6 +5,7 @@ export class Settings { private tree: OptionNode private flatten: { [path: string]: OptionNode } private customs = shallowRef>() + private envs = shallowRef<{ [key: string]: string }>() constructor(yamlObj: YAML.Document.Parsed) { this.tree = getTree(yamlObj) @@ -27,7 +28,35 @@ export class Settings { this.customs.value = YAML.parseDocument(yamlStr) } + setEnvs(envs: string[]) { + const envsObj: { [key: string]: string } = {} + envs.forEach((env) => { + const [key, value] = env.split('=') + envsObj[key] = value + }) + this.envs.value = envsObj + } + + getEnv(key: string) { + return this.envs.value?.[key] || null + } + + getEnvByPath(path: string) { + // replace `.` to `_` and uppercase + // replace `ATK_TRUSTED_DOMAINS_0` to `ATK_TRUSTED_DOMAINS` + // replace `ATK_ADMIN_USERS_0_NAME` to `ATK_ADMIN_USERS` + return this.getEnv( + 'ATK_' + + path + .replace(/\./g, '_') + .toUpperCase() + .replace(/(_\d+?_\w+|_\d+)$/, ''), + ) + } + getCustom(path: string) { + const env = this.getEnvByPath(path) + if (env) return env return this.customs.value?.getIn(path.split('.')) as any } diff --git a/ui/artalk-sidebar/src/pages/settings.vue b/ui/artalk-sidebar/src/pages/settings.vue index 8a0e1989..35fc287b 100644 --- a/ui/artalk-sidebar/src/pages/settings.vue +++ b/ui/artalk-sidebar/src/pages/settings.vue @@ -33,6 +33,7 @@ onMounted(() => { tree.value = settings.init(yamlObj).getTree() // console.log(tree.value) settings.get().setCustoms(custom.data.yaml) + settings.get().setEnvs(custom.data.envs) }) }) diff --git a/ui/artalk/src/api/v2.ts b/ui/artalk/src/api/v2.ts index 2922ab90..3956e3bd 100644 --- a/ui/artalk/src/api/v2.ts +++ b/ui/artalk/src/api/v2.ts @@ -486,6 +486,7 @@ export interface HandlerResponsePageUpdate { } export interface HandlerResponseSettingGet { + envs: string[] yaml: string } @@ -697,8 +698,8 @@ export class HttpClient { property instanceof Blob ? property : typeof property === 'object' && property !== null - ? JSON.stringify(property) - : `${property}`, + ? JSON.stringify(property) + : `${property}`, ) return formData }, new FormData()), @@ -774,7 +775,7 @@ export class HttpClient { body: typeof body === 'undefined' || body === null ? null : payloadFormatter(body), }, ).then(async (response) => { - const r = response as HttpResponse + const r = response.clone() as HttpResponse r.data = null as unknown as T r.error = null as unknown as E diff --git a/ui/artalk/tests/ui-api.test.ts b/ui/artalk/tests/ui-api.test.ts index 101e060d..5cd3c230 100644 --- a/ui/artalk/tests/ui-api.test.ts +++ b/ui/artalk/tests/ui-api.test.ts @@ -18,49 +18,48 @@ const ContainerID = 'artalk-container' beforeAll(() => { // mock fetch - global.fetch = vi.fn().mockImplementation((url: string, init: RequestInit) => - Promise.resolve({ - ok: true, - status: 200, - json: () => { - let resp: any = {} - - const map = { - '/api/v2/conf': { - frontend_conf: RemoteConf, - version: {}, - }, - '/api/v2/stat': { data: { '/': 0 } }, - '/api/v2/pages/pv': { pv: 2 }, - '/api/v2/notifies': { notifies: [], count: 0 }, - '/api/v2/comments': { - comments: [], - count: 0, - roots_count: 0, - page: { - id: 4, - admin_only: false, - key: '/', - url: '/', - title: 'Artalk DEMO', - site_name: 'ArtalkDocs', - vote_up: 0, - vote_down: 0, - pv: 1, - }, - }, - } - - const path = new URL(url).pathname - - Object.entries(map).forEach(([k, v]) => { - if (path.startsWith(k)) resp = v - }) + global.fetch = vi.fn().mockImplementation((url: string, init: RequestInit) => { + let resp: any = {} - return Promise.resolve(resp) + const map = { + '/api/v2/conf': { + frontend_conf: RemoteConf, + version: {}, + }, + '/api/v2/stat': { data: { '/': 0 } }, + '/api/v2/pages/pv': { pv: 2 }, + '/api/v2/notifies': { notifies: [], count: 0 }, + '/api/v2/comments': { + comments: [], + count: 0, + roots_count: 0, + page: { + id: 4, + admin_only: false, + key: '/', + url: '/', + title: 'Artalk DEMO', + site_name: 'ArtalkDocs', + vote_up: 0, + vote_down: 0, + pv: 1, + }, }, - }), - ) as any + } + + const path = new URL(url).pathname + Object.entries(map).forEach(([k, v]) => { + if (path.startsWith(k)) resp = v + }) + + const mockResponse = new Response(JSON.stringify(resp), { + status: 200, + statusText: 'OK', + headers: { 'Content-Type': 'application/json' }, + }) + + return Promise.resolve(mockResponse) + }) }) describe('Artalk instance', () => {