feat: 创建网站支持直接选择 SSL 证书并启用 HTTPS (#6053)
Some checks failed
sync2gitee / repo-sync (push) Failing after -7m33s

This commit is contained in:
zhengkunwang 2024-08-07 16:40:35 +08:00 committed by GitHub
parent b359c7d990
commit 2f34e1727f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 177 additions and 12 deletions

View file

@ -33,6 +33,7 @@ type WebsiteCreate struct {
RuntimeConfig RuntimeConfig
FtpConfig FtpConfig
DataBaseConfig DataBaseConfig
SSLConfig
} }
type RuntimeConfig struct { type RuntimeConfig struct {
@ -54,6 +55,11 @@ type DataBaseConfig struct {
DBFormat string `json:"dbFormat"` DBFormat string `json:"dbFormat"`
} }
type SSLConfig struct {
EnableSSL bool `json:"enableSSL"`
WebsiteSSLID uint `json:"websiteSSLID"`
}
type NewAppInstall struct { type NewAppInstall struct {
Name string `json:"name"` Name string `json:"name"`
AppDetailId uint `json:"appDetailID"` AppDetailId uint `json:"appDetailID"`

View file

@ -442,6 +442,35 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite) createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
if create.EnableSSL {
enableSSL := func(t *task.Task) error {
websiteModel, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(create.WebsiteSSLID))
if err != nil {
return err
}
website.Protocol = constant.ProtocolHTTPS
website.WebsiteSSLID = create.WebsiteSSLID
appSSLReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
WebsiteSSLID: websiteModel.ID,
Type: "existed",
HttpConfig: "HTTPToHTTPS",
SSLProtocol: []string{"TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1"},
Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED",
Hsts: true,
}
if err = applySSL(*website, *websiteModel, appSSLReq); err != nil {
return err
}
if err = websiteRepo.Save(context.Background(), website); err != nil {
return err
}
return nil
}
createTask.AddSubTaskWithIgnoreErr(i18n.GetMsgByKey("EnableSSL"), enableSSL)
}
return createTask.Execute() return createTask.Execute()
} }

View file

