feat: implement auto-setup for Lite edition, enhancing user experience by automating user and tenant creation on first launch

This commit is contained in:
wizardchen
2026-04-11 12:47:17 +08:00
committed by lyingbug
parent 52bdfd388a
commit 92bf52721c
8 changed files with 221 additions and 56 deletions
+35 -4
View File
@@ -46,6 +46,11 @@ import (
const dragHandlerJS = `(function(){
if(window.__wkDragBound)return;
window.__wkDragBound=true;
document.documentElement.classList.add('wails-desktop');
// Disable rubber-band overscroll that reveals the dark window background
document.documentElement.style.overscrollBehavior='none';
document.body.style.overscrollBehavior='none';
if(window.wails&&window.wails.flags){
window.wails.flags.cssDragProperty='__disabled__';
@@ -107,6 +112,32 @@ window.addEventListener('mousedown',function(e){
e.preventDefault();
sendDrag();
},true);
// Intercept external link clicks and window.open so they open in the system browser
document.addEventListener('click',function(e){
var el=e.target;
while(el&&el.tagName!=='A')el=el.parentElement;
if(!el||!el.href)return;
var href=el.href;
if(href.indexOf('http://')===0||href.indexOf('https://')===0){
if(window.runtime&&window.runtime.BrowserOpenURL){
e.preventDefault();
e.stopPropagation();
window.runtime.BrowserOpenURL(href);
}
}
},true);
var origOpen=window.open;
window.open=function(url){
if(url&&(typeof url==='string')&&(url.indexOf('http://')===0||url.indexOf('https://')===0)){
if(window.runtime&&window.runtime.BrowserOpenURL){
window.runtime.BrowserOpenURL(url);
return null;
}
}
return origOpen.apply(window,arguments);
};
})();`
func main() {
@@ -267,11 +298,11 @@ func main() {
Bind: []interface{}{
app,
},
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Mac: &mac.Options{
TitleBar: mac.TitleBarHiddenInset(), // Beautiful borderless titlebar
Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true,
WindowIsTranslucent: true,
TitleBar: mac.TitleBarHiddenInset(),
WebviewIsTransparent: false,
WindowIsTranslucent: false,
},
})
+15
View File
@@ -190,6 +190,21 @@ export async function register(data: RegisterRequest): Promise<RegisterResponse>
}
}
/**
* Lite 版自动初始化(创建默认用户/租户 + 签发令牌)
*/
export async function autoSetup(): Promise<LoginResponse> {
try {
const response = await post('/api/v1/auth/auto-setup', {})
return response as unknown as LoginResponse
} catch (error: any) {
return {
success: false,
message: error.message || 'Auto-setup unavailable'
}
}
}
/**
* 获取当前用户信息
*/
+5
View File
@@ -768,6 +768,11 @@ const onDragHandleMouseDown = (e: MouseEvent) => {
transition: width 0.25s ease, min-width 0.25s ease;
position: relative;
// macOS Wails 桌面:红绿灯位于 HiddenInset 标题栏区域,需让出顶部空间
html.wails-desktop & {
padding-top: 30px;
}
&--collapsed {
min-width: 60px;
width: 60px;
+45 -18
View File
@@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import { listKnowledgeBases } from '@/api/knowledge-base'
import { useAuthStore } from '@/stores/auth'
import { validateToken } from '@/api/auth'
import { validateToken, autoSetup } from '@/api/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -105,6 +105,36 @@ const router = createRouter({
],
});
// 持久化 auto-setup / login 返回的认证信息到 store
function persistLoginResponse(authStore: ReturnType<typeof useAuthStore>, response: any) {
if (response.user && response.tenant && response.token) {
authStore.setUser({
id: response.user.id || '',
username: response.user.username || '',
email: response.user.email || '',
avatar: response.user.avatar,
tenant_id: String(response.tenant.id) || '',
can_access_all_tenants: response.user.can_access_all_tenants || false,
created_at: response.user.created_at || new Date().toISOString(),
updated_at: response.user.updated_at || new Date().toISOString()
})
authStore.setToken(response.token)
if (response.refresh_token) {
authStore.setRefreshToken(response.refresh_token)
}
authStore.setTenant({
id: String(response.tenant.id) || '',
name: response.tenant.name || '',
api_key: response.tenant.api_key || '',
owner_id: response.user.id || '',
created_at: response.tenant.created_at || new Date().toISOString(),
updated_at: response.tenant.updated_at || new Date().toISOString()
})
}
}
let autoSetupAttempted = false
// 路由守卫:检查认证状态和系统初始化状态
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
@@ -123,26 +153,23 @@ router.beforeEach(async (to, from, next) => {
// 检查用户认证状态
if (to.meta.requiresAuth !== false) {
if (!authStore.isLoggedIn) {
// 未登录,跳转到登录页面
// Lite 版:尝试自动初始化(仅一次),成功则跳过登录页
if (!autoSetupAttempted) {
autoSetupAttempted = true
try {
const response = await autoSetup()
if (response.success) {
persistLoginResponse(authStore, response)
next(to.fullPath)
return
}
} catch {
// auto-setup 不可用(非 lite 版本),走正常登录流程
}
}
next('/login')
return
}
// 验证Token有效性
// try {
// const { valid } = await validateToken()
// if (!valid) {
// // Token无效,清空认证信息并跳转到登录页面
// authStore.logout()
// next('/login')
// return
// }
// } catch (error) {
// console.error('Token验证失败:', error)
// authStore.logout()
// next('/login')
// return
// }
}
next()
+13 -3
View File
@@ -377,7 +377,7 @@ import { Autoplay, EffectFade, Pagination } from 'swiper/modules'
import 'swiper/css'
import 'swiper/css/effect-fade'
import 'swiper/css/pagination'
import { login, register, getOIDCAuthorizationURL, getOIDCConfig } from '@/api/auth'
import { login, register, getOIDCAuthorizationURL, getOIDCConfig, autoSetup } from '@/api/auth'
import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
@@ -668,13 +668,23 @@ const handleRegister = async () => {
}
}
// Check if already logged in
onMounted(() => {
// Check if already logged in; for lite edition, attempt transparent auto-setup
onMounted(async () => {
if (authStore.isLoggedIn) {
router.replace('/platform/knowledge-bases')
return
}
try {
const response = await autoSetup()
if (response.success) {
await persistLoginResponse(response)
return
}
} catch {
// auto-setup not available (standard edition) — show normal login form
}
loadOIDCConfig()
})
</script>
+75
View File
@@ -1,8 +1,10 @@
package handler
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
@@ -504,6 +506,79 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) {
})
}
// AutoSetup godoc
// @Summary 自动初始化(Lite 桌面版)
// @Description Lite 版专用:首次启动时自动创建默认用户和租户并返回令牌,后续启动直接签发令牌,免除手动注册/登录流程
// @Tags 认证
// @Accept json
// @Produce json
// @Success 200 {object} types.LoginResponse
// @Failure 403 {object} errors.AppError "非 Lite 版本"
// @Router /auth/auto-setup [post]
func (h *AuthHandler) AutoSetup(c *gin.Context) {
ctx := c.Request.Context()
if Edition != "lite" {
appErr := errors.NewForbiddenError("auto-setup is only available in lite edition")
c.Error(appErr)
return
}
const defaultEmail = "admin@weknora.local"
user, _ := h.userService.GetUserByEmail(ctx, defaultEmail)
if user == nil {
logger.Info(ctx, "Auto-setup: creating default user and tenant for lite edition")
randomBytes := make([]byte, 24)
if _, err := rand.Read(randomBytes); err != nil {
appErr := errors.NewInternalServerError("auto-setup failed: unable to generate credentials")
c.Error(appErr)
return
}
randomPassword := base64.RawURLEncoding.EncodeToString(randomBytes)
randomUsername := fmt.Sprintf("user_%s", base64.RawURLEncoding.EncodeToString(randomBytes[:6]))
_, err := h.userService.Register(ctx, &types.RegisterRequest{
Username: randomUsername,
Email: defaultEmail,
Password: randomPassword,
})
if err != nil {
logger.Errorf(ctx, "Auto-setup: failed to register default user: %v", err)
appErr := errors.NewInternalServerError("auto-setup failed").WithDetails(err.Error())
c.Error(appErr)
return
}
user, _ = h.userService.GetUserByEmail(ctx, defaultEmail)
if user == nil {
appErr := errors.NewInternalServerError("auto-setup failed: user not found after registration")
c.Error(appErr)
return
}
}
accessToken, refreshToken, err := h.userService.GenerateTokens(ctx, user)
if err != nil {
logger.Errorf(ctx, "Auto-setup: failed to generate tokens: %v", err)
appErr := errors.NewInternalServerError("auto-setup failed").WithDetails(err.Error())
c.Error(appErr)
return
}
tenant, _ := h.tenantService.GetTenantByID(ctx, user.TenantID)
logger.Info(ctx, "Auto-setup: completed successfully")
c.JSON(http.StatusOK, &types.LoginResponse{
Success: true,
Message: "Auto-setup successful",
User: user,
Tenant: tenant,
Token: accessToken,
RefreshToken: refreshToken,
})
}
// ValidateToken godoc
// @Summary 验证令牌
// @Description 验证访问令牌是否有效
+1
View File
@@ -21,6 +21,7 @@ var noAuthAPI = map[string][]string{
"/health": {"GET"},
"/api/v1/auth/register": {"POST"},
"/api/v1/auth/login": {"POST"},
"/api/v1/auth/auto-setup": {"POST"},
"/api/v1/auth/oidc/config": {"GET"},
"/api/v1/auth/oidc/url": {"GET"},
"/api/v1/auth/oidc/callback": {"GET"},
+32 -31
View File
@@ -31,37 +31,37 @@ import (
type RouterParams struct {
dig.In
Config *config.Config
UserService interfaces.UserService
KBService interfaces.KnowledgeBaseService
KnowledgeService interfaces.KnowledgeService
ChunkService interfaces.ChunkService
SessionService interfaces.SessionService
MessageService interfaces.MessageService
ModelService interfaces.ModelService
EvaluationService interfaces.EvaluationService
KBHandler *handler.KnowledgeBaseHandler
KnowledgeHandler *handler.KnowledgeHandler
TenantHandler *handler.TenantHandler
TenantService interfaces.TenantService
ChunkHandler *handler.ChunkHandler
SessionHandler *session.Handler
MessageHandler *handler.MessageHandler
ModelHandler *handler.ModelHandler
EvaluationHandler *handler.EvaluationHandler
AuthHandler *handler.AuthHandler
InitializationHandler *handler.InitializationHandler
SystemHandler *handler.SystemHandler
MCPServiceHandler *handler.MCPServiceHandler
WebSearchHandler *handler.WebSearchHandler
WebSearchProviderHandler *handler.WebSearchProviderHandler
FAQHandler *handler.FAQHandler
TagHandler *handler.TagHandler
CustomAgentHandler *handler.CustomAgentHandler
SkillHandler *handler.SkillHandler
OrganizationHandler *handler.OrganizationHandler
IMHandler *handler.IMHandler
DataSourceHandler *handler.DataSourceHandler
Config *config.Config
UserService interfaces.UserService
KBService interfaces.KnowledgeBaseService
KnowledgeService interfaces.KnowledgeService
ChunkService interfaces.ChunkService
SessionService interfaces.SessionService
MessageService interfaces.MessageService
ModelService interfaces.ModelService
EvaluationService interfaces.EvaluationService
KBHandler *handler.KnowledgeBaseHandler
KnowledgeHandler *handler.KnowledgeHandler
TenantHandler *handler.TenantHandler
TenantService interfaces.TenantService
ChunkHandler *handler.ChunkHandler
SessionHandler *session.Handler
MessageHandler *handler.MessageHandler
ModelHandler *handler.ModelHandler
EvaluationHandler *handler.EvaluationHandler
AuthHandler *handler.AuthHandler
InitializationHandler *handler.InitializationHandler
SystemHandler *handler.SystemHandler
MCPServiceHandler *handler.MCPServiceHandler
WebSearchHandler *handler.WebSearchHandler
WebSearchProviderHandler *handler.WebSearchProviderHandler
FAQHandler *handler.FAQHandler
TagHandler *handler.TagHandler
CustomAgentHandler *handler.CustomAgentHandler
SkillHandler *handler.SkillHandler
OrganizationHandler *handler.OrganizationHandler
IMHandler *handler.IMHandler
DataSourceHandler *handler.DataSourceHandler
}
// NewRouter 创建新的路由
@@ -397,6 +397,7 @@ func RegisterEvaluationRoutes(r *gin.RouterGroup, handler *handler.EvaluationHan
func RegisterAuthRoutes(r *gin.RouterGroup, handler *handler.AuthHandler) {
r.POST("/auth/register", handler.Register)
r.POST("/auth/login", handler.Login)
r.POST("/auth/auto-setup", handler.AutoSetup)
r.GET("/auth/oidc/config", handler.GetOIDCConfig)
r.GET("/auth/oidc/url", handler.GetOIDCAuthorizationURL)
r.GET("/auth/oidc/callback", handler.OIDCRedirectCallback)