mirror of https://github.com/halo-dev/halo
Merge pull request #6739 from guqing/refactor/6722
refactor: system initialization process to adapt to the new login methodpull/6801/head^2
commit
845893944c
|
@ -2651,30 +2651,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/plugin-presets": {
|
|
||||||
"get": {
|
|
||||||
"description": "List all plugin presets in the system.",
|
|
||||||
"operationId": "ListPluginPresets",
|
|
||||||
"responses": {
|
|
||||||
"default": {
|
|
||||||
"content": {
|
|
||||||
"*/*": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Plugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "default response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"PluginV1alpha1Console"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/apis/api.console.halo.run/v1alpha1/plugins": {
|
"/apis/api.console.halo.run/v1alpha1/plugins": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List plugins using query criteria and sort params",
|
"description": "List plugins using query criteria and sort params",
|
||||||
|
@ -4309,38 +4285,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/system/initialize": {
|
|
||||||
"post": {
|
|
||||||
"description": "Initialize system",
|
|
||||||
"operationId": "initialize",
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"*/*": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SystemInitializationRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "System initialization successfully.",
|
|
||||||
"headers": {
|
|
||||||
"Location": {
|
|
||||||
"description": "Redirect URL.",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"style": "simple"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"SystemV1alpha1Console"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/apis/api.console.halo.run/v1alpha1/tags": {
|
"/apis/api.console.halo.run/v1alpha1/tags": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List Post Tags.",
|
"description": "List Post Tags.",
|
||||||
|
@ -14722,6 +14666,41 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect": {
|
||||||
|
"put": {
|
||||||
|
"description": "Disconnect my connection from a third-party platform.",
|
||||||
|
"operationId": "DisconnectMyConnection",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The registration ID of the third-party platform.",
|
||||||
|
"in": "path",
|
||||||
|
"name": "registerId",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"default": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserConnection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "default response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"UserConnectionV1alpha1Uc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
|
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List posts owned by the current user.",
|
"description": "List posts owned by the current user.",
|
||||||
|
@ -20712,19 +20691,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PostStatus": {
|
"PostStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -22560,19 +22536,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SinglePageStatus": {
|
"SinglePageStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -22972,29 +22945,6 @@
|
||||||
},
|
},
|
||||||
"description": "The subscriber to be notified"
|
"description": "The subscriber to be notified"
|
||||||
},
|
},
|
||||||
"SystemInitializationRequest": {
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"username"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"email": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"minLength": 3,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"siteTitle": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Tag": {
|
"Tag": {
|
||||||
"required": [
|
"required": [
|
||||||
"apiVersion",
|
"apiVersion",
|
||||||
|
|
|
@ -518,30 +518,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/plugin-presets": {
|
|
||||||
"get": {
|
|
||||||
"description": "List all plugin presets in the system.",
|
|
||||||
"operationId": "ListPluginPresets",
|
|
||||||
"responses": {
|
|
||||||
"default": {
|
|
||||||
"content": {
|
|
||||||
"*/*": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Plugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "default response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"PluginV1alpha1Console"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/apis/api.console.halo.run/v1alpha1/plugins": {
|
"/apis/api.console.halo.run/v1alpha1/plugins": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List plugins using query criteria and sort params",
|
"description": "List plugins using query criteria and sort params",
|
||||||
|
@ -2176,38 +2152,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/system/initialize": {
|
|
||||||
"post": {
|
|
||||||
"description": "Initialize system",
|
|
||||||
"operationId": "initialize",
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"*/*": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SystemInitializationRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "System initialization successfully.",
|
|
||||||
"headers": {
|
|
||||||
"Location": {
|
|
||||||
"description": "Redirect URL.",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"style": "simple"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"SystemV1alpha1Console"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/apis/api.console.halo.run/v1alpha1/tags": {
|
"/apis/api.console.halo.run/v1alpha1/tags": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List Post Tags.",
|
"description": "List Post Tags.",
|
||||||
|
@ -5476,19 +5420,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PostStatus": {
|
"PostStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -6008,19 +5949,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SinglePageStatus": {
|
"SinglePageStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -6090,29 +6028,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SystemInitializationRequest": {
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"username"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"email": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"minLength": 3,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"siteTitle": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Tag": {
|
"Tag": {
|
||||||
"required": [
|
"required": [
|
||||||
"apiVersion",
|
"apiVersion",
|
||||||
|
|
|
@ -11300,19 +11300,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PostStatus": {
|
"PostStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -12487,19 +12484,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SinglePageStatus": {
|
"SinglePageStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
|
|
@ -17,6 +17,41 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect": {
|
||||||
|
"put": {
|
||||||
|
"description": "Disconnect my connection from a third-party platform.",
|
||||||
|
"operationId": "DisconnectMyConnection",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The registration ID of the third-party platform.",
|
||||||
|
"in": "path",
|
||||||
|
"name": "registerId",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"default": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserConnection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "default response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"UserConnectionV1alpha1Uc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
|
"/apis/uc.api.content.halo.run/v1alpha1/posts": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List posts owned by the current user.",
|
"description": "List posts owned by the current user.",
|
||||||
|
@ -1853,19 +1888,16 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "PUBLIC",
|
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
]
|
],
|
||||||
|
"default": "PUBLIC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PostStatus": {
|
"PostStatus": {
|
||||||
"required": [
|
|
||||||
"phase"
|
|
||||||
],
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"commentsCount": {
|
"commentsCount": {
|
||||||
|
@ -2262,6 +2294,52 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UserConnection": {
|
||||||
|
"required": [
|
||||||
|
"apiVersion",
|
||||||
|
"kind",
|
||||||
|
"metadata",
|
||||||
|
"spec"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/components/schemas/Metadata"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"$ref": "#/components/schemas/UserConnectionSpec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UserConnectionSpec": {
|
||||||
|
"required": [
|
||||||
|
"providerUserId",
|
||||||
|
"registrationId",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"providerUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"registrationId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"UserDevice": {
|
"UserDevice": {
|
||||||
"required": [
|
"required": [
|
||||||
"active",
|
"active",
|
||||||
|
|
|
@ -157,7 +157,6 @@ public class Post extends AbstractExtension {
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class PostStatus {
|
public static class PostStatus {
|
||||||
@Schema(requiredMode = RequiredMode.REQUIRED)
|
|
||||||
private String phase;
|
private String phase;
|
||||||
|
|
||||||
@Schema
|
@Schema
|
||||||
|
|
|
@ -71,7 +71,6 @@ import run.halo.app.extension.router.IListRequest;
|
||||||
import run.halo.app.extension.router.SortableRequest;
|
import run.halo.app.extension.router.SortableRequest;
|
||||||
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||||
import run.halo.app.infra.utils.SettingUtils;
|
import run.halo.app.infra.utils.SettingUtils;
|
||||||
import run.halo.app.plugin.PluginNotFoundException;
|
|
||||||
import run.halo.app.plugin.PluginService;
|
import run.halo.app.plugin.PluginService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -298,12 +297,6 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
|
||||||
.response(responseBuilder()
|
.response(responseBuilder()
|
||||||
.implementation(ObjectNode.class))
|
.implementation(ObjectNode.class))
|
||||||
)
|
)
|
||||||
.GET("plugin-presets", this::listPresets,
|
|
||||||
builder -> builder.operationId("ListPluginPresets")
|
|
||||||
.description("List all plugin presets in the system.")
|
|
||||||
.tag(tag)
|
|
||||||
.response(responseBuilder().implementationArray(Plugin.class))
|
|
||||||
)
|
|
||||||
.GET("plugins/-/bundle.js", this::fetchJsBundle,
|
.GET("plugins/-/bundle.js", this::fetchJsBundle,
|
||||||
builder -> builder.operationId("fetchJsBundle")
|
builder -> builder.operationId("fetchJsBundle")
|
||||||
.description("Merge all JS bundles of enabled plugins into one.")
|
.description("Merge all JS bundles of enabled plugins into one.")
|
||||||
|
@ -472,10 +465,6 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
|
||||||
return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);
|
return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<ServerResponse> listPresets(ServerRequest request) {
|
|
||||||
return ServerResponse.ok().body(pluginService.getPresets(), Plugin.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<ServerResponse> fetchPluginConfig(ServerRequest request) {
|
private Mono<ServerResponse> fetchPluginConfig(ServerRequest request) {
|
||||||
final var name = request.pathVariable("name");
|
final var name = request.pathVariable("name");
|
||||||
return client.fetch(Plugin.class, name)
|
return client.fetch(Plugin.class, name)
|
||||||
|
@ -564,10 +553,6 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
|
||||||
if (InstallSource.FILE.equals(source)) {
|
if (InstallSource.FILE.equals(source)) {
|
||||||
return installFromFile(installRequest.getFile(), pluginService::install);
|
return installFromFile(installRequest.getFile(), pluginService::install);
|
||||||
}
|
}
|
||||||
if (InstallSource.PRESET.equals(source)) {
|
|
||||||
return installFromPreset(installRequest.getPresetName(),
|
|
||||||
pluginService::install);
|
|
||||||
}
|
|
||||||
return Mono.error(
|
return Mono.error(
|
||||||
new UnsupportedOperationException("Unsupported install source " + source));
|
new UnsupportedOperationException("Unsupported install source " + source));
|
||||||
}))
|
}))
|
||||||
|
@ -586,10 +571,6 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
|
||||||
return installFromFile(installRequest.getFile(),
|
return installFromFile(installRequest.getFile(),
|
||||||
path -> pluginService.upgrade(pluginName, path));
|
path -> pluginService.upgrade(pluginName, path));
|
||||||
}
|
}
|
||||||
if (InstallSource.PRESET.equals(source)) {
|
|
||||||
return installFromPreset(installRequest.getPresetName(),
|
|
||||||
path -> pluginService.upgrade(pluginName, path));
|
|
||||||
}
|
|
||||||
return Mono.error(
|
return Mono.error(
|
||||||
new UnsupportedOperationException("Unsupported install source " + source));
|
new UnsupportedOperationException("Unsupported install source " + source));
|
||||||
}))
|
}))
|
||||||
|
@ -606,16 +587,6 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
|
||||||
this::deleteFileIfExists);
|
this::deleteFileIfExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Plugin> installFromPreset(Mono<String> presetNameMono,
|
|
||||||
Function<Path, Mono<Plugin>> resourceClosure) {
|
|
||||||
return presetNameMono.flatMap(pluginService::getPreset)
|
|
||||||
.switchIfEmpty(
|
|
||||||
Mono.error(() -> new PluginNotFoundException("Plugin preset was not found.")))
|
|
||||||
.map(pluginPreset -> pluginPreset.getStatus().getLoadLocation())
|
|
||||||
.map(Path::of)
|
|
||||||
.flatMap(resourceClosure);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ListRequest extends SortableRequest {
|
public static class ListRequest extends SortableRequest {
|
||||||
|
|
||||||
public ListRequest(ServerRequest request) {
|
public ListRequest(ServerRequest request) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.core.endpoint;
|
package run.halo.app.core.endpoint.console;
|
||||||
|
|
||||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
|
@ -1,147 +0,0 @@
|
||||||
package run.halo.app.core.endpoint.console;
|
|
||||||
|
|
||||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|
||||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
|
||||||
import static org.springdoc.core.fn.builders.header.Builder.headerBuilder;
|
|
||||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import reactor.util.retry.Retry;
|
|
||||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
|
||||||
import run.halo.app.extension.ConfigMap;
|
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
|
||||||
import run.halo.app.infra.InitializationStateGetter;
|
|
||||||
import run.halo.app.infra.SystemSetting;
|
|
||||||
import run.halo.app.infra.ValidationUtils;
|
|
||||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
|
||||||
import run.halo.app.security.SuperAdminInitializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* System initialization endpoint.
|
|
||||||
*
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.9.0
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class SystemInitializationEndpoint implements CustomEndpoint {
|
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
|
||||||
private final SuperAdminInitializer superAdminInitializer;
|
|
||||||
private final InitializationStateGetter initializationStateSupplier;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RouterFunction<ServerResponse> endpoint() {
|
|
||||||
var tag = "SystemV1alpha1Console";
|
|
||||||
// define a non-resource api
|
|
||||||
return SpringdocRouteBuilder.route()
|
|
||||||
.POST("/system/initialize", this::initialize,
|
|
||||||
builder -> builder.operationId("initialize")
|
|
||||||
.description("Initialize system")
|
|
||||||
.tag(tag)
|
|
||||||
.requestBody(requestBodyBuilder()
|
|
||||||
.implementation(SystemInitializationRequest.class))
|
|
||||||
.response(responseBuilder()
|
|
||||||
.responseCode(HttpStatus.CREATED.value() + "")
|
|
||||||
.description("System initialization successfully.")
|
|
||||||
.header(headerBuilder()
|
|
||||||
.name(HttpHeaders.LOCATION)
|
|
||||||
.description("Redirect URL.")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<ServerResponse> initialize(ServerRequest request) {
|
|
||||||
return request.bodyToMono(SystemInitializationRequest.class)
|
|
||||||
.switchIfEmpty(
|
|
||||||
Mono.error(new ServerWebInputException("Request body must not be empty"))
|
|
||||||
)
|
|
||||||
.doOnNext(requestBody -> {
|
|
||||||
if (!ValidationUtils.validateName(requestBody.getUsername())) {
|
|
||||||
throw new UnsatisfiedAttributeValueException(
|
|
||||||
"The username does not meet the specifications",
|
|
||||||
"problemDetail.user.username.unsatisfied", null);
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(requestBody.getPassword())) {
|
|
||||||
throw new UnsatisfiedAttributeValueException(
|
|
||||||
"The password does not meet the specifications",
|
|
||||||
"problemDetail.user.password.unsatisfied", null);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatMap(requestBody -> initializationStateSupplier.userInitialized()
|
|
||||||
.flatMap(result -> {
|
|
||||||
if (result) {
|
|
||||||
return Mono.error(new ResponseStatusException(HttpStatus.CONFLICT,
|
|
||||||
"System has been initialized"));
|
|
||||||
}
|
|
||||||
return initializeSystem(requestBody);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(ServerResponse.created(URI.create("/console")).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> initializeSystem(SystemInitializationRequest requestBody) {
|
|
||||||
Mono<Void> initializeAdminUser = superAdminInitializer.initialize(
|
|
||||||
SuperAdminInitializer.InitializationParam.builder()
|
|
||||||
.username(requestBody.getUsername())
|
|
||||||
.password(requestBody.getPassword())
|
|
||||||
.email(requestBody.getEmail())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
Mono<Void> siteSetting =
|
|
||||||
Mono.defer(() -> client.get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
|
||||||
.flatMap(config -> {
|
|
||||||
Map<String, String> data = config.getData();
|
|
||||||
if (data == null) {
|
|
||||||
data = new LinkedHashMap<>();
|
|
||||||
config.setData(data);
|
|
||||||
}
|
|
||||||
String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}");
|
|
||||||
SystemSetting.Basic basicSetting =
|
|
||||||
JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);
|
|
||||||
basicSetting.setTitle(requestBody.getSiteTitle());
|
|
||||||
data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));
|
|
||||||
return client.update(config);
|
|
||||||
}))
|
|
||||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
|
||||||
.filter(t -> t instanceof OptimisticLockingFailureException)
|
|
||||||
)
|
|
||||||
.then();
|
|
||||||
return Mono.when(initializeAdminUser, siteSetting);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class SystemInitializationRequest {
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, minLength = 1)
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, minLength = 3)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
private String siteTitle;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package run.halo.app.infra;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|
||||||
import org.springframework.context.ApplicationListener;
|
|
||||||
import org.springframework.core.io.UrlResource;
|
|
||||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
|
||||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.ResourceUtils;
|
|
||||||
import org.springframework.util.StreamUtils;
|
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
|
||||||
import run.halo.app.infra.properties.ThemeProperties;
|
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
|
||||||
import run.halo.app.theme.service.ThemeService;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class DefaultThemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
|
||||||
|
|
||||||
private final ThemeService themeService;
|
|
||||||
|
|
||||||
private final ThemeRootGetter themeRoot;
|
|
||||||
|
|
||||||
private final ThemeProperties themeProps;
|
|
||||||
|
|
||||||
public DefaultThemeInitializer(ThemeService themeService, ThemeRootGetter themeRoot,
|
|
||||||
HaloProperties haloProps) {
|
|
||||||
this.themeService = themeService;
|
|
||||||
this.themeRoot = themeRoot;
|
|
||||||
this.themeProps = haloProps.getTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
|
||||||
if (themeProps.getInitializer().isDisabled()) {
|
|
||||||
log.debug("Skipped initializing default theme due to disabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var themeRoot = this.themeRoot.get();
|
|
||||||
var location = themeProps.getInitializer().getLocation();
|
|
||||||
try {
|
|
||||||
// TODO Checking if any themes are installed here in the future might be better?
|
|
||||||
if (!FileUtils.isEmpty(themeRoot)) {
|
|
||||||
log.debug("Skipped initializing default theme because there are themes "
|
|
||||||
+ "inside theme root");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info("Initializing default theme from {}", location);
|
|
||||||
var themeUrl = ResourceUtils.getURL(location);
|
|
||||||
var content = DataBufferUtils.read(new UrlResource(themeUrl),
|
|
||||||
DefaultDataBufferFactory.sharedInstance,
|
|
||||||
StreamUtils.BUFFER_SIZE);
|
|
||||||
var theme = themeService.install(content).block();
|
|
||||||
log.info("Initialized default theme: {}", theme);
|
|
||||||
// Because default active theme is default, we don't need to enabled it manually.
|
|
||||||
} catch (IOException e) {
|
|
||||||
// we should skip the initialization error at here
|
|
||||||
log.warn("Failed to initialize theme from " + location, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,11 +3,19 @@ package run.halo.app.infra;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.github.fge.jsonpatch.JsonPatchException;
|
import com.github.fge.jsonpatch.JsonPatchException;
|
||||||
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
|
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.utils.JsonParseException;
|
import run.halo.app.infra.utils.JsonParseException;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
@ -68,6 +76,33 @@ public class SystemState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Update system state by the given {@link Consumer}.</p>
|
||||||
|
* <p>if the system state config map does not exist, it will create a new one.</p>
|
||||||
|
*/
|
||||||
|
public static Mono<Void> upsetSystemState(ReactiveExtensionClient client,
|
||||||
|
Consumer<SystemState> consumer) {
|
||||||
|
return Mono.defer(() -> client.fetch(ConfigMap.class, SYSTEM_STATES_CONFIGMAP)
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
ConfigMap configMap = new ConfigMap();
|
||||||
|
configMap.setMetadata(new Metadata());
|
||||||
|
configMap.getMetadata().setName(SYSTEM_STATES_CONFIGMAP);
|
||||||
|
configMap.setData(new HashMap<>());
|
||||||
|
return client.create(configMap);
|
||||||
|
}))
|
||||||
|
.flatMap(configMap -> {
|
||||||
|
SystemState systemState = deserialize(configMap);
|
||||||
|
consumer.accept(systemState);
|
||||||
|
update(systemState, configMap);
|
||||||
|
return client.update(configMap);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||||
|
.filter(OptimisticLockingFailureException.class::isInstance)
|
||||||
|
)
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
private static String emptyJsonObject() {
|
private static String emptyJsonObject() {
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,14 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class ValidationUtils {
|
public class ValidationUtils {
|
||||||
public static final Pattern NAME_PATTERN =
|
public static final String NAME_REGEX =
|
||||||
Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$");
|
"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$";
|
||||||
|
public static final Pattern NAME_PATTERN = Pattern.compile(NAME_REGEX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No Chinese, no spaces.
|
||||||
|
*/
|
||||||
|
public static final String PASSWORD_REGEX = "^(?!.*[\\u4e00-\\u9fa5])(?=\\S+$).+$";
|
||||||
|
|
||||||
public static final String EMAIL_REGEX =
|
public static final String EMAIL_REGEX =
|
||||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
|
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.util.zip.ZipInputStream;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
@ -296,6 +297,14 @@ public abstract class FileUtils {
|
||||||
.subscribeOn(scheduler);
|
.subscribeOn(scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void copyResource(Resource resource, Path path) {
|
||||||
|
try (var inputStream = resource.getInputStream()) {
|
||||||
|
Files.copy(inputStream, path, REPLACE_EXISTING);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void copy(Path source, Path dest, CopyOption... options) {
|
public static void copy(Path source, Path dest, CopyOption... options) {
|
||||||
try {
|
try {
|
||||||
Files.copy(source, dest, options);
|
Files.copy(source, dest, options);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
@ -14,12 +15,22 @@ import org.springframework.util.StreamUtils;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Halo utilities.
|
||||||
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @date 2022-04-12
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
public class HaloUtils {
|
public class HaloUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request is an XMLHttpRequest.
|
||||||
|
*/
|
||||||
|
public static boolean isXhr(HttpHeaders headers) {
|
||||||
|
return headers.getOrEmpty("X-Requested-With").contains("XMLHttpRequest");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Read the file under the classpath as a string.</p>
|
* <p>Read the file under the classpath as a string.</p>
|
||||||
*
|
*
|
||||||
|
@ -51,7 +62,7 @@ public class HaloUtils {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
||||||
userAgent = httpHeaders.getFirst("Sec-CH-UA");
|
userAgent = httpHeaders.getFirst("Sec-CH-UA");
|
||||||
}
|
}
|
||||||
return StringUtils.defaultString(userAgent, "unknown");
|
return StringUtils.defaultIfBlank(userAgent, "unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getDayText(Instant instant) {
|
public static String getDayText(Instant instant) {
|
||||||
|
|
|
@ -10,15 +10,7 @@ import run.halo.app.core.extension.Plugin;
|
||||||
|
|
||||||
public interface PluginService {
|
public interface PluginService {
|
||||||
|
|
||||||
Flux<Plugin> getPresets();
|
Mono<Void> installPresetPlugins();
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a plugin information by preset name from plugin presets.
|
|
||||||
*
|
|
||||||
* @param presetName is preset name of plugin.
|
|
||||||
* @return plugin preset information.
|
|
||||||
*/
|
|
||||||
Mono<Plugin> getPreset(String presetName);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs a plugin from a temporary Jar path.
|
* Installs a plugin from a temporary Jar path.
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.pf4j.DependencyResolver;
|
import org.pf4j.DependencyResolver;
|
||||||
import org.pf4j.PluginDescriptor;
|
import org.pf4j.PluginDescriptor;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
|
@ -36,6 +37,7 @@ import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
@ -110,18 +112,36 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<Plugin> getPresets() {
|
public Mono<Void> installPresetPlugins() {
|
||||||
// list presets from classpath
|
return getPresetJars()
|
||||||
return Flux.defer(() -> getPresetJars()
|
.flatMap(path -> this.install(path)
|
||||||
.map(this::toPath)
|
.onErrorResume(PluginAlreadyExistsException.class, e -> Mono.empty())
|
||||||
.map(path -> new YamlPluginFinder().find(path)));
|
.flatMap(plugin -> FileUtils.deleteFileSilently(path)
|
||||||
|
.thenReturn(plugin)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flatMap(this::enablePlugin)
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Mono<Plugin> enablePlugin(Plugin plugin) {
|
||||||
public Mono<Plugin> getPreset(String presetName) {
|
plugin.getSpec().setEnabled(true);
|
||||||
return getPresets()
|
return client.update(plugin)
|
||||||
.filter(plugin -> Objects.equals(plugin.getMetadata().getName(), presetName))
|
.onErrorResume(OptimisticLockingFailureException.class,
|
||||||
.next();
|
e -> enablePlugin(plugin.getMetadata().getName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Plugin> enablePlugin(String name) {
|
||||||
|
return Mono.defer(() -> client.get(Plugin.class, name)
|
||||||
|
.flatMap(plugin -> {
|
||||||
|
plugin.getSpec().setEnabled(true);
|
||||||
|
return client.update(plugin);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||||
|
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -481,24 +501,25 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flux<Resource> getPresetJars() {
|
private Flux<Path> getPresetJars() {
|
||||||
var resolver = new PathMatchingResourcePatternResolver();
|
var resolver = new PathMatchingResourcePatternResolver();
|
||||||
try {
|
try {
|
||||||
var resources = resolver.getResources(PRESETS_LOCATION_PATTERN);
|
var resources = resolver.getResources(PRESETS_LOCATION_PATTERN);
|
||||||
return Flux.fromArray(resources);
|
return Flux.fromArray(resources)
|
||||||
|
.mapNotNull(resource -> {
|
||||||
|
var filename = resource.getFilename();
|
||||||
|
if (StringUtils.isBlank(filename)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var path = tempDir.resolve(filename);
|
||||||
|
FileUtils.copyResource(resource, path);
|
||||||
|
return path;
|
||||||
|
});
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Flux.error(e);
|
return Flux.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path toPath(Resource resource) {
|
|
||||||
try {
|
|
||||||
return Path.of(resource.getURI());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw Exceptions.propagate(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updatePlugin(Plugin oldPlugin, Plugin newPlugin) {
|
private static void updatePlugin(Plugin oldPlugin, Plugin newPlugin) {
|
||||||
var oldMetadata = oldPlugin.getMetadata();
|
var oldMetadata = oldPlugin.getMetadata();
|
||||||
var newMetadata = newPlugin.getMetadata();
|
var newMetadata = newPlugin.getMetadata();
|
||||||
|
|
|
@ -18,7 +18,7 @@ public class CsrfConfigurer implements SecurityConfigurer {
|
||||||
public void configure(ServerHttpSecurity http) {
|
public void configure(ServerHttpSecurity http) {
|
||||||
var csrfMatcher = new AndServerWebExchangeMatcher(
|
var csrfMatcher = new AndServerWebExchangeMatcher(
|
||||||
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
|
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
|
||||||
new NegatedServerWebExchangeMatcher(pathMatchers("/api/**", "/apis/**")
|
new NegatedServerWebExchangeMatcher(pathMatchers("/api/**", "/apis/**", "/system/setup")
|
||||||
));
|
));
|
||||||
http.csrf(csrfSpec -> csrfSpec
|
http.csrf(csrfSpec -> csrfSpec
|
||||||
.csrfTokenRepository(withHttpOnlyFalse())
|
.csrfTokenRepository(withHttpOnlyFalse())
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.utils.HaloUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default authentication entry point.
|
* Default authentication entry point.
|
||||||
|
@ -22,8 +23,7 @@ import reactor.core.publisher.Mono;
|
||||||
public class DefaultServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
|
public class DefaultServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
|
||||||
|
|
||||||
private final ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
private final ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
||||||
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With")
|
if (HaloUtils.isXhr(exchange.getRequest().getHeaders())) {
|
||||||
.contains("XMLHttpRequest")) {
|
|
||||||
return MatchResult.match();
|
return MatchResult.match();
|
||||||
}
|
}
|
||||||
return MatchResult.notMatch();
|
return MatchResult.notMatch();
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package run.halo.app.security;
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
|
||||||
|
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -26,9 +30,11 @@ import run.halo.app.infra.InitializationStateGetter;
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InitializeRedirectionWebFilter implements WebFilter {
|
public class InitializeRedirectionWebFilter implements WebFilter {
|
||||||
private final URI location = URI.create("/console");
|
private final URI location = URI.create("/system/setup");
|
||||||
private final ServerWebExchangeMatcher redirectMatcher =
|
private final ServerWebExchangeMatcher redirectMatcher = new AndServerWebExchangeMatcher(
|
||||||
new PathPatternParserServerWebExchangeMatcher("/", HttpMethod.GET);
|
pathMatchers(HttpMethod.GET, "/", "/console/**", "/uc/**", "/login", "/signup"),
|
||||||
|
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML)
|
||||||
|
);
|
||||||
|
|
||||||
private final InitializationStateGetter initializationStateGetter;
|
private final InitializationStateGetter initializationStateGetter;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,274 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
|
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||||
|
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||||
|
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springdoc.core.fn.builders.content.Builder;
|
||||||
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
|
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.util.InMemoryResource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.PropertyPlaceholderHelper;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
import org.springframework.validation.BeanPropertyBindingResult;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.Unstructured;
|
||||||
|
import run.halo.app.infra.InitializationStateGetter;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.infra.SystemState;
|
||||||
|
import run.halo.app.infra.ValidationUtils;
|
||||||
|
import run.halo.app.infra.exception.RequestBodyValidationException;
|
||||||
|
import run.halo.app.infra.utils.HaloUtils;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
import run.halo.app.plugin.PluginService;
|
||||||
|
import run.halo.app.security.SuperAdminInitializer;
|
||||||
|
import run.halo.app.theme.service.ThemeService;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SystemSetupEndpoint {
|
||||||
|
static final String SETUP_TEMPLATE = "setup";
|
||||||
|
static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
|
||||||
|
new PropertyPlaceholderHelper(
|
||||||
|
PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_PREFIX,
|
||||||
|
PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_SUFFIX
|
||||||
|
);
|
||||||
|
|
||||||
|
private final InitializationStateGetter initializationStateGetter;
|
||||||
|
private final SystemConfigurableEnvironmentFetcher systemConfigFetcher;
|
||||||
|
private final SuperAdminInitializer superAdminInitializer;
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
private final PluginService pluginService;
|
||||||
|
private final ThemeService themeService;
|
||||||
|
private final Validator validator;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<ServerResponse> setupPageRouter() {
|
||||||
|
final var tag = "System";
|
||||||
|
return SpringdocRouteBuilder.route()
|
||||||
|
.GET(path("/system/setup").and(accept(MediaType.TEXT_HTML)), this::setupPage,
|
||||||
|
builder -> builder.operationId("JumpToSetupPage")
|
||||||
|
.description("Jump to setup page")
|
||||||
|
.tag(tag)
|
||||||
|
.response(responseBuilder()
|
||||||
|
.content(Builder.contentBuilder()
|
||||||
|
.mediaType(MediaType.TEXT_HTML_VALUE))
|
||||||
|
.implementation(String.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.POST("/system/setup", contentType(MediaType.APPLICATION_FORM_URLENCODED), this::setup,
|
||||||
|
builder -> builder
|
||||||
|
.operationId("SetupSystem")
|
||||||
|
.description("Setup system")
|
||||||
|
.tag(tag)
|
||||||
|
.requestBody(requestBodyBuilder()
|
||||||
|
.implementation(SetupRequest.class)
|
||||||
|
.content(Builder.contentBuilder()
|
||||||
|
.mediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.response(responseBuilder()
|
||||||
|
.responseCode(String.valueOf(HttpStatus.NO_CONTENT.value()))
|
||||||
|
.implementation(Void.class)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> setup(ServerRequest request) {
|
||||||
|
return request.formData()
|
||||||
|
.map(SetupRequest::new)
|
||||||
|
.filterWhen(body -> initializationStateGetter.userInitialized()
|
||||||
|
.map(initialized -> !initialized)
|
||||||
|
)
|
||||||
|
.flatMap(body -> {
|
||||||
|
var bindingResult = body.toBindingResult();
|
||||||
|
validator.validate(body, bindingResult);
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
return handleValidationErrors(bindingResult, request);
|
||||||
|
}
|
||||||
|
return doInitialization(body)
|
||||||
|
.then(Mono.defer(() -> handleSetupSuccessfully(request)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mono<ServerResponse> handleSetupSuccessfully(ServerRequest request) {
|
||||||
|
if (isHtmlRequest(request)) {
|
||||||
|
return redirectToConsole();
|
||||||
|
}
|
||||||
|
return ServerResponse.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> handleValidationErrors(BindingResult bindingResult,
|
||||||
|
ServerRequest request) {
|
||||||
|
if (isHtmlRequest(request)) {
|
||||||
|
return ServerResponse.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.render(SETUP_TEMPLATE, bindingResult.getModel());
|
||||||
|
}
|
||||||
|
return Mono.error(new RequestBodyValidationException(bindingResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHtmlRequest(ServerRequest request) {
|
||||||
|
return request.headers().accept().contains(MediaType.TEXT_HTML)
|
||||||
|
&& !HaloUtils.isXhr(request.headers().asHttpHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mono<ServerResponse> redirectToConsole() {
|
||||||
|
return ServerResponse.temporaryRedirect(URI.create("/console")).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> doInitialization(SetupRequest body) {
|
||||||
|
var superUserMono = superAdminInitializer.initialize(
|
||||||
|
SuperAdminInitializer.InitializationParam.builder()
|
||||||
|
.username(body.getUsername())
|
||||||
|
.password(body.getPassword())
|
||||||
|
.email(body.getEmail())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
|
||||||
|
var basicConfigMono = Mono.defer(() -> systemConfigFetcher.getConfigMap()
|
||||||
|
.flatMap(configMap -> {
|
||||||
|
mergeToBasicConfig(body, configMap);
|
||||||
|
return client.update(configMap);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||||
|
.filter(t -> t instanceof OptimisticLockingFailureException)
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.then();
|
||||||
|
return Mono.when(superUserMono, basicConfigMono,
|
||||||
|
initializeNecessaryData(body.getUsername()),
|
||||||
|
pluginService.installPresetPlugins(),
|
||||||
|
themeService.installPresetTheme()
|
||||||
|
)
|
||||||
|
.then(SystemState.upsetSystemState(client, state -> state.setIsSetup(true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> initializeNecessaryData(String username) {
|
||||||
|
return loadPresetExtensions(username)
|
||||||
|
.concatMap(client::create)
|
||||||
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mergeToBasicConfig(SetupRequest body, ConfigMap configMap) {
|
||||||
|
Map<String, String> data = configMap.getData();
|
||||||
|
if (data == null) {
|
||||||
|
data = new LinkedHashMap<>();
|
||||||
|
configMap.setData(data);
|
||||||
|
}
|
||||||
|
String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}");
|
||||||
|
var basicSetting = JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);
|
||||||
|
basicSetting.setTitle(body.getSiteTitle());
|
||||||
|
data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> setupPage(ServerRequest request) {
|
||||||
|
return initializationStateGetter.userInitialized()
|
||||||
|
.flatMap(initialized -> {
|
||||||
|
if (initialized) {
|
||||||
|
return redirectToConsole();
|
||||||
|
}
|
||||||
|
var body = new SetupRequest(new LinkedMultiValueMap<>());
|
||||||
|
var bindingResult = body.toBindingResult();
|
||||||
|
return ServerResponse.ok().render(SETUP_TEMPLATE, bindingResult.getModel());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
record SetupRequest(MultiValueMap<String, String> formData) {
|
||||||
|
|
||||||
|
@Schema(requiredMode = REQUIRED, minLength = 4, maxLength = 63)
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 4, max = 63)
|
||||||
|
@Pattern(regexp = ValidationUtils.NAME_REGEX,
|
||||||
|
message = "{validation.error.username.pattern}")
|
||||||
|
public String getUsername() {
|
||||||
|
return formData.getFirst("username");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(requiredMode = REQUIRED, minLength = 5, maxLength = 257)
|
||||||
|
@NotBlank
|
||||||
|
@Pattern(regexp = ValidationUtils.PASSWORD_REGEX,
|
||||||
|
message = "{validation.error.password.pattern}")
|
||||||
|
@Size(min = 5, max = 257)
|
||||||
|
public String getPassword() {
|
||||||
|
return formData.getFirst("password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Email
|
||||||
|
public String getEmail() {
|
||||||
|
return formData.getFirst("email");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 80)
|
||||||
|
public String getSiteTitle() {
|
||||||
|
return formData.getFirst("siteTitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
public BindingResult toBindingResult() {
|
||||||
|
return new BeanPropertyBindingResult(this, "form");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flux<Unstructured> loadPresetExtensions(String username) {
|
||||||
|
return Mono.fromCallable(
|
||||||
|
() -> {
|
||||||
|
// read initial-data.yaml to string
|
||||||
|
var classPathResource = new ClassPathResource("initial-data.yaml");
|
||||||
|
String rawContent = StreamUtils.copyToString(classPathResource.getInputStream(),
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
// build properties
|
||||||
|
var properties = new Properties();
|
||||||
|
properties.setProperty("username", username);
|
||||||
|
properties.setProperty("timestamp", Instant.now().toString());
|
||||||
|
// replace placeholders
|
||||||
|
var processedContent =
|
||||||
|
PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(rawContent, properties);
|
||||||
|
// load yaml to unstructured
|
||||||
|
var stringResource = new InMemoryResource(processedContent);
|
||||||
|
var loader = new YamlUnstructuredLoader(stringResource);
|
||||||
|
return loader.load();
|
||||||
|
})
|
||||||
|
.flatMapMany(Flux::fromIterable)
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,10 @@
|
||||||
package run.halo.app.theme;
|
package run.halo.app.theme;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
|
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.ConcurrentLruCache;
|
import org.springframework.util.ConcurrentLruCache;
|
||||||
import org.springframework.util.ResourceUtils;
|
|
||||||
import org.thymeleaf.TemplateEngine;
|
import org.thymeleaf.TemplateEngine;
|
||||||
import org.thymeleaf.dialect.IDialect;
|
import org.thymeleaf.dialect.IDialect;
|
||||||
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
|
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
|
||||||
|
@ -17,7 +14,6 @@ import org.thymeleaf.templateresolver.FileTemplateResolver;
|
||||||
import org.thymeleaf.templateresolver.ITemplateResolver;
|
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.ExternalUrlSupplier;
|
import run.halo.app.infra.ExternalUrlSupplier;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.theme.dialect.HaloProcessorDialect;
|
import run.halo.app.theme.dialect.HaloProcessorDialect;
|
||||||
import run.halo.app.theme.engine.HaloTemplateEngine;
|
import run.halo.app.theme.engine.HaloTemplateEngine;
|
||||||
|
@ -71,24 +67,9 @@ public class TemplateEngineManager {
|
||||||
|
|
||||||
public ISpringWebFluxTemplateEngine getTemplateEngine(ThemeContext theme) {
|
public ISpringWebFluxTemplateEngine getTemplateEngine(ThemeContext theme) {
|
||||||
CacheKey cacheKey = buildCacheKey(theme);
|
CacheKey cacheKey = buildCacheKey(theme);
|
||||||
// cache not exists, will create new engine
|
|
||||||
if (!engineCache.contains(cacheKey)) {
|
|
||||||
// before this, check if theme exists
|
|
||||||
if (!fileExists(theme.getPath())) {
|
|
||||||
throw new NotFoundException("Theme not found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return engineCache.get(cacheKey);
|
return engineCache.get(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean fileExists(Path path) {
|
|
||||||
try {
|
|
||||||
return ResourceUtils.getFile(path.toUri()).exists();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mono<Void> clearCache(String themeName) {
|
public Mono<Void> clearCache(String themeName) {
|
||||||
return themeResolver.getThemeContext(themeName)
|
return themeResolver.getThemeContext(themeName)
|
||||||
.doOnNext(themeContext -> {
|
.doOnNext(themeContext -> {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import run.halo.app.extension.ConfigMap;
|
||||||
|
|
||||||
public interface ThemeService {
|
public interface ThemeService {
|
||||||
|
|
||||||
|
Mono<Void> installPresetTheme();
|
||||||
|
|
||||||
Mono<Theme> install(Publisher<DataBuffer> content);
|
Mono<Theme> install(Publisher<DataBuffer> content);
|
||||||
|
|
||||||
Mono<Theme> upgrade(String themeName, Publisher<DataBuffer> content);
|
Mono<Theme> upgrade(String themeName, Publisher<DataBuffer> content);
|
||||||
|
|
|
@ -8,6 +8,8 @@ import static run.halo.app.theme.service.ThemeUtils.loadThemeManifest;
|
||||||
import static run.halo.app.theme.service.ThemeUtils.locateThemeManifest;
|
import static run.halo.app.theme.service.ThemeUtils.locateThemeManifest;
|
||||||
import static run.halo.app.theme.service.ThemeUtils.unzipThemeTo;
|
import static run.halo.app.theme.service.ThemeUtils.unzipThemeTo;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -18,11 +20,17 @@ import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.retry.RetryException;
|
import org.springframework.retry.RetryException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
import org.springframework.web.server.ServerErrorException;
|
import org.springframework.web.server.ServerErrorException;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
@ -41,6 +49,8 @@ import run.halo.app.infra.SystemVersionSupplier;
|
||||||
import run.halo.app.infra.ThemeRootGetter;
|
import run.halo.app.infra.ThemeRootGetter;
|
||||||
import run.halo.app.infra.exception.ThemeUpgradeException;
|
import run.halo.app.infra.exception.ThemeUpgradeException;
|
||||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||||
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
import run.halo.app.infra.utils.SettingUtils;
|
import run.halo.app.infra.utils.SettingUtils;
|
||||||
import run.halo.app.infra.utils.VersionUtils;
|
import run.halo.app.infra.utils.VersionUtils;
|
||||||
|
|
||||||
|
@ -53,10 +63,48 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
private final ThemeRootGetter themeRoot;
|
private final ThemeRootGetter themeRoot;
|
||||||
|
|
||||||
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
private final SystemVersionSupplier systemVersionSupplier;
|
private final SystemVersionSupplier systemVersionSupplier;
|
||||||
|
|
||||||
private final Scheduler scheduler = Schedulers.boundedElastic();
|
private final Scheduler scheduler = Schedulers.boundedElastic();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> installPresetTheme() {
|
||||||
|
var themeProps = haloProperties.getTheme();
|
||||||
|
var location = themeProps.getInitializer().getLocation();
|
||||||
|
return createThemeTempPath()
|
||||||
|
.flatMap(tempPath -> Mono.usingWhen(copyPresetThemeToPath(location, tempPath),
|
||||||
|
path -> {
|
||||||
|
var content = DataBufferUtils.read(new FileSystemResource(path),
|
||||||
|
DefaultDataBufferFactory.sharedInstance,
|
||||||
|
StreamUtils.BUFFER_SIZE);
|
||||||
|
return install(content);
|
||||||
|
}, path -> deleteRecursivelyAndSilently(tempPath, scheduler)
|
||||||
|
))
|
||||||
|
.onErrorResume(IOException.class, e -> {
|
||||||
|
log.warn("Failed to initialize theme from {}", location, e);
|
||||||
|
return Mono.empty();
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Path> copyPresetThemeToPath(String location, Path tempDir) {
|
||||||
|
return Mono.fromCallable(
|
||||||
|
() -> {
|
||||||
|
var themeUrl = ResourceUtils.getURL(location);
|
||||||
|
var resource = new UrlResource(themeUrl);
|
||||||
|
var tempThemePath = tempDir.resolve("theme.zip");
|
||||||
|
FileUtils.copyResource(resource, tempThemePath);
|
||||||
|
return tempThemePath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mono<Path> createThemeTempPath() {
|
||||||
|
return Mono.fromCallable(() -> Files.createTempDirectory("halo-theme-preset"))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Theme> install(Publisher<DataBuffer> content) {
|
public Mono<Theme> install(Publisher<DataBuffer> content) {
|
||||||
var themeRoot = this.themeRoot.get();
|
var themeRoot = this.themeRoot.get();
|
||||||
|
|
|
@ -87,3 +87,6 @@ problemDetail.comment.waitingForApproval=Comment is awaiting approval.
|
||||||
title.visibility.identification.private=(Private)
|
title.visibility.identification.private=(Private)
|
||||||
signup.error.confirm-password-not-match=The confirmation password does not match the password.
|
signup.error.confirm-password-not-match=The confirmation password does not match the password.
|
||||||
signup.error.email-code.invalid=Invalid email code.
|
signup.error.email-code.invalid=Invalid email code.
|
||||||
|
|
||||||
|
validation.error.username.pattern=The username can only be lowercase and can only contain letters, numbers, hyphens, and dots, starting and ending with characters.
|
||||||
|
validation.error.password.pattern=The password cannot contain Chinese characters and spaces.
|
|
@ -60,3 +60,6 @@ problemDetail.comment.waitingForApproval=评论审核中。
|
||||||
title.visibility.identification.private=(私有)
|
title.visibility.identification.private=(私有)
|
||||||
signup.error.confirm-password-not-match=确认密码与密码不匹配。
|
signup.error.confirm-password-not-match=确认密码与密码不匹配。
|
||||||
signup.error.email-code.invalid=邮箱验证码无效。
|
signup.error.email-code.invalid=邮箱验证码无效。
|
||||||
|
|
||||||
|
validation.error.username.pattern=用户名只能小写且只能包含字母、数字、中划线和点,以字符开头和结尾
|
||||||
|
validation.error.password.pattern=密码不能包含中文和空格
|
|
@ -23,8 +23,6 @@ rules:
|
||||||
verbs: [ "create" ]
|
verbs: [ "create" ]
|
||||||
- nonResourceURLs: [ "/actuator/globalinfo", "/actuator/health", "/actuator/health/*", "/login/public-key" ]
|
- nonResourceURLs: [ "/actuator/globalinfo", "/actuator/health", "/actuator/health/*", "/login/public-key" ]
|
||||||
verbs: [ "get" ]
|
verbs: [ "get" ]
|
||||||
- nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/system/initialize" ]
|
|
||||||
verbs: [ "create" ]
|
|
||||||
---
|
---
|
||||||
apiVersion: v1alpha1
|
apiVersion: v1alpha1
|
||||||
kind: "Role"
|
kind: "Role"
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
# 提供了 timestamp、username 变量,用于初始化数据时填充时间戳和用户名
|
||||||
|
# 初始化文章关联的分类、标签数据
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: Category
|
||||||
|
metadata:
|
||||||
|
name: 76514a40-6ef1-4ed9-b58a-e26945bde3ca
|
||||||
|
spec:
|
||||||
|
displayName: 默认分类
|
||||||
|
slug: default
|
||||||
|
description: 这是你的默认分类,如不需要,删除即可。
|
||||||
|
cover: ""
|
||||||
|
template: ""
|
||||||
|
priority: 0
|
||||||
|
children: [ ]
|
||||||
|
status:
|
||||||
|
permalink: "/categories/default"
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: Tag
|
||||||
|
metadata:
|
||||||
|
name: c33ceabb-d8f1-4711-8991-bb8f5c92ad7c
|
||||||
|
spec:
|
||||||
|
displayName: Halo
|
||||||
|
slug: halo
|
||||||
|
color: "#ffffff"
|
||||||
|
cover: ""
|
||||||
|
status:
|
||||||
|
permalink: "/tags/halo"
|
||||||
|
|
||||||
|
---
|
||||||
|
# 文章关联的内容
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: Snapshot
|
||||||
|
metadata:
|
||||||
|
name: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9
|
||||||
|
annotations:
|
||||||
|
content.halo.run/keep-raw: "true"
|
||||||
|
spec:
|
||||||
|
subjectRef:
|
||||||
|
group: content.halo.run
|
||||||
|
version: v1alpha1
|
||||||
|
kind: Post
|
||||||
|
name: 5152aea5-c2e8-4717-8bba-2263d46e19d5
|
||||||
|
rawType: HTML
|
||||||
|
rawPatch: <h2 id="hello-halo"><strong>Hello
|
||||||
|
Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow" href="https://www.halo.run/">Halo</a>
|
||||||
|
进行创作,希望能够使用愉快。</p><h2
|
||||||
|
id="%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5"><strong>相关链接</strong></h2><ul><li><p>官网:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://www.halo.run">https://www.halo.run</a></p></li><li><p>文档:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://docs.halo.run">https://docs.halo.run</a></p></li><li><p>社区:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://bbs.halo.run">https://bbs.halo.run</a></p></li><li><p>应用市场:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://www.halo.run/store/apps">https://www.halo.run/store/apps</a></p></li><li><p>开源地址:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://github.com/halo-dev/halo">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>
|
||||||
|
contentPatch: <h2 id="hello-halo"><strong>Hello
|
||||||
|
Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow" href="https://www.halo.run/">Halo</a>
|
||||||
|
进行创作,希望能够使用愉快。</p><h2
|
||||||
|
id="%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5"><strong>相关链接</strong></h2><ul><li><p>官网:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://www.halo.run">https://www.halo.run</a></p></li><li><p>文档:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://docs.halo.run">https://docs.halo.run</a></p></li><li><p>社区:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://bbs.halo.run">https://bbs.halo.run</a></p></li><li><p>应用市场:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://www.halo.run/store/apps">https://www.halo.run/store/apps</a></p></li><li><p>开源地址:<a
|
||||||
|
target="_blank" rel="noopener noreferrer nofollow"
|
||||||
|
href="https://github.com/halo-dev/halo">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>
|
||||||
|
lastModifyTime: "${timestamp}"
|
||||||
|
owner: "${username}"
|
||||||
|
contributors:
|
||||||
|
- "${username}"
|
||||||
|
|
||||||
|
---
|
||||||
|
# 初始化文章数据
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: Post
|
||||||
|
metadata:
|
||||||
|
name: 5152aea5-c2e8-4717-8bba-2263d46e19d5
|
||||||
|
spec:
|
||||||
|
title: Hello Halo
|
||||||
|
slug: hello-halo
|
||||||
|
releaseSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9
|
||||||
|
headSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9
|
||||||
|
baseSnapshot: fb5cd6bd-998d-4ccc-984d-5cc23b0a09f9
|
||||||
|
owner: "${username}"
|
||||||
|
template: ""
|
||||||
|
cover: ""
|
||||||
|
deleted: false
|
||||||
|
publish: true
|
||||||
|
publishTime: "${timestamp}"
|
||||||
|
pinned: false
|
||||||
|
allowComment: true
|
||||||
|
visible: PUBLIC
|
||||||
|
priority: 0
|
||||||
|
excerpt:
|
||||||
|
autoGenerate: false
|
||||||
|
raw: 如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 Halo 进行创作,希望能够使用愉快。
|
||||||
|
categories:
|
||||||
|
- 76514a40-6ef1-4ed9-b58a-e26945bde3ca
|
||||||
|
tags:
|
||||||
|
- c33ceabb-d8f1-4711-8991-bb8f5c92ad7c
|
||||||
|
htmlMetas: [ ]
|
||||||
|
status:
|
||||||
|
permalink: /archives/hello-halo
|
||||||
|
|
||||||
|
---
|
||||||
|
# 自定义页面关联的内容
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: Snapshot
|
||||||
|
metadata:
|
||||||
|
name: c3f73cc2-194e-4cd8-9092-7386aa50a0e5
|
||||||
|
annotations:
|
||||||
|
content.halo.run/keep-raw: "true"
|
||||||
|
spec:
|
||||||
|
subjectRef:
|
||||||
|
group: content.halo.run
|
||||||
|
version: v1alpha1
|
||||||
|
kind: SinglePage
|
||||||
|
name: 373a5f79-f44f-441a-9df1-85a4f553ece8
|
||||||
|
rawType: HTML
|
||||||
|
rawPatch: <h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code>
|
||||||
|
-> <code>自定义页面</code>
|
||||||
|
找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>
|
||||||
|
contentPatch: <h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code>
|
||||||
|
-> <code>自定义页面</code>
|
||||||
|
找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>
|
||||||
|
lastModifyTime: "${timestamp}"
|
||||||
|
owner: "${username}"
|
||||||
|
contributors:
|
||||||
|
- "${username}"
|
||||||
|
|
||||||
|
---
|
||||||
|
# 初始化自定义页面数据
|
||||||
|
apiVersion: content.halo.run/v1alpha1
|
||||||
|
kind: SinglePage
|
||||||
|
metadata:
|
||||||
|
name: 373a5f79-f44f-441a-9df1-85a4f553ece8
|
||||||
|
spec:
|
||||||
|
title: 关于
|
||||||
|
slug: about
|
||||||
|
template: ""
|
||||||
|
cover: ""
|
||||||
|
owner: "${username}"
|
||||||
|
deleted: false
|
||||||
|
publish: true
|
||||||
|
baseSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5
|
||||||
|
headSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5
|
||||||
|
releaseSnapshot: c3f73cc2-194e-4cd8-9092-7386aa50a0e5
|
||||||
|
pinned: false
|
||||||
|
allowComment: true
|
||||||
|
visible: PUBLIC
|
||||||
|
version: 1
|
||||||
|
priority: 0
|
||||||
|
excerpt:
|
||||||
|
autoGenerate: false
|
||||||
|
raw: 这是一个自定义页面,你可以在后台的 页面 -> 自定义页面 找到它,你可以用于新建关于页面、联系我们页面等等。
|
||||||
|
htmlMetas: [ ]
|
||||||
|
status:
|
||||||
|
permalink: "/about"
|
||||||
|
|
||||||
|
---
|
||||||
|
# 首页菜单项
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: MenuItem
|
||||||
|
metadata:
|
||||||
|
name: 88c3f10b-321c-4092-86a8-70db00251b74
|
||||||
|
spec:
|
||||||
|
displayName: 首页
|
||||||
|
href: /
|
||||||
|
children: [ ]
|
||||||
|
priority: 0
|
||||||
|
---
|
||||||
|
# 关联到文章作为菜单
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: MenuItem
|
||||||
|
metadata:
|
||||||
|
name: c4c814d1-0c2c-456b-8c96-4864965fee94
|
||||||
|
spec:
|
||||||
|
displayName: "Hello Halo"
|
||||||
|
href: "/archives/hello-halo"
|
||||||
|
children: [ ]
|
||||||
|
priority: 1
|
||||||
|
targetRef:
|
||||||
|
group: content.halo.run
|
||||||
|
version: v1alpha1
|
||||||
|
kind: Post
|
||||||
|
name: 5152aea5-c2e8-4717-8bba-2263d46e19d5
|
||||||
|
---
|
||||||
|
# 关联到标签作为菜单
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: MenuItem
|
||||||
|
metadata:
|
||||||
|
name: 35869bd3-33b5-448b-91ee-cf6517a59644
|
||||||
|
spec:
|
||||||
|
displayName: "Halo"
|
||||||
|
href: "/tags/halo"
|
||||||
|
children: [ ]
|
||||||
|
priority: 2
|
||||||
|
targetRef:
|
||||||
|
group: content.halo.run
|
||||||
|
version: v1alpha1
|
||||||
|
kind: Tag
|
||||||
|
name: c33ceabb-d8f1-4711-8991-bb8f5c92ad7c
|
||||||
|
---
|
||||||
|
# 关联到自定义页面作为菜单
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: MenuItem
|
||||||
|
metadata:
|
||||||
|
name: b0d041fa-dc99-48f6-a193-8604003379cf
|
||||||
|
spec:
|
||||||
|
displayName: "关于"
|
||||||
|
href: "/about"
|
||||||
|
children: [ ]
|
||||||
|
priority: 3
|
||||||
|
targetRef:
|
||||||
|
group: content.halo.run
|
||||||
|
version: v1alpha1
|
||||||
|
kind: SinglePage
|
||||||
|
name: 373a5f79-f44f-441a-9df1-85a4f553ece8
|
||||||
|
---
|
||||||
|
apiVersion: v1alpha1
|
||||||
|
kind: Menu
|
||||||
|
metadata:
|
||||||
|
name: primary
|
||||||
|
spec:
|
||||||
|
displayName: 主菜单
|
||||||
|
menuItems:
|
||||||
|
- 88c3f10b-321c-4092-86a8-70db00251b74
|
||||||
|
- c4c814d1-0c2c-456b-8c96-4864965fee94
|
||||||
|
- 35869bd3-33b5-448b-91ee-cf6517a59644
|
||||||
|
- b0d041fa-dc99-48f6-a193-8604003379cf
|
|
@ -134,7 +134,7 @@
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="password" th:text="#{form.signup.password.label}"></label>
|
<label for="password" th:text="#{form.signup.password.label}"></label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 6, enableToggle = true)}"
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
<p class="alert alert-error" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></p>
|
<p class="alert alert-error" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="confirmPassword" th:text="#{form.signup.confirmPassword.label}"></label>
|
<label for="confirmPassword" th:text="#{form.signup.confirmPassword.label}"></label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 6, enableToggle = true)}"
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
<p class="alert alert-error" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></p>
|
<p class="alert alert-error" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -254,13 +254,13 @@
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="password" th:text="#{form.passwordResetLink.password.label}">Password</label>
|
<label for="password" th:text="#{form.passwordResetLink.password.label}">Password</label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 6, enableToggle = true)}"
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label for="confirmPassword" th:text="#{form.passwordResetLink.confirmPassword.label}">Confirm Password</label>
|
<label for="confirmPassword" th:text="#{form.passwordResetLink.confirmPassword.label}">Confirm Password</label>
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 6, enableToggle = true)}"
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div th:remove="tag" th:fragment="password(id,name,required,minlength,enableToggle)">
|
<div th:remove="tag" th:fragment="password(id,name,required,minlength,maxlength,enableToggle)">
|
||||||
<div class="form-input" th:classappend="${enableToggle ? 'form-input-stack toggle-password-display-flag' : ''}">
|
<div class="form-input" th:classappend="${enableToggle ? 'form-input-stack toggle-password-display-flag' : ''}">
|
||||||
<input
|
<input
|
||||||
th:id="${id}"
|
th:id="${id}"
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
th:required="${required}"
|
th:required="${required}"
|
||||||
th:minlength="${minlength}"
|
th:minlength="${minlength}"
|
||||||
|
th:maxlength="${maxlength}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div th:if="${enableToggle}" class="form-input-stack-icon toggle-password-button">
|
<div th:if="${enableToggle}" class="form-input-stack-icon toggle-password-button">
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<th:block
|
<th:block
|
||||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = null, enableToggle = true)}"
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = null, maxlength = 257, enableToggle = true)}"
|
||||||
></th:block>
|
></th:block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,121 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html
|
||||||
|
xmlns:th="https://www.thymeleaf.org"
|
||||||
|
th:replace="~{gateway_modules/layout :: layout(title = |#{title} - Halo|, head = ~{::head}, body = ~{::body})}"
|
||||||
|
>
|
||||||
|
<th:block th:fragment="head">
|
||||||
|
<style>
|
||||||
|
.setup-page-wrapper {
|
||||||
|
max-width: 35em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:fragment="body">
|
||||||
|
<div class="gateway-wrapper setup-page-wrapper">
|
||||||
|
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||||
|
|
||||||
|
<div class="halo-form-wrapper">
|
||||||
|
<h1 class="form-title" th:text="#{title}"></h1>
|
||||||
|
|
||||||
|
<form th:object="${form}" th:action="@{/system/setup}" class="halo-form" method="post">
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="siteTitle" th:text="#{form.siteTitle.label}"></label>
|
||||||
|
<div class="form-input">
|
||||||
|
<input
|
||||||
|
name="siteTitle"
|
||||||
|
id="siteTitle"
|
||||||
|
type="text"
|
||||||
|
th:field="*{siteTitle}"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
maxlength="80"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('siteTitle')}" th:errors="*{siteTitle}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="username" th:text="#{form.username.label}"></label>
|
||||||
|
<div class="form-input">
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
th:field="*{username}"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
maxlength="63"
|
||||||
|
minlength="4"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="email" th:text="#{form.email.label}"></label>
|
||||||
|
<div class="form-input">
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
th:field="*{email}"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="password" th:text="#{form.password.label}"></label>
|
||||||
|
<th:block
|
||||||
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
|
></th:block>
|
||||||
|
<p class="alert alert-error" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label for="confirmPassword" th:text="#{form.confirmPassword.label}"></label>
|
||||||
|
<th:block
|
||||||
|
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = null, required = 'true', minlength = 5, maxlength = 257, enableToggle = true)}"
|
||||||
|
></th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<button type="submit" th:text="#{form.submit}">初始化</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div th:replace="~{gateway_modules/common_fragments::languageSwitcher}"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
var password = document.getElementById("password"),
|
||||||
|
confirm_password = document.getElementById("confirmPassword");
|
||||||
|
|
||||||
|
function validatePassword() {
|
||||||
|
if (password.value != confirm_password.value) {
|
||||||
|
confirm_password.setCustomValidity("Passwords Don't Match");
|
||||||
|
} else {
|
||||||
|
confirm_password.setCustomValidity("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
password.onchange = validatePassword;
|
||||||
|
confirm_password.onkeyup = validatePassword;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</html>
|
|
@ -0,0 +1,7 @@
|
||||||
|
title=系统初始化
|
||||||
|
form.siteTitle.label=站点标题
|
||||||
|
form.username.label=用户名
|
||||||
|
form.email.label=电子邮箱
|
||||||
|
form.password.label=密码
|
||||||
|
form.confirmPassword.label=确认密码
|
||||||
|
form.submit=初始化
|
|
@ -0,0 +1,7 @@
|
||||||
|
title=Setup
|
||||||
|
form.siteTitle.label=Site title
|
||||||
|
form.username.label=Username
|
||||||
|
form.email.label=Email
|
||||||
|
form.password.label=Password
|
||||||
|
form.confirmPassword.label=Confirm Password
|
||||||
|
form.submit=Setup
|
|
@ -0,0 +1,7 @@
|
||||||
|
title=Configuración
|
||||||
|
form.siteTitle.label=Título del Sitio
|
||||||
|
form.username.label=Nombre de Usuario
|
||||||
|
form.email.label=Correo Electrónico
|
||||||
|
form.password.label=Contraseña
|
||||||
|
form.confirmPassword.label=Confirmar Contraseña
|
||||||
|
form.submit=Configurar
|
|
@ -0,0 +1,7 @@
|
||||||
|
title=系統初始化
|
||||||
|
form.siteTitle.label=站點標題
|
||||||
|
form.username.label=使用者名稱
|
||||||
|
form.email.label=電子郵件
|
||||||
|
form.password.label=密碼
|
||||||
|
form.confirmPassword.label=確認密碼
|
||||||
|
form.submit=初始化
|
|
@ -1,89 +0,0 @@
|
||||||
package run.halo.app.core.endpoint.console;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import run.halo.app.core.endpoint.console.SystemInitializationEndpoint.SystemInitializationRequest;
|
|
||||||
import run.halo.app.extension.ConfigMap;
|
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
|
||||||
import run.halo.app.infra.InitializationStateGetter;
|
|
||||||
import run.halo.app.infra.SystemSetting;
|
|
||||||
import run.halo.app.security.SuperAdminInitializer;
|
|
||||||
import run.halo.app.security.SuperAdminInitializer.InitializationParam;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link SystemInitializationEndpoint}.
|
|
||||||
*
|
|
||||||
* @author guqing
|
|
||||||
* @since 2.9.0
|
|
||||||
*/
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class SystemInitializationEndpointTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
InitializationStateGetter initializationStateGetter;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
SuperAdminInitializer superAdminInitializer;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
ReactiveExtensionClient client;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
SystemInitializationEndpoint initializationEndpoint;
|
|
||||||
|
|
||||||
WebTestClient webTestClient;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
webTestClient = bindToRouterFunction(initializationEndpoint.endpoint()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void initializeWithoutRequestBody() {
|
|
||||||
webTestClient.post()
|
|
||||||
.uri("/system/initialize")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isBadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void initializeWithRequestBody() {
|
|
||||||
var initialization = new SystemInitializationRequest();
|
|
||||||
initialization.setUsername("faker");
|
|
||||||
initialization.setPassword("openfaker");
|
|
||||||
initialization.setEmail("faker@halo.run");
|
|
||||||
initialization.setSiteTitle("Fake Site");
|
|
||||||
|
|
||||||
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(false));
|
|
||||||
when(superAdminInitializer.initialize(any(InitializationParam.class)))
|
|
||||||
.thenReturn(Mono.empty());
|
|
||||||
|
|
||||||
var configMap = new ConfigMap();
|
|
||||||
when(client.get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG))
|
|
||||||
.thenReturn(Mono.just(configMap));
|
|
||||||
when(client.update(configMap)).thenReturn(Mono.just(configMap));
|
|
||||||
|
|
||||||
webTestClient.post().uri("/system/initialize")
|
|
||||||
.bodyValue(initialization)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isCreated()
|
|
||||||
.expectHeader().location("/console");
|
|
||||||
|
|
||||||
verify(initializationStateGetter).userInitialized();
|
|
||||||
verify(superAdminInitializer).initialize(any());
|
|
||||||
verify(client).get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);
|
|
||||||
verify(client).update(configMap);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -77,33 +77,6 @@ class PluginServiceImplTest {
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
PluginServiceImpl pluginService;
|
PluginServiceImpl pluginService;
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPresetsTest() {
|
|
||||||
var presets = pluginService.getPresets();
|
|
||||||
StepVerifier.create(presets)
|
|
||||||
.assertNext(plugin -> {
|
|
||||||
assertEquals("fake-plugin", plugin.getMetadata().getName());
|
|
||||||
assertEquals("0.0.2", plugin.getSpec().getVersion());
|
|
||||||
assertEquals(Plugin.Phase.PENDING, plugin.getStatus().getPhase());
|
|
||||||
})
|
|
||||||
.verifyComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPresetIfNotFound() {
|
|
||||||
var plugin = pluginService.getPreset("not-found-plugin");
|
|
||||||
StepVerifier.create(plugin)
|
|
||||||
.verifyComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPresetIfFound() {
|
|
||||||
var plugin = pluginService.getPreset("fake-plugin");
|
|
||||||
StepVerifier.create(plugin)
|
|
||||||
.expectNextCount(1)
|
|
||||||
.verifyComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class InstallUpdateReloadTest {
|
class InstallUpdateReloadTest {
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package run.halo.app.security;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -14,6 +15,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||||
|
@ -50,8 +52,10 @@ class InitializeRedirectionWebFilterTest {
|
||||||
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(false));
|
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(false));
|
||||||
|
|
||||||
WebFilterChain chain = mock(WebFilterChain.class);
|
WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
var paths = new String[] {"/", "/console/test", "/uc/test", "/login", "/signup"};
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
for (String path : paths) {
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get(path)
|
||||||
|
.accept(MediaType.TEXT_HTML).build();
|
||||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then());
|
when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then());
|
||||||
|
@ -63,17 +67,22 @@ class InitializeRedirectionWebFilterTest {
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
|
|
||||||
verify(serverRedirectStrategy).sendRedirect(eq(exchange), eq(URI.create("/console")));
|
verify(serverRedirectStrategy).sendRedirect(eq(exchange),
|
||||||
|
eq(URI.create("/system/setup")));
|
||||||
verify(chain, never()).filter(eq(exchange));
|
verify(chain, never()).filter(eq(exchange));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotRedirectWhenSystemInitialized() {
|
void shouldNotRedirectWhenSystemInitialized() {
|
||||||
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));
|
lenient().when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));
|
||||||
|
|
||||||
WebFilterChain chain = mock(WebFilterChain.class);
|
WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
var paths = new String[] {"/test", "/apis/test", "system/setup", "/logout"};
|
||||||
|
for (String path : paths) {
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get(path)
|
||||||
|
.accept(MediaType.TEXT_HTML).build();
|
||||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||||
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
||||||
Mono<Void> result = filter.filter(exchange, chain);
|
Mono<Void> result = filter.filter(exchange, chain);
|
||||||
|
@ -83,16 +92,17 @@ class InitializeRedirectionWebFilterTest {
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
|
|
||||||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
|
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), any());
|
||||||
eq(URI.create("/console")));
|
|
||||||
verify(chain).filter(eq(exchange));
|
verify(chain).filter(eq(exchange));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotRedirectWhenNotHomePage() {
|
void shouldNotRedirectTest() {
|
||||||
WebFilterChain chain = mock(WebFilterChain.class);
|
WebFilterChain chain = mock(WebFilterChain.class);
|
||||||
|
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test")
|
||||||
|
.accept(MediaType.TEXT_HTML).build();
|
||||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||||
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
||||||
Mono<Void> result = filter.filter(exchange, chain);
|
Mono<Void> result = filter.filter(exchange, chain);
|
||||||
|
@ -102,8 +112,7 @@ class InitializeRedirectionWebFilterTest {
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
|
|
||||||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
|
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange), any());
|
||||||
eq(URI.create("/console")));
|
|
||||||
verify(chain).filter(eq(exchange));
|
verify(chain).filter(eq(exchange));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package run.halo.app.security.preauth;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link SystemSetupEndpoint}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
class SystemSetupEndpointTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void placeholderTest() {
|
||||||
|
var properties = new Properties();
|
||||||
|
properties.setProperty("username", "guqing");
|
||||||
|
properties.setProperty("timestamp", "2024-09-30");
|
||||||
|
var str = SystemSetupEndpoint.PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders("""
|
||||||
|
${username}
|
||||||
|
${timestamp}
|
||||||
|
""", properties);
|
||||||
|
assertThat(str).isEqualTo("""
|
||||||
|
guqing
|
||||||
|
2024-09-30
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,22 +8,18 @@ param:
|
||||||
notificationName: "{{randAlpha 6}}"
|
notificationName: "{{randAlpha 6}}"
|
||||||
auth: "Basic YWRtaW46MTIzNDU2"
|
auth: "Basic YWRtaW46MTIzNDU2"
|
||||||
items:
|
items:
|
||||||
- name: init
|
- name: setup
|
||||||
request:
|
request:
|
||||||
api: /api.console.halo.run/v1alpha1/system/initialize
|
api: |
|
||||||
|
{{default "http://halo:8090" (env "SERVER")}}/system/setup
|
||||||
method: POST
|
method: POST
|
||||||
header:
|
header:
|
||||||
Content-Type: application/json
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Accept: application/json
|
||||||
body: |
|
body: |
|
||||||
{
|
siteTitle=testing&username={{.param.userName}}&password=123456&email=testing@halo.run
|
||||||
"siteTitle": "testing",
|
|
||||||
"username": "admin",
|
|
||||||
"password": "123456",
|
|
||||||
"email": "testing@halo.com",
|
|
||||||
"password_confirm": "123456"
|
|
||||||
}
|
|
||||||
expect:
|
expect:
|
||||||
statusCode: 201
|
statusCode: 204
|
||||||
- name: createPost
|
- name: createPost
|
||||||
request:
|
request:
|
||||||
api: /api.console.halo.run/v1alpha1/posts
|
api: /api.console.halo.run/v1alpha1/posts
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
|
||||||
import { useUserStore } from "@/stores/user";
|
|
||||||
import type { Router } from "vue-router";
|
|
||||||
|
|
||||||
export function setupCheckStatesGuard(router: Router) {
|
|
||||||
router.beforeEach(async (to, _, next) => {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const { globalInfo } = useGlobalInfoStore();
|
|
||||||
const { userInitialized, dataInitialized } = globalInfo || {};
|
|
||||||
|
|
||||||
if (to.name === "Setup" && userInitialized) {
|
|
||||||
next({ name: "Dashboard" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to.name === "SetupInitialData" && dataInitialized) {
|
|
||||||
next({ name: "Dashboard" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInitialized === false && to.name !== "Setup") {
|
|
||||||
next({ name: "Setup" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
dataInitialized === false &&
|
|
||||||
!userStore.isAnonymous &&
|
|
||||||
to.name !== "SetupInitialData"
|
|
||||||
) {
|
|
||||||
next({ name: "SetupInitialData" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
type RouteLocationNormalizedLoaded,
|
type RouteLocationNormalizedLoaded,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import { setupAuthCheckGuard } from "./guards/auth-check";
|
import { setupAuthCheckGuard } from "./guards/auth-check";
|
||||||
import { setupCheckStatesGuard } from "./guards/check-states";
|
|
||||||
import { setupPermissionGuard } from "./guards/permission";
|
import { setupPermissionGuard } from "./guards/permission";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -22,7 +21,6 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setupCheckStatesGuard(router);
|
|
||||||
setupAuthCheckGuard(router);
|
setupAuthCheckGuard(router);
|
||||||
setupPermissionGuard(router);
|
setupPermissionGuard(router);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||||
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
||||||
import Setup from "@console/views/system/Setup.vue";
|
|
||||||
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
export const routes: Array<RouteRecordRaw> = [
|
export const routes: Array<RouteRecordRaw> = [
|
||||||
|
@ -22,22 +20,6 @@ export const routes: Array<RouteRecordRaw> = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/setup",
|
|
||||||
component: Setup,
|
|
||||||
name: "Setup",
|
|
||||||
meta: {
|
|
||||||
title: "core.setup.title",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/setup-initial-data",
|
|
||||||
name: "SetupInitialData",
|
|
||||||
component: SetupInitialData,
|
|
||||||
meta: {
|
|
||||||
title: "core.setup.title",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import LocaleChange from "@/components/common/LocaleChange.vue";
|
|
||||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
|
||||||
import type { SystemInitializationRequest } from "@halo-dev/api-client";
|
|
||||||
import { consoleApiClient } from "@halo-dev/api-client";
|
|
||||||
import { Toast, VButton } from "@halo-dev/components";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const formState = ref<SystemInitializationRequest>({
|
|
||||||
siteTitle: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
email: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
await consoleApiClient.system.initialize({
|
|
||||||
systemInitializationRequest: formState.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const globalInfoStore = useGlobalInfoStore();
|
|
||||||
await globalInfoStore.fetchGlobalInfo();
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
|
|
||||||
router.push({ name: "Login" });
|
|
||||||
|
|
||||||
Toast.success(t("core.setup.operations.submit.toast_success"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputClasses = {
|
|
||||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex h-screen flex-col items-center overflow-auto bg-white/90 pt-[20vh]"
|
|
||||||
>
|
|
||||||
<IconLogo class="mb-8 flex-none" />
|
|
||||||
|
|
||||||
<div class="flex w-72 flex-col">
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<FormKit
|
|
||||||
id="setup-form"
|
|
||||||
v-model="formState"
|
|
||||||
name="setup-form"
|
|
||||||
:actions="false"
|
|
||||||
:classes="{
|
|
||||||
form: '!divide-none',
|
|
||||||
}"
|
|
||||||
:config="{ validationVisibility: 'submit' }"
|
|
||||||
type="form"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@keyup.enter="$formkit.submit('setup-form')"
|
|
||||||
>
|
|
||||||
<FormKit
|
|
||||||
name="siteTitle"
|
|
||||||
:classes="inputClasses"
|
|
||||||
:autofocus="true"
|
|
||||||
type="text"
|
|
||||||
:validation-label="$t('core.setup.fields.site_title.label')"
|
|
||||||
:placeholder="$t('core.setup.fields.site_title.label')"
|
|
||||||
validation="required:trim|length:0,100"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
name="email"
|
|
||||||
:classes="inputClasses"
|
|
||||||
type="text"
|
|
||||||
:validation-label="$t('core.setup.fields.email.label')"
|
|
||||||
:placeholder="$t('core.setup.fields.email.label')"
|
|
||||||
validation="required|email|length:0,100"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
name="username"
|
|
||||||
:classes="inputClasses"
|
|
||||||
type="text"
|
|
||||||
:validation-label="$t('core.setup.fields.username.label')"
|
|
||||||
:placeholder="$t('core.setup.fields.username.label')"
|
|
||||||
:validation="[
|
|
||||||
['required'],
|
|
||||||
['length:0,63'],
|
|
||||||
[
|
|
||||||
'matches',
|
|
||||||
/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/,
|
|
||||||
],
|
|
||||||
]"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
name="password"
|
|
||||||
:classes="inputClasses"
|
|
||||||
type="password"
|
|
||||||
:validation-label="$t('core.setup.fields.password.label')"
|
|
||||||
:placeholder="$t('core.setup.fields.password.label')"
|
|
||||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
|
||||||
autocomplete="current-password"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
name="password_confirm"
|
|
||||||
:classes="inputClasses"
|
|
||||||
type="password"
|
|
||||||
:validation-label="$t('core.setup.fields.confirm_password.label')"
|
|
||||||
:placeholder="$t('core.setup.fields.confirm_password.label')"
|
|
||||||
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
|
|
||||||
autocomplete="current-password"
|
|
||||||
></FormKit>
|
|
||||||
</FormKit>
|
|
||||||
<VButton
|
|
||||||
block
|
|
||||||
class="mt-8"
|
|
||||||
type="secondary"
|
|
||||||
:loading="loading"
|
|
||||||
@click="$formkit.submit('setup-form')"
|
|
||||||
>
|
|
||||||
{{ $t("core.setup.operations.submit.button") }}
|
|
||||||
</VButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5"
|
|
||||||
>
|
|
||||||
<LocaleChange />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,202 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import H2WarningAlert from "@/components/alerts/H2WarningAlert.vue";
|
|
||||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
|
||||||
import { useUserStore } from "@/stores/user";
|
|
||||||
import type { Info } from "@/types";
|
|
||||||
import {
|
|
||||||
consoleApiClient,
|
|
||||||
coreApiClient,
|
|
||||||
type Category,
|
|
||||||
type Plugin,
|
|
||||||
type PostRequest,
|
|
||||||
type SinglePageRequest,
|
|
||||||
type Tag,
|
|
||||||
} from "@halo-dev/api-client";
|
|
||||||
import { VButton, VLoading } from "@halo-dev/components";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
|
||||||
import axios from "axios";
|
|
||||||
import { onMounted, ref, watch } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import category from "./setup-data/category.json";
|
|
||||||
import menuItems from "./setup-data/menu-items.json";
|
|
||||||
import menu from "./setup-data/menu.json";
|
|
||||||
import post from "./setup-data/post.json";
|
|
||||||
import singlePage from "./setup-data/singlePage.json";
|
|
||||||
import tag from "./setup-data/tag.json";
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const globalInfoStore = useGlobalInfoStore();
|
|
||||||
|
|
||||||
const { mutateAsync: pluginInstallMutate } = useMutation({
|
|
||||||
mutationKey: ["plugin-install"],
|
|
||||||
mutationFn: async (plugin: Plugin) => {
|
|
||||||
const { data } = await consoleApiClient.plugin.plugin.installPlugin(
|
|
||||||
{
|
|
||||||
source: "PRESET",
|
|
||||||
presetName: plugin.metadata.name as string,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mute: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
retry: 3,
|
|
||||||
retryDelay: 1000,
|
|
||||||
async onSuccess(data) {
|
|
||||||
await pluginStartMutate(data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: pluginStartMutate } = useMutation({
|
|
||||||
mutationKey: ["plugin-start"],
|
|
||||||
mutationFn: async (plugin: Plugin) => {
|
|
||||||
return await consoleApiClient.plugin.plugin.changePluginRunningState(
|
|
||||||
{
|
|
||||||
name: plugin.metadata.name,
|
|
||||||
pluginRunningStateRequest: {
|
|
||||||
enable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ mute: true }
|
|
||||||
);
|
|
||||||
},
|
|
||||||
retry: 3,
|
|
||||||
retryDelay: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const processing = ref(false);
|
|
||||||
|
|
||||||
async function setupInitialData() {
|
|
||||||
try {
|
|
||||||
processing.value = true;
|
|
||||||
|
|
||||||
// Create category / tag / post
|
|
||||||
await coreApiClient.content.category.createCategory({
|
|
||||||
category: category as Category,
|
|
||||||
});
|
|
||||||
await coreApiClient.content.tag.createTag({
|
|
||||||
tag: tag as Tag,
|
|
||||||
});
|
|
||||||
const { data: postData } = await consoleApiClient.content.post.draftPost({
|
|
||||||
postRequest: post as PostRequest,
|
|
||||||
});
|
|
||||||
await consoleApiClient.content.post.publishPost({
|
|
||||||
name: postData.metadata.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create singlePage
|
|
||||||
const { data: singlePageData } =
|
|
||||||
await consoleApiClient.content.singlePage.draftSinglePage({
|
|
||||||
singlePageRequest: singlePage as SinglePageRequest,
|
|
||||||
});
|
|
||||||
|
|
||||||
await consoleApiClient.content.singlePage.publishSinglePage({
|
|
||||||
name: singlePageData.metadata.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create menu and menu items
|
|
||||||
const menuItemPromises = menuItems.map((item) => {
|
|
||||||
return coreApiClient.menuItem.createMenuItem({
|
|
||||||
menuItem: item,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await Promise.all(menuItemPromises);
|
|
||||||
await coreApiClient.menu.createMenu({ menu: menu });
|
|
||||||
|
|
||||||
// Install preset plugins
|
|
||||||
const { data: presetPlugins } =
|
|
||||||
await consoleApiClient.plugin.plugin.listPluginPresets();
|
|
||||||
|
|
||||||
for (let i = 0; i < presetPlugins.length; i++) {
|
|
||||||
const presetPlugin = presetPlugins[i];
|
|
||||||
try {
|
|
||||||
await pluginInstallMutate(presetPlugin);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to install plugin: ", presetPlugin.metadata.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
await coreApiClient.configMap.createConfigMap({
|
|
||||||
configMap: {
|
|
||||||
metadata: {
|
|
||||||
name: "system-states",
|
|
||||||
},
|
|
||||||
kind: "ConfigMap",
|
|
||||||
apiVersion: "v1alpha1",
|
|
||||||
data: {
|
|
||||||
states: JSON.stringify({ isSetup: true }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload page to fetch plugin's bundle files
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const { data: info } = useQuery<Info>({
|
|
||||||
queryKey: ["system-info"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await axios.get<Info>(`/actuator/info`, {
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
retry: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => info.value,
|
|
||||||
(value) => {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!value.database.name.startsWith("H2")) {
|
|
||||||
setupInitialData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await globalInfoStore.fetchGlobalInfo();
|
|
||||||
|
|
||||||
if (userStore.isAnonymous) {
|
|
||||||
window.location.href = "/";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalInfoStore.globalInfo?.dataInitialized) {
|
|
||||||
router.push({ name: "Dashboard" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex h-screen flex-col items-center justify-center">
|
|
||||||
<template v-if="info?.database.name.startsWith('H2') && !processing">
|
|
||||||
<H2WarningAlert class="max-w-md">
|
|
||||||
<template #actions>
|
|
||||||
<VButton type="secondary" size="sm" @click="setupInitialData()">
|
|
||||||
{{ $t("core.common.buttons.continue") }}
|
|
||||||
</VButton>
|
|
||||||
</template>
|
|
||||||
</H2WarningAlert>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="processing">
|
|
||||||
<VLoading />
|
|
||||||
<div class="text-xs text-gray-600">
|
|
||||||
{{ $t("core.setup.operations.setup_initial_data.loading") }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "默认分类",
|
|
||||||
"slug": "default",
|
|
||||||
"description": "这是你的默认分类,如不需要,删除即可。",
|
|
||||||
"cover": "",
|
|
||||||
"template": "",
|
|
||||||
"priority": 0,
|
|
||||||
"children": []
|
|
||||||
},
|
|
||||||
"apiVersion": "content.halo.run/v1alpha1",
|
|
||||||
"kind": "Category",
|
|
||||||
"metadata": {
|
|
||||||
"name": "76514a40-6ef1-4ed9-b58a-e26945bde3ca"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "首页",
|
|
||||||
"href": "/",
|
|
||||||
"children": [],
|
|
||||||
"priority": 0
|
|
||||||
},
|
|
||||||
"apiVersion": "v1alpha1",
|
|
||||||
"kind": "MenuItem",
|
|
||||||
"metadata": { "name": "88c3f10b-321c-4092-86a8-70db00251b74" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"children": [],
|
|
||||||
"priority": 1,
|
|
||||||
"targetRef": {
|
|
||||||
"group": "content.halo.run",
|
|
||||||
"version": "v1alpha1",
|
|
||||||
"kind": "Post",
|
|
||||||
"name": "5152aea5-c2e8-4717-8bba-2263d46e19d5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiVersion": "v1alpha1",
|
|
||||||
"kind": "MenuItem",
|
|
||||||
"metadata": { "name": "c4c814d1-0c2c-456b-8c96-4864965fee94" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "",
|
|
||||||
"href": "",
|
|
||||||
"children": [],
|
|
||||||
"priority": 2,
|
|
||||||
"targetRef": {
|
|
||||||
"group": "content.halo.run",
|
|
||||||
"version": "v1alpha1",
|
|
||||||
"kind": "Tag",
|
|
||||||
"name": "c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiVersion": "v1alpha1",
|
|
||||||
"kind": "MenuItem",
|
|
||||||
"metadata": { "name": "35869bd3-33b5-448b-91ee-cf6517a59644" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "",
|
|
||||||
"href": "",
|
|
||||||
"children": [],
|
|
||||||
"priority": 3,
|
|
||||||
"targetRef": {
|
|
||||||
"group": "content.halo.run",
|
|
||||||
"version": "v1alpha1",
|
|
||||||
"kind": "SinglePage",
|
|
||||||
"name": "373a5f79-f44f-441a-9df1-85a4f553ece8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiVersion": "v1alpha1",
|
|
||||||
"kind": "MenuItem",
|
|
||||||
"metadata": { "name": "b0d041fa-dc99-48f6-a193-8604003379cf" }
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "主菜单",
|
|
||||||
"menuItems": [
|
|
||||||
"88c3f10b-321c-4092-86a8-70db00251b74",
|
|
||||||
"c4c814d1-0c2c-456b-8c96-4864965fee94",
|
|
||||||
"35869bd3-33b5-448b-91ee-cf6517a59644",
|
|
||||||
"b0d041fa-dc99-48f6-a193-8604003379cf"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"apiVersion": "v1alpha1",
|
|
||||||
"kind": "Menu",
|
|
||||||
"metadata": { "name": "primary" }
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"post": {
|
|
||||||
"spec": {
|
|
||||||
"title": "Hello Halo",
|
|
||||||
"slug": "hello-halo",
|
|
||||||
"template": "",
|
|
||||||
"cover": "",
|
|
||||||
"deleted": false,
|
|
||||||
"publish": false,
|
|
||||||
"publishTime": "",
|
|
||||||
"pinned": false,
|
|
||||||
"allowComment": true,
|
|
||||||
"visible": "PUBLIC",
|
|
||||||
"version": 1,
|
|
||||||
"priority": 0,
|
|
||||||
"excerpt": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"raw": "如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 Halo 进行创作,希望能够使用愉快。"
|
|
||||||
},
|
|
||||||
"categories": ["76514a40-6ef1-4ed9-b58a-e26945bde3ca"],
|
|
||||||
"tags": ["c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"],
|
|
||||||
"htmlMetas": []
|
|
||||||
},
|
|
||||||
"apiVersion": "content.halo.run/v1alpha1",
|
|
||||||
"kind": "Post",
|
|
||||||
"metadata": {
|
|
||||||
"name": "5152aea5-c2e8-4717-8bba-2263d46e19d5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"raw": "<h2 id=\"hello-halo\"><strong>Hello Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/\">Halo</a> 进行创作,希望能够使用愉快。</p><h2 id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run\">https://www.halo.run</a></p></li><li><p>文档:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>应用市场:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/store/apps\">https://www.halo.run/store/apps</a></p></li><li><p>开源地址:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>",
|
|
||||||
"content": "<h2 id=\"hello-halo\"><strong>Hello Halo</strong></h2><p>如果你看到了这一篇文章,那么证明你已经安装成功了,感谢使用 <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/\">Halo</a> 进行创作,希望能够使用愉快。</p><h2 id=\"%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5\"><strong>相关链接</strong></h2><ul><li><p>官网:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run\">https://www.halo.run</a></p></li><li><p>文档:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://docs.halo.run\">https://docs.halo.run</a></p></li><li><p>社区:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://bbs.halo.run\">https://bbs.halo.run</a></p></li><li><p>应用市场:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://www.halo.run/store/apps\">https://www.halo.run/store/apps</a></p></li><li><p>开源地址:<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://github.com/halo-dev/halo\">https://github.com/halo-dev/halo</a></p></li></ul><p>在使用过程中,有任何问题都可以通过以上链接找寻答案,或者联系我们。</p><blockquote><p>这是一篇自动生成的文章,请删除这篇文章之后开始你的创作吧!</p></blockquote>",
|
|
||||||
"rawType": "HTML"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"page": {
|
|
||||||
"spec": {
|
|
||||||
"title": "关于",
|
|
||||||
"slug": "about",
|
|
||||||
"template": "",
|
|
||||||
"cover": "",
|
|
||||||
"deleted": false,
|
|
||||||
"publish": false,
|
|
||||||
"publishTime": "",
|
|
||||||
"pinned": false,
|
|
||||||
"allowComment": true,
|
|
||||||
"visible": "PUBLIC",
|
|
||||||
"version": 1,
|
|
||||||
"priority": 0,
|
|
||||||
"excerpt": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"raw": "这是一个自定义页面,你可以在后台的 页面 -> 自定义页面 找到它,你可以用于新建关于页面、联系我们页面等等。"
|
|
||||||
},
|
|
||||||
"htmlMetas": []
|
|
||||||
},
|
|
||||||
"apiVersion": "content.halo.run/v1alpha1",
|
|
||||||
"kind": "SinglePage",
|
|
||||||
"metadata": { "name": "373a5f79-f44f-441a-9df1-85a4f553ece8" }
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"raw": "<h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code> -> <code>自定义页面</code> 找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>",
|
|
||||||
"content": "<h2><strong>关于页面</strong></h2><p>这是一个自定义页面,你可以在后台的 <code>页面</code> -> <code>自定义页面</code> 找到它,你可以用于新建关于页面、联系我们页面等等。</p><blockquote><p>这是一篇自动生成的页面,你可以在后台删除它。</p></blockquote>",
|
|
||||||
"rawType": "HTML"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"spec": {
|
|
||||||
"displayName": "Halo",
|
|
||||||
"slug": "halo",
|
|
||||||
"color": "#ffffff",
|
|
||||||
"cover": ""
|
|
||||||
},
|
|
||||||
"apiVersion": "content.halo.run/v1alpha1",
|
|
||||||
"kind": "Tag",
|
|
||||||
"metadata": {
|
|
||||||
"name": "c33ceabb-d8f1-4711-8991-bb8f5c92ad7c"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -76,6 +76,7 @@ api/thumbnail-v1alpha1-api.ts
|
||||||
api/thumbnail-v1alpha1-public-api.ts
|
api/thumbnail-v1alpha1-public-api.ts
|
||||||
api/two-factor-auth-v1alpha1-uc-api.ts
|
api/two-factor-auth-v1alpha1-uc-api.ts
|
||||||
api/user-connection-v1alpha1-api.ts
|
api/user-connection-v1alpha1-api.ts
|
||||||
|
api/user-connection-v1alpha1-uc-api.ts
|
||||||
api/user-v1alpha1-api.ts
|
api/user-v1alpha1-api.ts
|
||||||
api/user-v1alpha1-console-api.ts
|
api/user-v1alpha1-console-api.ts
|
||||||
base.ts
|
base.ts
|
||||||
|
@ -303,7 +304,6 @@ models/subscription-list.ts
|
||||||
models/subscription-spec.ts
|
models/subscription-spec.ts
|
||||||
models/subscription-subscriber.ts
|
models/subscription-subscriber.ts
|
||||||
models/subscription.ts
|
models/subscription.ts
|
||||||
models/system-initialization-request.ts
|
|
||||||
models/tag-list.ts
|
models/tag-list.ts
|
||||||
models/tag-spec.ts
|
models/tag-spec.ts
|
||||||
models/tag-status.ts
|
models/tag-status.ts
|
||||||
|
|
|
@ -88,6 +88,7 @@ export * from './api/thumbnail-v1alpha1-api';
|
||||||
export * from './api/thumbnail-v1alpha1-public-api';
|
export * from './api/thumbnail-v1alpha1-public-api';
|
||||||
export * from './api/two-factor-auth-v1alpha1-uc-api';
|
export * from './api/two-factor-auth-v1alpha1-uc-api';
|
||||||
export * from './api/user-connection-v1alpha1-api';
|
export * from './api/user-connection-v1alpha1-api';
|
||||||
|
export * from './api/user-connection-v1alpha1-uc-api';
|
||||||
export * from './api/user-v1alpha1-api';
|
export * from './api/user-v1alpha1-api';
|
||||||
export * from './api/user-v1alpha1-console-api';
|
export * from './api/user-v1alpha1-console-api';
|
||||||
|
|
||||||
|
|
|
@ -384,43 +384,6 @@ export const PluginV1alpha1ConsoleApiAxiosParamCreator = function (configuration
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* List all plugin presets in the system.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
listPluginPresets: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/plugin-presets`;
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
|
||||||
let baseOptions;
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
|
||||||
const localVarHeaderParameter = {} as any;
|
|
||||||
const localVarQueryParameter = {} as any;
|
|
||||||
|
|
||||||
// authentication basicAuth required
|
|
||||||
// http basic authentication required
|
|
||||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
|
||||||
|
|
||||||
// authentication bearerAuth required
|
|
||||||
// http bearer authentication required
|
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* List plugins using query criteria and sort params
|
* List plugins using query criteria and sort params
|
||||||
* @param {number} [page] Page number. Default is 0.
|
* @param {number} [page] Page number. Default is 0.
|
||||||
|
@ -884,17 +847,6 @@ export const PluginV1alpha1ConsoleApiFp = function(configuration?: Configuration
|
||||||
const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.installPluginFromUri']?.[localVarOperationServerIndex]?.url;
|
const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.installPluginFromUri']?.[localVarOperationServerIndex]?.url;
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* List all plugin presets in the system.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async listPluginPresets(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Plugin>>> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listPluginPresets(options);
|
|
||||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
|
||||||
const localVarOperationServerBasePath = operationServerMap['PluginV1alpha1ConsoleApi.listPluginPresets']?.[localVarOperationServerIndex]?.url;
|
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* List plugins using query criteria and sort params
|
* List plugins using query criteria and sort params
|
||||||
* @param {number} [page] Page number. Default is 0.
|
* @param {number} [page] Page number. Default is 0.
|
||||||
|
@ -1072,14 +1024,6 @@ export const PluginV1alpha1ConsoleApiFactory = function (configuration?: Configu
|
||||||
installPluginFromUri(requestParameters: PluginV1alpha1ConsoleApiInstallPluginFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {
|
installPluginFromUri(requestParameters: PluginV1alpha1ConsoleApiInstallPluginFromUriRequest, options?: RawAxiosRequestConfig): AxiosPromise<Plugin> {
|
||||||
return localVarFp.installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(axios, basePath));
|
return localVarFp.installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* List all plugin presets in the system.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
listPluginPresets(options?: RawAxiosRequestConfig): AxiosPromise<Array<Plugin>> {
|
|
||||||
return localVarFp.listPluginPresets(options).then((request) => request(axios, basePath));
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* List plugins using query criteria and sort params
|
* List plugins using query criteria and sort params
|
||||||
* @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.
|
* @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.
|
||||||
|
@ -1527,16 +1471,6 @@ export class PluginV1alpha1ConsoleApi extends BaseAPI {
|
||||||
return PluginV1alpha1ConsoleApiFp(this.configuration).installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(this.axios, this.basePath));
|
return PluginV1alpha1ConsoleApiFp(this.configuration).installPluginFromUri(requestParameters.installFromUriRequest, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* List all plugin presets in the system.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof PluginV1alpha1ConsoleApi
|
|
||||||
*/
|
|
||||||
public listPluginPresets(options?: RawAxiosRequestConfig) {
|
|
||||||
return PluginV1alpha1ConsoleApiFp(this.configuration).listPluginPresets(options).then((request) => request(this.axios, this.basePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List plugins using query criteria and sort params
|
* List plugins using query criteria and sort params
|
||||||
* @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.
|
* @param {PluginV1alpha1ConsoleApiListPluginsRequest} requestParameters Request parameters.
|
||||||
|
|
|
@ -23,8 +23,6 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
|
||||||
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { DashboardStats } from '../models';
|
import { DashboardStats } from '../models';
|
||||||
// @ts-ignore
|
|
||||||
import { SystemInitializationRequest } from '../models';
|
|
||||||
/**
|
/**
|
||||||
* SystemV1alpha1ConsoleApi - axios parameter creator
|
* SystemV1alpha1ConsoleApi - axios parameter creator
|
||||||
* @export
|
* @export
|
||||||
|
@ -63,47 +61,6 @@ export const SystemV1alpha1ConsoleApiAxiosParamCreator = function (configuration
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
return {
|
|
||||||
url: toPathString(localVarUrlObj),
|
|
||||||
options: localVarRequestOptions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Initialize system
|
|
||||||
* @param {SystemInitializationRequest} [systemInitializationRequest]
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
initialize: async (systemInitializationRequest?: SystemInitializationRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
|
||||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/system/initialize`;
|
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
|
||||||
let baseOptions;
|
|
||||||
if (configuration) {
|
|
||||||
baseOptions = configuration.baseOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
|
||||||
const localVarHeaderParameter = {} as any;
|
|
||||||
const localVarQueryParameter = {} as any;
|
|
||||||
|
|
||||||
// authentication basicAuth required
|
|
||||||
// http basic authentication required
|
|
||||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
|
||||||
|
|
||||||
// authentication bearerAuth required
|
|
||||||
// http bearer authentication required
|
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
|
||||||
localVarRequestOptions.data = serializeDataIfNeeded(systemInitializationRequest, localVarRequestOptions, configuration)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
|
@ -130,18 +87,6 @@ export const SystemV1alpha1ConsoleApiFp = function(configuration?: Configuration
|
||||||
const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1ConsoleApi.getStats']?.[localVarOperationServerIndex]?.url;
|
const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1ConsoleApi.getStats']?.[localVarOperationServerIndex]?.url;
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Initialize system
|
|
||||||
* @param {SystemInitializationRequest} [systemInitializationRequest]
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
async initialize(systemInitializationRequest?: SystemInitializationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.initialize(systemInitializationRequest, options);
|
|
||||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
|
||||||
const localVarOperationServerBasePath = operationServerMap['SystemV1alpha1ConsoleApi.initialize']?.[localVarOperationServerIndex]?.url;
|
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -160,32 +105,9 @@ export const SystemV1alpha1ConsoleApiFactory = function (configuration?: Configu
|
||||||
getStats(options?: RawAxiosRequestConfig): AxiosPromise<DashboardStats> {
|
getStats(options?: RawAxiosRequestConfig): AxiosPromise<DashboardStats> {
|
||||||
return localVarFp.getStats(options).then((request) => request(axios, basePath));
|
return localVarFp.getStats(options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Initialize system
|
|
||||||
* @param {SystemV1alpha1ConsoleApiInitializeRequest} requestParameters Request parameters.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
*/
|
|
||||||
initialize(requestParameters: SystemV1alpha1ConsoleApiInitializeRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
|
||||||
return localVarFp.initialize(requestParameters.systemInitializationRequest, options).then((request) => request(axios, basePath));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Request parameters for initialize operation in SystemV1alpha1ConsoleApi.
|
|
||||||
* @export
|
|
||||||
* @interface SystemV1alpha1ConsoleApiInitializeRequest
|
|
||||||
*/
|
|
||||||
export interface SystemV1alpha1ConsoleApiInitializeRequest {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {SystemInitializationRequest}
|
|
||||||
* @memberof SystemV1alpha1ConsoleApiInitialize
|
|
||||||
*/
|
|
||||||
readonly systemInitializationRequest?: SystemInitializationRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SystemV1alpha1ConsoleApi - object-oriented interface
|
* SystemV1alpha1ConsoleApi - object-oriented interface
|
||||||
* @export
|
* @export
|
||||||
|
@ -202,16 +124,5 @@ export class SystemV1alpha1ConsoleApi extends BaseAPI {
|
||||||
public getStats(options?: RawAxiosRequestConfig) {
|
public getStats(options?: RawAxiosRequestConfig) {
|
||||||
return SystemV1alpha1ConsoleApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));
|
return SystemV1alpha1ConsoleApiFp(this.configuration).getStats(options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize system
|
|
||||||
* @param {SystemV1alpha1ConsoleApiInitializeRequest} requestParameters Request parameters.
|
|
||||||
* @param {*} [options] Override http request option.
|
|
||||||
* @throws {RequiredError}
|
|
||||||
* @memberof SystemV1alpha1ConsoleApi
|
|
||||||
*/
|
|
||||||
public initialize(requestParameters: SystemV1alpha1ConsoleApiInitializeRequest = {}, options?: RawAxiosRequestConfig) {
|
|
||||||
return SystemV1alpha1ConsoleApiFp(this.configuration).initialize(requestParameters.systemInitializationRequest, options).then((request) => request(this.axios, this.basePath));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Halo
|
||||||
|
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 2.20.0-SNAPSHOT
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type { Configuration } from '../configuration';
|
||||||
|
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||||
|
import globalAxios from 'axios';
|
||||||
|
// Some imports not used depending on template conditions
|
||||||
|
// @ts-ignore
|
||||||
|
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';
|
||||||
|
// @ts-ignore
|
||||||
|
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||||
|
// @ts-ignore
|
||||||
|
import { UserConnection } from '../models';
|
||||||
|
/**
|
||||||
|
* UserConnectionV1alpha1UcApi - axios parameter creator
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const UserConnectionV1alpha1UcApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Disconnect my connection from a third-party platform.
|
||||||
|
* @param {string} registerId The registration ID of the third-party platform.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
disconnectMyConnection: async (registerId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'registerId' is not null or undefined
|
||||||
|
assertParamExists('disconnectMyConnection', 'registerId', registerId)
|
||||||
|
const localVarPath = `/apis/uc.api.auth.halo.run/v1alpha1/user-connections/{registerId}/disconnect`
|
||||||
|
.replace(`{${"registerId"}}`, encodeURIComponent(String(registerId)));
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;
|
||||||
|
|
||||||
|
// authentication basicAuth required
|
||||||
|
// http basic authentication required
|
||||||
|
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||||
|
|
||||||
|
// authentication bearerAuth required
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserConnectionV1alpha1UcApi - functional programming interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const UserConnectionV1alpha1UcApiFp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = UserConnectionV1alpha1UcApiAxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Disconnect my connection from a third-party platform.
|
||||||
|
* @param {string} registerId The registration ID of the third-party platform.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async disconnectMyConnection(registerId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserConnection>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.disconnectMyConnection(registerId, options);
|
||||||
|
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||||
|
const localVarOperationServerBasePath = operationServerMap['UserConnectionV1alpha1UcApi.disconnectMyConnection']?.[localVarOperationServerIndex]?.url;
|
||||||
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserConnectionV1alpha1UcApi - factory interface
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const UserConnectionV1alpha1UcApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = UserConnectionV1alpha1UcApiFp(configuration)
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Disconnect my connection from a third-party platform.
|
||||||
|
* @param {UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
disconnectMyConnection(requestParameters: UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<UserConnection>> {
|
||||||
|
return localVarFp.disconnectMyConnection(requestParameters.registerId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for disconnectMyConnection operation in UserConnectionV1alpha1UcApi.
|
||||||
|
* @export
|
||||||
|
* @interface UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest
|
||||||
|
*/
|
||||||
|
export interface UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest {
|
||||||
|
/**
|
||||||
|
* The registration ID of the third-party platform.
|
||||||
|
* @type {string}
|
||||||
|
* @memberof UserConnectionV1alpha1UcApiDisconnectMyConnection
|
||||||
|
*/
|
||||||
|
readonly registerId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserConnectionV1alpha1UcApi - object-oriented interface
|
||||||
|
* @export
|
||||||
|
* @class UserConnectionV1alpha1UcApi
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
export class UserConnectionV1alpha1UcApi extends BaseAPI {
|
||||||
|
/**
|
||||||
|
* Disconnect my connection from a third-party platform.
|
||||||
|
* @param {UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof UserConnectionV1alpha1UcApi
|
||||||
|
*/
|
||||||
|
public disconnectMyConnection(requestParameters: UserConnectionV1alpha1UcApiDisconnectMyConnectionRequest, options?: RawAxiosRequestConfig) {
|
||||||
|
return UserConnectionV1alpha1UcApiFp(this.configuration).disconnectMyConnection(requestParameters.registerId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -217,7 +217,6 @@ export * from './subscription';
|
||||||
export * from './subscription-list';
|
export * from './subscription-list';
|
||||||
export * from './subscription-spec';
|
export * from './subscription-spec';
|
||||||
export * from './subscription-subscriber';
|
export * from './subscription-subscriber';
|
||||||
export * from './system-initialization-request';
|
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './tag-list';
|
export * from './tag-list';
|
||||||
export * from './tag-spec';
|
export * from './tag-spec';
|
||||||
|
|
|
@ -82,6 +82,6 @@ export interface PostStatus {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof PostStatus
|
* @memberof PostStatus
|
||||||
*/
|
*/
|
||||||
'phase': string;
|
'phase'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,6 @@ export interface SinglePageStatus {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof SinglePageStatus
|
* @memberof SinglePageStatus
|
||||||
*/
|
*/
|
||||||
'phase': string;
|
'phase'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* Halo
|
|
||||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
|
||||||
*
|
|
||||||
* The version of the OpenAPI document: 2.20.0-SNAPSHOT
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
||||||
* https://openapi-generator.tech
|
|
||||||
* Do not edit the class manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @interface SystemInitializationRequest
|
|
||||||
*/
|
|
||||||
export interface SystemInitializationRequest {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInitializationRequest
|
|
||||||
*/
|
|
||||||
'email'?: string;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInitializationRequest
|
|
||||||
*/
|
|
||||||
'password': string;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInitializationRequest
|
|
||||||
*/
|
|
||||||
'siteTitle'?: string;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
* @memberof SystemInitializationRequest
|
|
||||||
*/
|
|
||||||
'username': string;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1470,25 +1470,6 @@ core:
|
||||||
message: Unauthorized access to this page
|
message: Unauthorized access to this page
|
||||||
actions:
|
actions:
|
||||||
home: Back to home
|
home: Back to home
|
||||||
setup:
|
|
||||||
title: Setup
|
|
||||||
operations:
|
|
||||||
submit:
|
|
||||||
button: Setup
|
|
||||||
toast_success: Setup successfully
|
|
||||||
setup_initial_data:
|
|
||||||
loading: Initializing data, please wait...
|
|
||||||
fields:
|
|
||||||
site_title:
|
|
||||||
label: Site title
|
|
||||||
email:
|
|
||||||
label: Email
|
|
||||||
username:
|
|
||||||
label: Username
|
|
||||||
password:
|
|
||||||
label: Password
|
|
||||||
confirm_password:
|
|
||||||
label: Confirm password
|
|
||||||
rbac:
|
rbac:
|
||||||
Attachments Management: Attachments
|
Attachments Management: Attachments
|
||||||
Attachment Manage: Attachment Manage
|
Attachment Manage: Attachment Manage
|
||||||
|
|
|
@ -1104,25 +1104,6 @@ core:
|
||||||
message: Acceso no autorizado a esta página
|
message: Acceso no autorizado a esta página
|
||||||
actions:
|
actions:
|
||||||
home: Ir a la página de inicio
|
home: Ir a la página de inicio
|
||||||
setup:
|
|
||||||
title: Configuración
|
|
||||||
operations:
|
|
||||||
submit:
|
|
||||||
button: Configurar
|
|
||||||
toast_success: Configuración exitosa
|
|
||||||
setup_initial_data:
|
|
||||||
loading: Inicializando datos, por favor espera...
|
|
||||||
fields:
|
|
||||||
site_title:
|
|
||||||
label: Título del Sitio
|
|
||||||
email:
|
|
||||||
label: Correo Electrónico
|
|
||||||
username:
|
|
||||||
label: Nombre de Usuario
|
|
||||||
password:
|
|
||||||
label: Contraseña
|
|
||||||
confirm_password:
|
|
||||||
label: Confirmar Contraseña
|
|
||||||
rbac:
|
rbac:
|
||||||
Attachments Management: Gestión de archivos adjuntos
|
Attachments Management: Gestión de archivos adjuntos
|
||||||
Attachment Manage: Gestor de adjuntos
|
Attachment Manage: Gestor de adjuntos
|
||||||
|
|
|
@ -1369,25 +1369,6 @@ core:
|
||||||
message: 没有权限访问此页面
|
message: 没有权限访问此页面
|
||||||
actions:
|
actions:
|
||||||
home: 返回首页
|
home: 返回首页
|
||||||
setup:
|
|
||||||
title: 系统初始化
|
|
||||||
operations:
|
|
||||||
submit:
|
|
||||||
button: 初始化
|
|
||||||
toast_success: 初始化成功
|
|
||||||
setup_initial_data:
|
|
||||||
loading: 正在初始化数据,请稍后...
|
|
||||||
fields:
|
|
||||||
site_title:
|
|
||||||
label: 站点名称
|
|
||||||
email:
|
|
||||||
label: 邮箱
|
|
||||||
username:
|
|
||||||
label: 用户名
|
|
||||||
password:
|
|
||||||
label: 密码
|
|
||||||
confirm_password:
|
|
||||||
label: 确认密码
|
|
||||||
rbac:
|
rbac:
|
||||||
Attachments Management: 附件
|
Attachments Management: 附件
|
||||||
Attachment Manage: 附件管理
|
Attachment Manage: 附件管理
|
||||||
|
|
|
@ -1345,25 +1345,6 @@ core:
|
||||||
message: 沒有權限訪問此頁面
|
message: 沒有權限訪問此頁面
|
||||||
actions:
|
actions:
|
||||||
home: 返回首頁
|
home: 返回首頁
|
||||||
setup:
|
|
||||||
title: 系統初始化
|
|
||||||
operations:
|
|
||||||
submit:
|
|
||||||
button: 初始化
|
|
||||||
toast_success: 初始化成功
|
|
||||||
setup_initial_data:
|
|
||||||
loading: 正在初始化資料,請稍後...
|
|
||||||
fields:
|
|
||||||
site_title:
|
|
||||||
label: 站點名稱
|
|
||||||
email:
|
|
||||||
label: 電子郵箱
|
|
||||||
username:
|
|
||||||
label: 用戶名
|
|
||||||
password:
|
|
||||||
label: 密碼
|
|
||||||
confirm_password:
|
|
||||||
label: 確認密碼
|
|
||||||
rbac:
|
rbac:
|
||||||
Attachments Management: 附件
|
Attachments Management: 附件
|
||||||
Attachment Manage: 附件管理
|
Attachment Manage: 附件管理
|
||||||
|
|
Loading…
Reference in New Issue