feat: implement auto-setup for Lite edition, enhancing user experience by automating user and tenant creation on first launch
This commit is contained in:
+35
-4
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 验证访问令牌是否有效
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user