feat!(init): add initialization wizard (#3041)

feat: add system initialization (setup wizard) support

Co-authored-by: PIKACHUIM <PIKACHUIM@users.noreply.github.com>
This commit is contained in:
Pikachu Ren
2026-09-07 14:14:22 +08:00
committed by GitHub
parent d9d8aa24e6
commit 2d51c9ab4b
3 changed files with 107 additions and 10 deletions
+19 -6
View File
@@ -15,18 +15,28 @@ import (
)
func initUser() {
admin, err := op.GetAdmin()
adminPassword := random.String(8)
initAdmin()
initGuest()
}
func initAdmin() {
_, err := op.GetAdmin()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 系统尚未初始化:仅在开发模式或显式配置 OPENLIST_ADMIN_PASSWORD 时
// 自动创建管理员;否则交由 Web 安装向导(POST /api/public/init/setup)完成。
adminPassword := "admin"
envpass := os.Getenv("OPENLIST_ADMIN_PASSWORD")
if flags.Dev {
adminPassword = "admin"
} else if len(envpass) > 0 {
adminPassword = envpass
} else {
// 未初始化:不自动创建管理员,等待 Web 安装向导。
return
}
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16)
admin = &model.User{
admin := &model.User{
Username: "admin",
Salt: salt,
PwdHash: model.TwoHashPwd(adminPassword, salt),
@@ -47,7 +57,10 @@ func initUser() {
utils.Log.Fatalf("[init user] Failed to get admin user: %v", err)
}
}
_, err = op.GetGuest()
}
func initGuest() {
_, err := op.GetGuest()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
salt := random.String(16)
+82
View File
@@ -0,0 +1,82 @@
package handles
import (
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/server/common"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"gorm.io/gorm"
)
// InitStatus 返回系统是否已完成初始化(即是否已存在管理员账号)。
// 前端据此判断是否跳转到安装向导。
func InitStatus(c *gin.Context) {
if _, err := op.GetAdmin(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.SuccessResp(c, gin.H{"initialized": false})
return
}
common.ErrorResp(c, err, 500, true)
return
}
common.SuccessResp(c, gin.H{"initialized": true})
}
// InitSetupReq 系统初始化请求体
type InitSetupReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
SiteTitle string `json:"site_title"`
}
// InitSetup 执行系统初始化:创建管理员账号并设置站点名称等初始参数。
// 仅在系统尚未初始化时允许调用;已初始化则返回错误。
func InitSetup(c *gin.Context) {
var req InitSetupReq
if err := c.ShouldBind(&req); err != nil {
common.ErrorResp(c, err, 400)
return
}
// 已初始化则拒绝
if _, err := op.GetAdmin(); err == nil {
common.ErrorStrResp(c, "system has already been initialized", 400)
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
common.ErrorResp(c, err, 500, true)
return
}
// 密码最小长度校验
if len(req.Password) < 4 {
common.ErrorStrResp(c, "password must be at least 4 characters", 400)
return
}
admin := &model.User{
Username: req.Username,
Role: model.ADMIN,
BasePath: "/",
Authn: "[]",
Permission: 0x71FF,
}
admin.SetPassword(req.Password)
if err := op.CreateUser(admin); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
// 设置站点名称(如果提供)
if req.SiteTitle != "" {
item := model.SettingItem{
Key: conf.SiteTitle,
Value: req.SiteTitle,
Type: conf.TypeString,
Group: model.SITE,
Flag: model.PUBLIC,
}
if err := op.SaveSettingItem(&item); err != nil {
common.ErrorResp(c, err, 500, true)
return
}
}
common.SuccessResp(c)
}
+2
View File
@@ -102,6 +102,8 @@ func Init(e *gin.Engine) {
public.Any("/settings", handles.PublicSettings)
public.Any("/offline_download_tools", handles.OfflineDownloadTools)
public.Any("/archive_extensions", handles.ArchiveExtensions)
public.Any("/init_status", handles.InitStatus)
public.POST("/init/setup", handles.InitSetup)
_fs(auth.Group("/fs"))
fsAndShare(api.Group("/fs", middlewares.Auth(true)))