@ -31,13 +31,14 @@ type Task struct {
} }
type SubTask struct { type SubTask struct {
RootTask *Task RootTask *Task
Name string Name string
Retry int Retry int
Timeout time.Duration Timeout time.Duration
Action ActionFunc Action ActionFunc
Rollback RollbackFunc Rollback RollbackFunc
Error error Error error
IgnoreErr bool
} }
const ( const (
@ -111,6 +112,11 @@ func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback Rollba
t.SubTasks = append(t.SubTasks, subTask) t.SubTasks = append(t.SubTasks, subTask)
} }
func (t *Task) AddSubTaskWithIgnoreErr(name string, action ActionFunc) {
subTask := &SubTask{RootTask: t, Name: name, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: nil, IgnoreErr: true}
t.SubTasks = append(t.SubTasks, subTask)
}
func (s *SubTask) Execute() error { func (s *SubTask) Execute() error {
s.RootTask.Log(s.Name) s.RootTask.Log(s.Name)
var err error var err error
@ -166,6 +172,10 @@ func (t *Task) Execute() error {
t.Rollbacks = append(t.Rollbacks, subTask.Rollback) t.Rollbacks = append(t.Rollbacks, subTask.Rollback)
} }
} else { } else {
if subTask.IgnoreErr {
err = nil
continue
}
t.Task.ErrorMsg = err.Error() t.Task.ErrorMsg = err.Error()
t.Task.Status = constant.StatusFailed t.Task.Status = constant.StatusFailed
for _, rollback := range t.Rollbacks { for _, rollback := range t.Rollbacks {

View file

@ -233,4 +233,5 @@ Run: "Run"
Stop: 'Stop', Stop: 'Stop',
Image: 'Image', Image: 'Image',
AppLink: 'Associated Application' AppLink: 'Associated Application'
EnableSSL: "Enable HTTPS"

View file

@ -235,4 +235,5 @@ Run: "啟動"
Stop: '停止', Stop: '停止',
Image: '鏡像', Image: '鏡像',
AppLink: '關聯應用' AppLink: '關聯應用'
EnableSSL: "開啟 HTTPS"

View file

@ -237,3 +237,4 @@ Run: "启动"
Stop: "停止" Stop: "停止"
Image: "镜像" Image: "镜像"
AppLink: "关联应用" AppLink: "关联应用"
EnableSSL: "开启 HTTPS"

View file

@ -80,6 +80,14 @@ export namespace Website {
ftpUser: string; ftpUser: string;
ftpPassword: string; ftpPassword: string;
taskID: string; taskID: string;
SSLID?: number;
enableSSL: boolean;
createDB?: boolean;
dbName?: string;
dbPassword?: string;
dbFormat?: string;
dbUser?: string;
dbHost?: string;
} }
export interface WebSiteUpdateReq { export interface WebSiteUpdateReq {

View file

@ -2128,6 +2128,7 @@ const message = {
sniHelper: sniHelper:
"When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.", "When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.",
createDb: 'Create Database', createDb: 'Create Database',
enableSSLHelper: 'Failure to enable will not affect the creation of the website',
}, },
php: { php: {
short_open_tag: 'Short tag support', short_open_tag: 'Short tag support',

View file

@ -1978,6 +1978,7 @@ const message = {
sni: '回源 SNI', sni: '回源 SNI',
sniHelper: '反代後端為 https 的時候可能需要設置回源 SNI具體需要看 CDN 服務商文檔', sniHelper: '反代後端為 https 的時候可能需要設置回源 SNI具體需要看 CDN 服務商文檔',
createDb: '建立資料庫', createDb: '建立資料庫',
enableSSLHelper: '開啟失敗不會影響網站創建',
}, },
php: { php: {
short_open_tag: '短標簽支持', short_open_tag: '短標簽支持',

View file

@ -1980,6 +1980,7 @@ const message = {
sni: '回源 SNI', sni: '回源 SNI',
sniHelper: '反代后端为 https 的时候可能需要设置回源 SNI具体需要看 CDN 服务商文档', sniHelper: '反代后端为 https 的时候可能需要设置回源 SNI具体需要看 CDN 服务商文档',
createDb: '创建数据库', createDb: '创建数据库',
enableSSLHelper: '开启失败不会影响网站创建',
}, },
php: { php: {
short_open_tag: '短标签支持', short_open_tag: '短标签支持',

View file

@ -16,7 +16,7 @@
<el-text type="warning" class="!ml-2">{{ $t('website.ipWebsiteWarn') }}</el-text> <el-text type="warning" class="!ml-2">{{ $t('website.ipWebsiteWarn') }}</el-text>
<el-divider content-position="left">{{ $t('website.SSLConfig') }}</el-divider> <el-divider content-position="left">{{ $t('website.SSLConfig') }}</el-divider>
<el-form-item :label="$t('website.HTTPConfig')" prop="httpConfig"> <el-form-item :label="$t('website.HTTPConfig')" prop="httpConfig">
<el-select v-model="form.httpConfig" style="width: 240px"> <el-select v-model="form.httpConfig" class="p-w-200">
<el-option :label="$t('website.HTTPToHTTPS')" :value="'HTTPToHTTPS'"></el-option> <el-option :label="$t('website.HTTPToHTTPS')" :value="'HTTPToHTTPS'"></el-option>
<el-option :label="$t('website.HTTPAlso')" :value="'HTTPAlso'"></el-option> <el-option :label="$t('website.HTTPAlso')" :value="'HTTPAlso'"></el-option>
<el-option :label="$t('website.HTTPSOnly')" :value="'HTTPSOnly'"></el-option> <el-option :label="$t('website.HTTPSOnly')" :value="'HTTPSOnly'"></el-option>

View file

@ -351,7 +351,7 @@
<el-form-item prop="createDb" v-if="website.type === 'runtime'"> <el-form-item prop="createDb" v-if="website.type === 'runtime'">
<el-checkbox <el-checkbox
@change="random" @change="randomDbPassword"
v-model="website.createDb" v-model="website.createDb"
:label="$t('website.createDb')" :label="$t('website.createDb')"
size="large" size="large"
@ -415,6 +415,75 @@
</el-col> </el-col>
</el-row> </el-row>
<el-form-item prop="enableSSL">
<el-checkbox v-model="website.enableSSL" :label="$t('website.enableHTTPS')" size="large" />
<span class="input-help">{{ $t('website.enableSSLHelper') }}</span>
</el-form-item>
<div v-if="website.enableSSL">
<el-form-item :label="$t('website.acmeAccountManage')" prop="acmeAccountID">
<el-select
v-model="website.acmeAccountID"
:placeholder="$t('website.selectAcme')"
@change="listSSL"
>
<el-option :key="0" :label="$t('website.imported')" :value="0"></el-option>
<el-option
v-for="(acme, index) in acmeAccounts"
:key="index"
:label="acme.email"
:value="acme.id"
>
<span>
{{ acme.email }}
<el-tag class="ml-5">{{ getAccountName(acme.type) }}</el-tag>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.ssl')" prop="websiteSSLID" :hide-required-asterisk="true">
<el-select
v-model="website.websiteSSLID"
:placeholder="$t('website.selectSSL')"
@change="changeSSl(website.websiteSSLID)"
>
<el-option
v-for="(ssl, index) in ssls"
:key="index"
:label="ssl.primaryDomain"
:value="ssl.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
<el-descriptions :column="7" border direction="vertical">
<el-descriptions-item :label="$t('website.primaryDomain')">
{{ websiteSSL.primaryDomain }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.otherDomains')">
{{ websiteSSL.domains }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">
{{ websiteSSL.organization }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ssl.provider')">
{{ getProvider(websiteSSL.provider) }}
</el-descriptions-item>
<el-descriptions-item
:label="$t('ssl.acmeAccount')"
v-if="websiteSSL.acmeAccount && websiteSSL.provider !== 'manual'"
>
{{ websiteSSL.acmeAccount.email }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.expireDate')">
{{ dateFormatSimple(websiteSSL.expireDate) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.remark')">
{{ websiteSSL.description }}
</el-descriptions-item>
</el-descriptions>
</el-form-item>
</div>
<el-form-item :label="$t('website.remark')" prop="remark"> <el-form-item :label="$t('website.remark')" prop="remark">
<el-input type="textarea" :rows="3" clearable v-model="website.remark" /> <el-input type="textarea" :rows="3" clearable v-model="website.remark" />
</el-form-item> </el-form-item>
@ -441,7 +510,7 @@
<script lang="ts" setup name="CreateWebSite"> <script lang="ts" setup name="CreateWebSite">
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import { GetApp, GetAppDetail, SearchApp, GetAppInstalled, GetAppDetailByID } from '@/api/modules/app'; import { GetApp, GetAppDetail, SearchApp, GetAppInstalled, GetAppDetailByID } from '@/api/modules/app';
import { CreateWebsite, PreCheck } from '@/api/modules/website'; import { CreateWebsite, ListSSL, PreCheck, SearchAcmeAccount } from '@/api/modules/website';
import { Rules, checkNumberRange } from '@/global/form-rules'; import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm, FormInstance } from 'element-plus'; import { ElForm, FormInstance } from 'element-plus';
@ -457,9 +526,12 @@ import { getRandomStr } from '@/utils/util';
import TaskLog from '@/components/task-log/index.vue'; import TaskLog from '@/components/task-log/index.vue';
import { GetAppService } from '@/api/modules/app'; import { GetAppService } from '@/api/modules/app';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { dateFormatSimple, getProvider, getAccountName } from '@/utils/util';
import { Website } from '@/api/interface/website';
const websiteForm = ref<FormInstance>(); const websiteForm = ref<FormInstance>();
const website = ref({
const initData = () => ({
primaryDomain: '', primaryDomain: '',
type: 'deployment', type: 'deployment',
alias: '', alias: '',
@ -502,7 +574,11 @@ const website = ref({
dbUser: '', dbUser: '',
dbType: 'mysql', dbType: 'mysql',
dbHost: '', dbHost: '',
enableSSL: false,
websiteSSLID: undefined,
acmeAccountID: undefined,
}); });
const website = ref(initData());
const rules = ref<any>({ const rules = ref<any>({
primaryDomain: [Rules.domainWithPort], primaryDomain: [Rules.domainWithPort],
alias: [Rules.linuxName], alias: [Rules.linuxName],
@ -529,12 +605,13 @@ const rules = ref<any>({
dbUser: [Rules.requiredInput, Rules.name], dbUser: [Rules.requiredInput, Rules.name],
dbPassword: [Rules.requiredInput, Rules.paramComplexity], dbPassword: [Rules.requiredInput, Rules.paramComplexity],
dbHost: [Rules.requiredSelect], dbHost: [Rules.requiredSelect],
websiteSSLID: [Rules.requiredSelect],
}); });
const open = ref(false); const open = ref(false);
const loading = ref(false); const loading = ref(false);
const groups = ref<Group.GroupInfo[]>([]); const groups = ref<Group.GroupInfo[]>([]);
const acmeAccounts = ref();
const appInstalls = ref<App.AppInstalled[]>([]); const appInstalls = ref<App.AppInstalled[]>([]);
const appReq = reactive({ const appReq = reactive({
type: 'website', type: 'website',
@ -559,6 +636,8 @@ const versionExist = ref(true);
const em = defineEmits(['close']); const em = defineEmits(['close']);
const taskLog = ref(); const taskLog = ref();
const dbServices = ref(); const dbServices = ref();
const ssls = ref();
const websiteSSL = ref();
const handleClose = () => { const handleClose = () => {
open.value = false; open.value = false;
@ -708,6 +787,7 @@ const getRuntimes = async () => {
}; };
const acceptParams = async (installPath: string) => { const acceptParams = async (installPath: string) => {
website.value = initData();
if (websiteForm.value) { if (websiteForm.value) {
websiteForm.value.resetFields(); websiteForm.value.resetFields();
} }
@ -720,6 +800,7 @@ const acceptParams = async (installPath: string) => {
runtimeResource.value = 'appstore'; runtimeResource.value = 'appstore';
searchAppInstalled('website'); searchAppInstalled('website');
listAcmeAccount();
open.value = true; open.value = true;
}; };
@ -746,6 +827,31 @@ const openTaskLog = (taskID: string) => {
taskLog.value.acceptParams(taskID); taskLog.value.acceptParams(taskID);
}; };
const listAcmeAccount = () => {
SearchAcmeAccount({ page: 1, pageSize: 100 }).then((res) => {
acmeAccounts.value = res.data.items || [];
});
};
const listSSL = () => {
ListSSL({
acmeAccountID: String(website.value.acmeAccountID),
}).then((res) => {
ssls.value = res.data || [];
if (ssls.value.length > 0) {
website.value.websiteSSLID = ssls.value[0].id;
changeSSl(website.value.websiteSSLID);
}
});
};
const changeSSl = (sslid: number) => {
const res = ssls.value.filter((element: Website.SSL) => {
return element.id == sslid;
});
websiteSSL.value = res[0];
};
const submit = async (formEl: FormInstance | undefined) => { const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid) => { await formEl.validate((valid) => {