mirror of
https://github.com/caprover/caprover
synced 2026-09-26 08:35:39 +00:00
@@ -14,6 +14,7 @@ import InjectionExtractor from './injection/InjectionExtractor'
|
||||
import * as Injector from './injection/Injector'
|
||||
import DownloadRouter from './routes/download/DownloadRouter'
|
||||
import LoginRouter from './routes/login/LoginRouter'
|
||||
import ThemePublicRouter from './routes/public/ThemePublicRouter'
|
||||
import UserRouter from './routes/user/UserRouter'
|
||||
import CaptainManager from './user/system/CaptainManager'
|
||||
import CaptainConstants from './utils/CaptainConstants'
|
||||
@@ -215,6 +216,7 @@ app.use(
|
||||
API_PREFIX + CaptainConstants.apiVersion + '/downloads/',
|
||||
DownloadRouter
|
||||
)
|
||||
app.use(API_PREFIX + CaptainConstants.apiVersion + '/theme/', ThemePublicRouter)
|
||||
|
||||
// secured end points
|
||||
app.use(API_PREFIX + CaptainConstants.apiVersion + '/user/', UserRouter)
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
AutomatedCleanupConfigsCleaner,
|
||||
IAutomatedCleanupConfigs,
|
||||
} from '../models/AutomatedCleanupConfigs'
|
||||
import CapRoverTheme from '../models/CapRoverTheme'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import CaptainEncryptor from '../utils/Encryptor'
|
||||
import Utils from '../utils/Utils'
|
||||
import AppsDataStore from './AppsDataStore'
|
||||
import ProDataStore from './ProDataStore'
|
||||
import ProjectsDataStore from './ProjectsDataStore'
|
||||
@@ -28,6 +30,8 @@ const NGINX_CAPTAIN_CONFIG = 'nginxCaptainConfig'
|
||||
const CUSTOM_ONE_CLICK_APP_URLS = 'oneClickAppUrls'
|
||||
const FEATURE_FLAGS = 'featureFlags'
|
||||
const AUTOMATED_CLEANUP = 'automatedCleanup'
|
||||
const THEMES = 'themes'
|
||||
const CURRENT_THEME = 'currentTheme'
|
||||
|
||||
const DEFAULT_CAPTAIN_ROOT_DOMAIN = 'captain.localhost'
|
||||
|
||||
@@ -104,6 +108,54 @@ class DataStore {
|
||||
})
|
||||
}
|
||||
|
||||
getThemes(): Promise<CapRoverTheme[]> {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.data.get(THEMES) || []
|
||||
})
|
||||
}
|
||||
|
||||
deleteTheme(themeName: string) {
|
||||
const self = this
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.getThemes()
|
||||
})
|
||||
.then(function (themesFetched) {
|
||||
self.data.set(
|
||||
THEMES,
|
||||
Utils.copyObject(themesFetched).filter(
|
||||
(it) => it.name !== themeName
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
saveThemes(themes: CapRoverTheme[]) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
self.data.set(
|
||||
THEMES,
|
||||
(themes || []).filter((it) => !it.builtIn)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setCurrentTheme(themeName: string | undefined) {
|
||||
const self = this
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return self.data.set(CURRENT_THEME, themeName || '')
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentThemeName(): Promise<string | undefined> {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.data.get(CURRENT_THEME)
|
||||
})
|
||||
}
|
||||
|
||||
setHashedPassword(newHashedPassword: string) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface CapRoverExtraTheme {
|
||||
siderTheme?: string
|
||||
}
|
||||
|
||||
export default interface CapRoverTheme {
|
||||
content: string
|
||||
name: string
|
||||
extra?: string
|
||||
headEmbed?: string
|
||||
builtIn?: boolean
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import express = require('express')
|
||||
import ApiStatusCodes from '../../api/ApiStatusCodes'
|
||||
import BaseApi from '../../api/BaseApi'
|
||||
import { ThemeManagerPublic } from '../../user/ThemeManager'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/current', function (req, res, next) {
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManagerPublic().getCurrentTheme()
|
||||
})
|
||||
.then(function (t) {
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Current theme is retrieved.'
|
||||
)
|
||||
baseApi.data = {
|
||||
theme: t,
|
||||
}
|
||||
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -12,10 +12,12 @@ import CaptainConstants from '../../../utils/CaptainConstants'
|
||||
import Logger from '../../../utils/Logger'
|
||||
import Utils from '../../../utils/Utils'
|
||||
import SystemRouteSelfHostRegistry from './selfhostregistry/SystemRouteSelfHostRegistry'
|
||||
import ThemesRouter from './ThemesRouter'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/selfhostregistry/', SystemRouteSelfHostRegistry)
|
||||
router.use('/themes/', ThemesRouter)
|
||||
|
||||
router.post('/createbackup/', function (req, res, next) {
|
||||
const backupManager = CaptainManager.get().getBackupManager()
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import express = require('express')
|
||||
import ApiStatusCodes from '../../../api/ApiStatusCodes'
|
||||
import BaseApi from '../../../api/BaseApi'
|
||||
import InjectionExtractor from '../../../injection/InjectionExtractor'
|
||||
import CapRoverTheme from '../../../models/CapRoverTheme'
|
||||
import { ThemeManager } from '../../../user/ThemeManager'
|
||||
import Logger from '../../../utils/Logger'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/setcurrent/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
const themeName = req.body.themeName || ''
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).setCurrent(themeName)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Current theme is stored.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/update/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
const oldName = req.body.oldName || ''
|
||||
const theme: CapRoverTheme = {
|
||||
name: req.body.name || '',
|
||||
content: req.body.content || '',
|
||||
extra: req.body.extra || '',
|
||||
headEmbed: req.body.headEmbed || '',
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).updateTheme(oldName, theme)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Theme is stored.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/delete/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
const themeName = req.body.themeName || ''
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).deleteTheme(themeName)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Theme is deleted.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.get('/all/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).getAllThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Themes are retrieved.'
|
||||
)
|
||||
baseApi.data = {
|
||||
themes: themes,
|
||||
}
|
||||
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,258 @@
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import DataStore from '../datastore/DataStore'
|
||||
import DataStoreProvider from '../datastore/DataStoreProvider'
|
||||
import CapRoverTheme from '../models/CapRoverTheme'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import Logger from '../utils/Logger'
|
||||
import Utils from '../utils/Utils'
|
||||
import fs = require('fs-extra')
|
||||
|
||||
const builtInThemes = [] as CapRoverTheme[]
|
||||
|
||||
/**
|
||||
* Parses a string containing themed configuration fields into a JSON object.
|
||||
* Each field must start with "###CapRoverTheme." followed by the field name and its content.
|
||||
* The function dynamically identifies and extracts these fields, preserving their original formatting.
|
||||
* Field names are converted to lowercase to serve as keys in the resulting JSON object,
|
||||
* with the corresponding content as the values, maintaining any internal formatting.
|
||||
*
|
||||
* @param {string} input - Themed configuration string.
|
||||
* @return {Object} JSON object with keys representing field names and values containing the respective content.
|
||||
*
|
||||
* Example:
|
||||
* Input:
|
||||
* "###CapRoverTheme.name:
|
||||
* Green Arrow
|
||||
* ###CapRoverTheme.content:
|
||||
* { colorA: '#fff',
|
||||
* colorB: '#fff'
|
||||
* }"
|
||||
* Output:
|
||||
* { name: "Green Arrow", content: "{ colorA: '#fff' , colorB: '#fff' }" }
|
||||
*/
|
||||
|
||||
function parseCapRoverTheme(input: string) {
|
||||
const result = {} as { [id: string]: string }
|
||||
const lines = input.split('\n')
|
||||
let currentField = undefined as string | undefined
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.startsWith('###CapRoverTheme.')) {
|
||||
// Calculate the start position for the field name and remove the prefix '###CapRoverTheme.'
|
||||
const start = line.indexOf('.') + 1
|
||||
currentField = line.substring(start, line.length - 1).trim()
|
||||
result[currentField] = ''
|
||||
} else if (currentField) {
|
||||
// Check if we already have content for the current field to add a newline
|
||||
if (result[currentField].length > 0) {
|
||||
result[currentField] += '\n' + line
|
||||
} else {
|
||||
result[currentField] += line
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const key in result) {
|
||||
result[key] = result[key].trim()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function populateBuiltInThemes() {
|
||||
const themesDirectory = __dirname + '/../../template/themes'
|
||||
const rawContent = [] as string[]
|
||||
|
||||
const files = fs.readdirSync(themesDirectory).map((it) => {
|
||||
return {
|
||||
fileName: it,
|
||||
number: parseInt(it.split('-')[0]),
|
||||
}
|
||||
})
|
||||
|
||||
files.sort((a, b) => a.number - b.number)
|
||||
|
||||
files
|
||||
.map((it) => it.fileName)
|
||||
.forEach((file) => {
|
||||
const fileContent = fs.readFileSync(
|
||||
`${themesDirectory}/${file}`,
|
||||
'utf8'
|
||||
)
|
||||
rawContent.push(fileContent)
|
||||
})
|
||||
|
||||
rawContent.forEach((it) => {
|
||||
const parsedTheme = {
|
||||
...parseCapRoverTheme(it),
|
||||
builtIn: true,
|
||||
} as CapRoverTheme
|
||||
builtInThemes.push(parsedTheme)
|
||||
})
|
||||
}
|
||||
|
||||
populateBuiltInThemes()
|
||||
|
||||
export class ThemeManager {
|
||||
constructor(private dataStore: DataStore) {}
|
||||
|
||||
getAllThemes() {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return self.dataStore.getThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
return [...builtInThemes, ...themes]
|
||||
})
|
||||
}
|
||||
|
||||
deleteTheme(themeName: string) {
|
||||
const self = this
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return Promise.all([
|
||||
self.getAllThemes(),
|
||||
self.dataStore.getCurrentThemeName(),
|
||||
])
|
||||
})
|
||||
.then(function ([themesFetched, currentTheme]) {
|
||||
const themes = Utils.copyObject(themesFetched)
|
||||
const newThemes = [] as CapRoverTheme[]
|
||||
themes.forEach((it) => {
|
||||
if (it.name !== themeName) {
|
||||
newThemes.push(it)
|
||||
} else if (it.builtIn) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Cannot delete a built-in theme'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (themes.length === newThemes.length) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
if (currentTheme && currentTheme === themeName) {
|
||||
return self.dataStore.setCurrentTheme('')
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
return self.dataStore.deleteTheme(themeName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateTheme(oldName: string, theme: CapRoverTheme) {
|
||||
const self = this
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
theme.builtIn = false
|
||||
return self.getAllThemes()
|
||||
})
|
||||
.then(function (themesFetched) {
|
||||
const themes = Utils.copyObject(themesFetched)
|
||||
const idx = themes.findIndex((t) => t.name === oldName)
|
||||
if (!oldName) {
|
||||
// new theme
|
||||
|
||||
if (themes.some((t) => t.name === theme.name)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Wanted to store a new theme, but it already exists with the same name'
|
||||
)
|
||||
}
|
||||
|
||||
themes.push(theme)
|
||||
} else if (idx >= 0) {
|
||||
// replacing existing theme
|
||||
|
||||
if (themes[idx].builtIn) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Cannot edit a built-in theme'
|
||||
)
|
||||
}
|
||||
|
||||
themes[idx] = theme
|
||||
} else {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
}
|
||||
|
||||
return self.dataStore
|
||||
.saveThemes(themes) //
|
||||
.then(() => {
|
||||
return self.dataStore.setCurrentTheme(theme.name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setCurrent(themeName: string) {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.getAllThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
if (!themeName || themes.some((it) => it.name === themeName))
|
||||
return self.dataStore.setCurrentTheme(themeName)
|
||||
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentTheme(): Promise<CapRoverTheme | undefined> {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return Promise.all([
|
||||
self.getAllThemes(),
|
||||
self.dataStore.getCurrentThemeName(),
|
||||
])
|
||||
})
|
||||
.then(function ([themes, themeName]) {
|
||||
if (!themeName) return undefined
|
||||
|
||||
const theme = themes.find((it) => it.name === themeName)
|
||||
|
||||
if (!theme) {
|
||||
Logger.e(
|
||||
new Error(
|
||||
'Theme name was provided but could not be found: ' +
|
||||
themeName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return theme
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeManagerPublic {
|
||||
private static themeManagerPublic = new ThemeManager(
|
||||
DataStoreProvider.getDataStore(CaptainConstants.rootNameSpace)
|
||||
)
|
||||
|
||||
getCurrentTheme() {
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return ThemeManagerPublic.themeManagerPublic.getCurrentTheme()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import * as yaml from 'yaml'
|
||||
import Logger from './Logger'
|
||||
|
||||
export default class Utils {
|
||||
static copyObject<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj)) as T
|
||||
}
|
||||
|
||||
static removeHttpHttps(input: string) {
|
||||
input = input.trim()
|
||||
input = input.replace(/^(?:http?:\/\/)?/i, '')
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
###CapRoverTheme.name:
|
||||
Legacy
|
||||
|
||||
|
||||
###CapRoverTheme.content:
|
||||
{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#1b8ad3',
|
||||
colorLink: '#1b8ad3',
|
||||
fontFamily: `QuickSand, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
###CapRoverTheme.headEmbed:
|
||||
<link href="https://fonts.googleapis.com/css?family=Quicksand:300,500" rel="stylesheet" />
|
||||
|
||||
|
||||
###CapRoverTheme.extra:
|
||||
{siderTheme:'dark'}
|
||||
Reference in New Issue
Block a user