rename to vbase

v3
veypi 3 months ago
parent 4e48cd187d
commit 6d0ec8e6ba

@ -1,3 +1,3 @@
# VBase
对标 Firebase 的后端服务,基于 vyes/vigo 框架实现,提供用户认证、数据库存储、文件存储等功能。
对标 Firebase 的后端服务,基于 vhtml/vigo 框架实现,提供用户认证、数据库存储、文件存储等功能。

@ -1,33 +1,6 @@
# UI Refactoring TODO List
任务描述阅读agents.md指令, 我重构了common.css现在两个任务一个是查阅vyes-ui的文档尽量使用vyes-ui组件去重构界面减少代码量 一个是修改所有界面及组件的样式使用全局变量颜色可以直接使用也可以用color-mix去混色增加颜色层次感和丰富度。 将界面修改任务写到TODO.md里更改完一个界面就标记这个页面完成
## 接口规范检查与修复任务
## Pages
- [x] /ui/page/index.html
- [x] /ui/page/login.html
- [x] /ui/page/profile.html
- [x] /ui/page/stats.html
- [x] /ui/page/app.html
- [x] /ui/page/settings.html
- [x] /ui/page/latest.html
- [x] /ui/page/dsr1.html
- [x] /ui/page/test.html
- [x] /ui/page/404.html
## App Pages
- [x] /ui/page/app/index.html
- [x] /ui/page/app/auth.html
- [x] /ui/page/app/user.html
- [x] /ui/page/app/settings.html
## Layouts
- [x] /ui/layout/default.html
- [x] /ui/layout/public.html
- [x] /ui/layout/app.html
## Components
- [x] /ui/c/app/create.html
- [x] /ui/c/app/menu.html (Deleted)
- [x] /ui/c/table.html
- [x] /ui/vselect.html
- [x] /ui/ico.html
- [x] /ui/root.html
- [ ] 检查并修复 `api/app` 及其子模块接口规范: `access`, `app_user`, `resource`, `role`
- [ ] 检查并修复 `api/sms` 接口规范
- [ ] 检查并修复 `api/token` 接口规范
- [ ] 检查并修复 `api/user` 及其子模块接口规范: `role`

@ -1,21 +1,23 @@
# 开发规范
## 注意
如果开发中发现什么开发规则或者技巧,你可以更新在这个文档,供其他人看
## UI界面开发指南
- 界面采用vyes.js 框架该框架可以将一个html文件自动加载为一个组件
- 开始写界面前请阅读全局样式文件 /ui/assets/common.css 保证所有界面的样式一致
- 组件内部避免重复的样式定义 如body内无需重复定义字体
- 本项目使用vyes-ui 组件库,该组件库可以通过 curl -sS http://localhost:4000/v/README.md 查看文档 其组件代码都已经映射到了/v/目录下
## UI 界面开发指南
- 界面采用 vhtml 框架,该框架可以将一个 html 文件自动加载为一个组件
- 开始写界面前请阅读全局样式文件 /ui/assets/common.css组件内必须使用全局中的变量去组合或者直接使用全局中的样式 保证所有界面的样式一致, 比如只能使用颜色变量或者通过 color-mix 函数去包含至少一个颜色变量
- 组件内部避免重复的样式定义 如 body 内无需重复定义字体
- 本项目使用 vhtml-ui 组件库,该组件库可以通过 curl -sS <http://localhost:4000/v/README.md> 查看文档 其组件代码都已经映射到了/v/目录下
- 前端路由文件 /ui/routes.js 该文件定义了所有的路由规则
## vhtml-ui 文档查看方法
## vyes-ui 文档查看方法
curl 指令可以不用沙盒运行
获取文档目录 该操作可以查看所有组件的目录结构
curl -sS http://localhost:4000/v/docs/README.md?toc=1
curl -sS <http://localhost:4000/v/docs/README.md?toc=1>
获取文档全文(较长,一般建议查询目录再查询章节内容)
curl -sS http://localhost:4000/v/docs/README.md
获取章节内容(可以根据第一步获取的目录编号查询内容) 一般使用这个查询章节内容查询内容时不能带toc参数
curl -sS http://localhost:4000/v/docs/README.md?from=1.2&to=1.2
curl -sS <http://localhost:4000/v/docs/README.md>
获取章节内容(可以根据第一步获取的目录编号查询内容) 一般使用这个查询章节内容,查询内容时不能带 toc 参数
curl -sS <http://localhost:4000/v/docs/README.md?from=1.2&to=1.2>

@ -7,9 +7,9 @@
package access
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/veypi/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
)
type createOpts struct {

@ -7,9 +7,9 @@
package access
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type deleteOpts struct {

@ -7,9 +7,9 @@
package access
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type getIDReq struct {

@ -8,7 +8,7 @@
package access
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -7,9 +7,9 @@
package access
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type listOpts struct {

@ -7,9 +7,9 @@
package access
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type updateOpts struct {

@ -5,11 +5,11 @@ import (
"math/rand"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"gorm.io/gorm"
)

@ -1,9 +1,9 @@
package app_user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type createOpts struct {

@ -1,9 +1,9 @@
package app_user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type deleteOpts struct {

@ -1,9 +1,9 @@
package app_user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type appUserIDReq struct {

@ -8,7 +8,7 @@
package app_user
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -1,9 +1,9 @@
package app_user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type listOpts struct {

@ -1,9 +1,9 @@
package app_user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type updateOpts struct {

@ -8,12 +8,12 @@
package app
import (
"github.com/veypi/OneAuth/api/app/access"
"github.com/veypi/OneAuth/api/app/app_user"
"github.com/veypi/OneAuth/api/app/resource"
"github.com/veypi/OneAuth/api/app/role"
"github.com/veypi/OneAuth/libs/auth"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/api/app/access"
"github.com/veypi/vbase/api/app/app_user"
"github.com/veypi/vbase/api/app/resource"
"github.com/veypi/vbase/api/app/role"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -1,9 +1,9 @@
package resource
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type createOpts struct {

@ -1,9 +1,9 @@
package resource
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type deleteOpts struct {

@ -1,9 +1,9 @@
package resource
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type getIDReq struct {

@ -8,7 +8,7 @@
package resource
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -1,9 +1,9 @@
package resource
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type listOpts struct {

@ -1,9 +1,9 @@
package resource
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type updateOpts struct {

@ -1,9 +1,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type createOpts struct {

@ -1,9 +1,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type deleteOpts struct {

@ -1,9 +1,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type getIDReq struct {

@ -8,7 +8,7 @@
package role
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -1,9 +1,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
var _ = Router.Get("/", "获取角色列表", listRoles)

@ -1,9 +1,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type updateOpts struct {

@ -8,14 +8,14 @@
package api
import (
"github.com/veypi/OneAuth/api/app"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/api/token"
"github.com/veypi/OneAuth/api/user"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/common"
"github.com/veypi/vbase/api/app"
"github.com/veypi/vbase/api/sms"
"github.com/veypi/vbase/api/token"
"github.com/veypi/vbase/api/user"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/common"
)
var Router = vigo.NewRouter()

@ -7,7 +7,7 @@
package sms
import "github.com/vyes-ai/vigo"
import "github.com/veypi/vigo"
var Router = vigo.NewRouter()

@ -4,12 +4,12 @@ import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/sms_providers"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/limiter"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/sms_providers"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/limiter"
"gorm.io/gorm"
)

@ -11,8 +11,8 @@ import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"gorm.io/gorm"
)

@ -11,10 +11,10 @@ import (
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"gorm.io/gorm"
)

@ -3,9 +3,9 @@ package token
import (
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type patchOpts struct {

@ -12,11 +12,11 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/logv"
)
type postOpts struct {

@ -8,9 +8,9 @@
package token
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type getOpts struct {

@ -8,7 +8,7 @@
package token
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -15,11 +15,11 @@ import (
"time"
"github.com/google/uuid"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/api/sms"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"gorm.io/gorm"
)

@ -8,9 +8,9 @@
package user
import (
"github.com/veypi/OneAuth/api/user/role"
"github.com/veypi/OneAuth/libs/auth"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/api/user/role"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vigo"
)
var Router = vigo.NewRouter()

@ -12,14 +12,14 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/veypi/OneAuth/api/sms"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/libs/utils"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/limiter"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vbase/api/sms"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vbase/libs/utils"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/limiter"
"github.com/veypi/vigo/logv"
)
var publicLimits = limiter.NewAdvancedRequestLimiter(time.Minute*5, 20, time.Second*3, nil).Limit

@ -8,9 +8,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
var _ = Router.Post("/", "创建用户角色", userRolePost)

@ -8,9 +8,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type deleteIDReq struct {

@ -8,10 +8,10 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
type getIDReq struct {

@ -7,6 +7,6 @@
package role
import "github.com/vyes-ai/vigo"
import "github.com/veypi/vigo"
var Router = vigo.NewRouter()

@ -8,9 +8,9 @@
package role
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
)
type patchOpts struct {

@ -1,11 +1,11 @@
package user
import (
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/libs/auth"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/crud"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/libs/auth"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/crud"
)
type userIDReq struct {

@ -8,48 +8,34 @@
package main
import (
"github.com/veypi/OneAuth"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/OneAuth/models"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/flags"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vbase"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vbase/models"
"github.com/veypi/vigo"
"github.com/veypi/vigo/flags"
"github.com/veypi/vigo/logv"
)
var (
cmdMain = flags.New("app", "the backend server of app")
cmdCfg = cmdMain.SubCommand("cfg", "generate cfg file")
cmdDB = cmdMain.SubCommand("db", "database operations")
)
var configFile = cmdMain.String("f", "./dev.yaml", "the config file")
var cliOpts = &struct {
Host string `json:"host"`
Port int `json:"port"`
Host string `json:"host" short:"h`
Port int `json:"port" short:"p"`
LoggerPath string `json:"logger_path,omitempty"`
LoggerLevel string `json:"logger_level,omitempty"`
Core *cfg.Options
*cfg.Options
}{
Core: cfg.Config,
Host: "0.0.0.0",
Port: 4000,
LoggerLevel: "debug",
Options: cfg.Config,
}
func init() {
cmdMain.StringVar(&cliOpts.Host, "host", "0.0.0.0", "host")
cmdMain.IntVar(&cliOpts.Port, "p", 4000, "port")
cmdMain.StringVar(&cliOpts.LoggerLevel, "l", "info", "log level")
cmdMain.AutoRegister(cfg.Config)
var (
cmdMain = flags.New("app", "the backend server of app", cliOpts)
cmdDB = cmdMain.SubCommand("db", "database operations")
)
cmdMain.Before = func() error {
flags.LoadCfg(*configFile, cfg.Config)
cmdMain.Parse()
logv.SetLevel(logv.AssertFuncErr(logv.ParseLevel(cliOpts.LoggerLevel)))
return nil
}
func init() {
cmdMain.Command = runWeb
cmdCfg.Command = func() error {
return flags.DumpCfg(*configFile, cfg.Config)
}
cmdDB.SubCommand("migrate", "migrate database").Command = models.Migrate
cmdDB.SubCommand("drop", "drop database").Command = models.Drop
cmdDB.SubCommand("init", "init db data").Command = models.InitDB
@ -64,10 +50,11 @@ func main() {
}
func runWeb() error {
logv.SetLevel(logv.AssertFuncErr(logv.ParseLevel(cliOpts.LoggerLevel)))
server, err := vigo.New(vigo.WithHost(cliOpts.Host), vigo.WithPort(cliOpts.Port))
if err != nil {
return err
}
server.SetRouter(OneAuth.Router)
server.SetRouter(vbase.Router)
return server.Run()
}

@ -2,5 +2,5 @@ host: 0.0.0.0
port: 4003
level: debug
dsn: root:123456@tcp(127.0.0.1:3306)/test2?charset=utf8&parseTime=True&loc=Local
repo: /home/v/workspace/OneAuth/new/
repo: /home/v/workspace/vbase/new/
dev: true

@ -7,7 +7,7 @@
package errs
import "github.com/vyes-ai/vigo"
import "github.com/veypi/vigo"
var (
AuthNotFound = vigo.NewError("auth not found").WithCode(40100)

@ -1,10 +1,10 @@
module github.com/veypi/OneAuth
module github.com/veypi/vbase
go 1.24.1
replace github.com/veypi/vyes-ui => ../vyes-ui/
replace github.com/veypi/vhtml-ui => ../vhtml-ui/
replace github.com/vyes-ai/vigo => ../vigo/
replace github.com/veypi/vigo => ../vigo/
require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9
@ -15,9 +15,8 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/uuid v1.6.0
github.com/veypi/vyes-ui v0.0.0-00010101000000-000000000000
github.com/vyes-ai/vigo v0.5.2
golang.org/x/crypto v0.40.0
github.com/veypi/vhtml-ui v0.0.0-00010101000000-000000000000
github.com/veypi/vigo v0.6.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1
@ -50,6 +49,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect

@ -4,16 +4,16 @@
// Distributed under terms of the MIT license.
//
package OneAuth
package vbase
import (
"embed"
"github.com/veypi/OneAuth/api"
vyesui "github.com/veypi/vyes-ui"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/contrib/cors"
"github.com/vyes-ai/vigo/contrib/vyes"
"github.com/veypi/vbase/api"
vhtmlui "github.com/veypi/vhtml-ui"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/cors"
"github.com/veypi/vigo/contrib/vhtml"
)
var Router = vigo.NewRouter()
@ -22,8 +22,8 @@ var Router = vigo.NewRouter()
var uifs embed.FS
func init() {
Router.Extend("v", vyesui.Router)
Router.Extend("v", vhtmlui.Router)
Router.Extend("api", api.Router)
Router.SubRouter("/**").Use(cors.AllowAny)
vyes.WrapUI(Router, uifs)
vhtml.WrapUI(Router, uifs)
}

@ -14,8 +14,8 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vigo"
)
var (

@ -10,9 +10,9 @@ import (
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
credential "github.com/aliyun/credentials-go/credentials"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vbase/cfg"
"github.com/veypi/vigo"
"github.com/veypi/vigo/logv"
)
// AliyunProvider 阿里云短信服务

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/vbase/cfg"
)
// SMSProvider 短信服务商接口

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/vbase/cfg"
)
// TencentProvider 腾讯云短信服务

@ -16,7 +16,7 @@ import (
"net/url"
"strings"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vigo/logv"
)
type anyDirs interface {

@ -17,7 +17,7 @@ import (
"strings"
"time"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vigo/logv"
)
func NewWebdav(p string) *Handler {

@ -32,7 +32,7 @@ import (
// In the long term, this package should use the standard library's version
// only, and the internal fork deleted, once
// https://github.com/golang/go/issues/13400 is resolved.
ixml "github.com/veypi/OneAuth/libs/webdav/internal/xml"
ixml "github.com/veypi/vbase/libs/webdav/internal/xml"
)
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo

@ -16,7 +16,7 @@ import (
"strings"
"testing"
ixml "github.com/veypi/OneAuth/libs/webdav/internal/xml"
ixml "github.com/veypi/vbase/libs/webdav/internal/xml"
)
func TestReadLockInfo(t *testing.T) {

@ -1,8 +1,8 @@
package models
import (
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vigo"
"github.com/veypi/vigo/logv"
"gorm.io/gorm"
)

@ -9,11 +9,11 @@ package models
import (
"strings"
"github.com/veypi/OneAuth/cfg"
"github.com/veypi/vbase/cfg"
"github.com/google/uuid"
"github.com/vyes-ai/vigo"
"github.com/vyes-ai/vigo/logv"
"github.com/veypi/vigo"
"github.com/veypi/vigo/logv"
)
var AllModels = &vigo.ModelList{}

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
// SMSCode 短信验证码记录

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
)
// refresh token由oa 秘钥签发,有效期长, 存储在token表

@ -1,7 +1,7 @@
package models
import (
"github.com/vyes-ai/vigo"
"github.com/veypi/vigo"
"gorm.io/gorm"
)

@ -1,242 +0,0 @@
# OAuth 服务器数据库设计
本文档介绍了基于您现有用户管理系统的OAuth 2.0服务器数据库设计。
## 核心表结构
### 1. OAuth 客户端相关
#### `oauth_clients` - OAuth客户端表
存储注册到系统的OAuth客户端应用信息。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| client_id | varchar(255) | 客户端ID唯一 |
| client_secret | varchar(255) | 客户端密钥(加密存储) |
| client_name | varchar(255) | 客户端应用名称 |
| client_uri | varchar(500) | 客户端主页 |
| logo_uri | varchar(500) | 客户端Logo |
| redirect_uris | text | 重定向URI列表JSON格式 |
| response_types | varchar(255) | 支持的响应类型 |
| grant_types | varchar(255) | 支持的授权类型 |
| scope | text | 授权范围 |
| is_public | boolean | 是否为公开客户端 |
| is_active | boolean | 是否激活 |
| owner_id | varchar(32) | 客户端拥有者ID |
#### `oauth_scopes` - OAuth授权范围表
定义可用的授权范围。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| name | varchar(100) | 范围名称(唯一) |
| display_name | varchar(100) | 显示名称 |
| description | text | 范围描述 |
| is_default | boolean | 是否为默认范围 |
| is_system | boolean | 是否为系统范围 |
#### `oauth_client_scopes` - 客户端授权范围关联表
定义客户端可以请求的授权范围。
| 字段 | 类型 | 说明 |
|-----|------|------|
| client_id | varchar(32) | 客户端ID |
| scope_id | varchar(32) | 范围ID |
### 2. OAuth 授权流程相关
#### `oauth_authorization_codes` - 授权码表
存储授权码流程中的临时授权码。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| code | varchar(255) | 授权码(唯一) |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| redirect_uri | varchar(500) | 重定向URI |
| scope | text | 授权范围 |
| code_challenge | varchar(255) | PKCE代码挑战 |
| code_challenge_method | varchar(50) | PKCE挑战方法 |
| expires_at | timestamp | 过期时间 |
| used | boolean | 是否已使用 |
#### `oauth_access_tokens` - 访问令牌表
存储颁发的访问令牌。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| token | varchar(500) | 访问令牌(唯一) |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| revoked | boolean | 是否已撤销 |
#### `oauth_refresh_tokens` - 刷新令牌表
存储刷新令牌,用于获取新的访问令牌。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| token | varchar(500) | 刷新令牌(唯一) |
| access_token_id | varchar(32) | 关联的访问令牌ID |
| client_id | varchar(32) | 客户端ID |
| user_id | varchar(32) | 用户ID |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| revoked | boolean | 是否已撤销 |
#### `oauth_user_consents` - 用户授权同意表
记录用户对客户端的授权同意。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| client_id | varchar(32) | 客户端ID |
| scope | text | 授权范围 |
| consent_at | timestamp | 同意时间 |
| expires_at | timestamp | 同意过期时间 |
### 3. 第三方OAuth登录相关
#### `oauth_providers` - 第三方OAuth提供商表
配置第三方OAuth提供商如GitHub, Google等
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| name | varchar(100) | 提供商名称(唯一) |
| display_name | varchar(100) | 显示名称 |
| client_id | varchar(255) | 客户端ID |
| client_secret | varchar(255) | 客户端密钥 |
| auth_url | varchar(500) | 授权URL |
| token_url | varchar(500) | 令牌URL |
| user_info_url | varchar(500) | 用户信息URL |
| scope | text | 默认授权范围 |
| is_active | boolean | 是否激活 |
#### `oauth_accounts` - 用户OAuth账户表
存储用户通过第三方OAuth登录的账户信息。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 本系统用户ID |
| provider_id | varchar(32) | 提供商ID |
| provider_user_id | varchar(255) | 提供商用户ID |
| email | varchar(255) | 邮箱 |
| username | varchar(255) | 用户名 |
| nickname | varchar(255) | 昵称 |
| avatar | varchar(500) | 头像URL |
| access_token | text | 访问令牌 |
| refresh_token | text | 刷新令牌 |
| expires_at | timestamp | 令牌过期时间 |
### 4. 用户会话和令牌管理
#### `user_sessions` - 用户会话表
管理用户登录会话。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| session_id | varchar(255) | 会话ID唯一 |
| ip_address | varchar(45) | IP地址 |
| user_agent | text | 用户代理 |
| expires_at | timestamp | 过期时间 |
| is_active | boolean | 是否激活 |
| last_activity | timestamp | 最后活动时间 |
#### `user_tokens` - 用户令牌表
管理用户的API令牌等。
| 字段 | 类型 | 说明 |
|-----|------|------|
| id | varchar(32) | 主键ID |
| user_id | varchar(32) | 用户ID |
| token_type | varchar(50) | 令牌类型 |
| token | varchar(500) | 令牌值(唯一) |
| name | varchar(100) | 令牌名称 |
| description | text | 令牌描述 |
| scope | text | 授权范围 |
| expires_at | timestamp | 过期时间 |
| last_used_at | timestamp | 最后使用时间 |
| is_active | boolean | 是否激活 |
## 关系说明
### 用户与OAuth的关系
- 一个用户可以拥有多个OAuth客户端`users.id` -> `oauth_clients.owner_id`
- 一个用户可以授权多个客户端(`users.id` -> `oauth_user_consents.user_id`
- 一个用户可以关联多个第三方账户(`users.id` -> `oauth_accounts.user_id`
### OAuth授权流程关系
- 授权码关联客户端和用户(`oauth_authorization_codes.client_id` -> `oauth_clients.id`
- 访问令牌关联刷新令牌(`oauth_refresh_tokens.access_token_id` -> `oauth_access_tokens.id`
### 权限控制
- 基于现有的RBAC系统为OAuth相关操作定义权限
- 管理员可以管理所有OAuth客户端
- 普通用户只能管理自己的OAuth客户端
## 预定义数据
### 默认授权范围
- `profile`: 基本资料访问
- `email`: 邮箱地址访问
- `phone`: 手机号码访问
- `read`: 读取权限
- `write`: 写入权限
- `admin`: 管理员权限
### 预配置的第三方提供商
- GitHub
- Google
- 微信
- 钉钉
### OAuth相关权限
- `oauth.client.create`: 创建OAuth客户端
- `oauth.client.read`: 查看OAuth客户端
- `oauth.client.update`: 更新OAuth客户端
- `oauth.client.delete`: 删除OAuth客户端
- `oauth.token.manage`: 管理OAuth令牌
- `oauth.scope.manage`: 管理OAuth作用域
- `oauth.provider.manage`: 管理OAuth提供商
## 安全考虑
1. **令牌存储**: 所有敏感令牌都应该进行哈希或加密存储
2. **HTTPS强制**: 所有OAuth端点必须使用HTTPS
3. **PKCE支持**: 支持PKCE以增强安全性
4. **令牌过期**: 设置合理的令牌过期时间
5. **审计日志**: 记录所有OAuth相关操作
## 使用示例
### 初始化OAuth数据
```go
import "vyes_cli/oauth"
// 在数据库迁移后调用
err := oauth.InitializeOAuthData(db)
if err != nil {
log.Fatal("Failed to initialize OAuth data:", err)
}
```
### 创建测试客户端
```go
client, err := oauth.CreateDefaultOAuthClient(db, adminUserID)
if err != nil {
log.Fatal("Failed to create default OAuth client:", err)
}
```
这个设计基于OAuth 2.0 RFC标准同时考虑了您现有的用户管理系统架构可以无缝集成到您的项目中。

@ -1,127 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
"gorm.io/gorm"
)
// AuthorizeRequest 授权请求参数
type AuthorizeRequest struct {
ResponseType string `json:"response_type" src:"query" desc:"响应类型"`
ClientID string `json:"client_id" src:"query" desc:"客户端ID"`
RedirectURI string `json:"redirect_uri" src:"query" desc:"重定向URI"`
Scope string `json:"scope" src:"query" desc:"授权范围"`
State string `json:"state" src:"query" desc:"状态"`
CodeChallenge string `json:"code_challenge" src:"query" desc:"代码挑战"`
CodeChallengeMethod string `json:"code_challenge_method" src:"query" desc:"代码挑战方法"`
}
// AuthorizeResponse 授权响应
type AuthorizeResponse struct {
Code string `json:"code,omitempty"`
State string `json:"state,omitempty"`
RedirectURI string `json:"redirect_uri"`
Error string `json:"error,omitempty"`
ErrorDesc string `json:"error_description,omitempty"`
}
// handleAuthorize 处理OAuth授权请求
func handleAuthorize(x *vigo.X, args *AuthorizeRequest) (*AuthorizeResponse, error) {
db := cfg.DB()
// 1. 验证响应类型
if args.ResponseType != ResponseTypeCode {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorUnsupportedResponseType, "不支持的响应类型", args.State)
return &AuthorizeResponse{
Error: "unsupported_response_type",
ErrorDesc: "不支持的响应类型",
RedirectURI: errorURI,
}, nil
}
// 2. 验证客户端
var client OAuthClient
if err := db.Where("client_id = ? AND is_active = ?", args.ClientID, true).First(&client).Error; err != nil {
if err == gorm.ErrRecordNotFound {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorInvalidClient, "无效的客户端", args.State)
return &AuthorizeResponse{
Error: "invalid_client",
ErrorDesc: "无效的客户端",
RedirectURI: errorURI,
}, nil
}
return nil, vigo.NewError("数据库查询失败").WithError(err).WithCode(500)
}
// 3. 验证重定向URI
if !client.IsRedirectURIValid(args.RedirectURI) {
return nil, vigo.NewError("无效的重定向URI").WithCode(400)
}
// 4. 验证作用域
if args.Scope != "" && !client.HasScope(args.Scope) {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorInvalidScope, "无效的授权范围", args.State)
return &AuthorizeResponse{
Error: "invalid_scope",
ErrorDesc: "无效的授权范围",
RedirectURI: errorURI,
}, nil
}
// TODO: 在实际应用中,这里应该:
// 1. 检查用户是否已登录
// 2. 显示授权同意页面
// 3. 用户同意后生成授权码
// 为了演示,这里假设用户已登录且同意授权
// 假设当前用户ID (实际应从session or JWT token中获取)
userID := "demo-user-id"
// 5. 生成授权码
code, err := generateRandomString(32)
if err != nil {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorServerError, "授权码生成失败", args.State)
return &AuthorizeResponse{
Error: "server_error",
ErrorDesc: "授权码生成失败",
RedirectURI: errorURI,
}, nil
}
// 6. 保存授权码
authCode := &OAuthAuthorizationCode{
Code: code,
ClientID: client.ID,
UserID: userID,
RedirectURI: args.RedirectURI,
Scope: args.Scope,
ExpiresAt: time.Now().Add(10 * time.Minute), // 授权码10分钟有效
CodeChallenge: args.CodeChallenge,
CodeChallengeMethod: args.CodeChallengeMethod,
}
if err := db.Create(authCode).Error; err != nil {
errorURI := BuildErrorRedirectURI(args.RedirectURI, ErrorServerError, "授权码保存失败", args.State)
return &AuthorizeResponse{
Error: "server_error",
ErrorDesc: "授权码保存失败",
RedirectURI: errorURI,
}, nil
}
// 7. 返回授权码重定向
return &AuthorizeResponse{
Code: code,
State: args.State,
RedirectURI: BuildRedirectURI(args.RedirectURI, code, args.State),
}, nil
}

@ -1,159 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import "time"
// OAuth 2.0 相关常量
const (
// Grant Types
GrantTypeAuthorizationCode = "authorization_code"
GrantTypeRefreshToken = "refresh_token"
GrantTypeClientCredentials = "client_credentials"
GrantTypePassword = "password"
GrantTypeImplicit = "implicit"
// Response Types
ResponseTypeCode = "code"
ResponseTypeToken = "token"
// Token Types
TokenTypeBearer = "Bearer"
// PKCE Challenge Methods
CodeChallengeMethodPlain = "plain"
CodeChallengeMethodS256 = "S256"
// Default Scopes
ScopeRead = "read"
ScopeWrite = "write"
ScopeProfile = "profile"
ScopeEmail = "email"
ScopePhone = "phone"
ScopeAdmin = "admin"
// Token 生存时间
DefaultAuthorizationCodeExpiry = 10 * time.Minute // 授权码10分钟过期
DefaultAccessTokenExpiry = 1 * time.Hour // 访问令牌1小时过期
DefaultRefreshTokenExpiry = 30 * 24 * time.Hour // 刷新令牌30天过期
DefaultSessionExpiry = 24 * time.Hour // 会话24小时过期
// Error Codes (RFC 6749)
ErrorInvalidRequest = "invalid_request"
ErrorInvalidClient = "invalid_client"
ErrorInvalidGrant = "invalid_grant"
ErrorUnauthorizedClient = "unauthorized_client"
ErrorUnsupportedGrantType = "unsupported_grant_type"
ErrorInvalidScope = "invalid_scope"
ErrorAccessDenied = "access_denied"
ErrorUnsupportedResponseType = "unsupported_response_type"
ErrorServerError = "server_error"
ErrorTemporarilyUnavailable = "temporarily_unavailable"
// PKCE Error Codes (RFC 7636)
ErrorInvalidGrant2 = "invalid_grant"
// Token 类型
UserTokenTypeAPI = "api" // API 令牌
UserTokenTypeSession = "session" // 会话令牌
UserTokenTypePersonal = "personal" // 个人访问令牌
)
// 默认作用域定义
var DefaultScopes = []struct {
Name string
DisplayName string
Description string
IsDefault bool
IsSystem bool
}{
{
Name: ScopeProfile,
DisplayName: "基本资料",
Description: "访问您的基本资料信息,如用户名、昵称等",
IsDefault: true,
IsSystem: true,
},
{
Name: ScopeEmail,
DisplayName: "邮箱地址",
Description: "访问您的邮箱地址",
IsDefault: false,
IsSystem: true,
},
{
Name: ScopePhone,
DisplayName: "手机号码",
Description: "访问您的手机号码",
IsDefault: false,
IsSystem: true,
},
{
Name: ScopeRead,
DisplayName: "读取权限",
Description: "读取您的数据",
IsDefault: true,
IsSystem: false,
},
{
Name: ScopeWrite,
DisplayName: "写入权限",
Description: "修改您的数据",
IsDefault: false,
IsSystem: false,
},
{
Name: ScopeAdmin,
DisplayName: "管理员权限",
Description: "完全的管理员权限",
IsDefault: false,
IsSystem: true,
},
}
// 预定义的第三方OAuth提供商
var DefaultOAuthProviders = []struct {
Name string
DisplayName string
AuthURL string
TokenURL string
UserInfoURL string
Scope string
}{
{
Name: "github",
DisplayName: "GitHub",
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
UserInfoURL: "https://api.github.com/user",
Scope: "user:email",
},
{
Name: "google",
DisplayName: "Google",
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
UserInfoURL: "https://www.googleapis.com/oauth2/v2/userinfo",
Scope: "openid profile email",
},
{
Name: "wechat",
DisplayName: "微信",
AuthURL: "https://open.weixin.qq.com/connect/oauth2/authorize",
TokenURL: "https://api.weixin.qq.com/sns/oauth2/access_token",
UserInfoURL: "https://api.weixin.qq.com/sns/userinfo",
Scope: "snsapi_userinfo",
},
{
Name: "dingtalk",
DisplayName: "钉钉",
AuthURL: "https://oapi.dingtalk.com/connect/oauth2/sns_authorize",
TokenURL: "https://oapi.dingtalk.com/sns/gettoken",
UserInfoURL: "https://oapi.dingtalk.com/sns/getuserinfo",
Scope: "snsapi_login",
},
}

@ -1,210 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"github.com/vyes-ai/vigo"
"gorm.io/gorm"
)
var Router = vigo.NewRouter()
func init() {
// OAuth 授权端点
var _ = Router.Get("/authorize", "OAuth授权端点 - 获取授权码", vigo.SkipBefore, handleAuthorize)
// OAuth 令牌端点
var _ = Router.Post("/token", "OAuth令牌端点 - 用授权码换取令牌或刷新令牌", vigo.SkipBefore, handleToken)
// OAuth 撤销端点
var _ = Router.Post("/revoke", "OAuth撤销端点 - 撤销访问令牌或刷新令牌", vigo.SkipBefore, handleRevoke)
}
// InitializeOAuthData 初始化OAuth相关的基础数据
func InitializeOAuthData(db *gorm.DB) error {
// 1. 创建默认的OAuth作用域
if err := createDefaultScopes(db); err != nil {
return err
}
// 2. 创建默认的第三方OAuth提供商
if err := createDefaultProviders(db); err != nil {
return err
}
// 3. 创建默认权限
if err := createDefaultPermissions(db); err != nil {
return err
}
return nil
}
func createDefaultScopes(db *gorm.DB) error {
for _, scopeData := range DefaultScopes {
scope := &OAuthScope{
Name: scopeData.Name,
DisplayName: scopeData.DisplayName,
Description: scopeData.Description,
IsDefault: scopeData.IsDefault,
IsSystem: scopeData.IsSystem,
}
// 如果不存在则创建
var existingScope OAuthScope
result := db.Where("name = ?", scope.Name).First(&existingScope)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(scope).Error; err != nil {
return err
}
}
}
return nil
}
func createDefaultProviders(db *gorm.DB) error {
for _, providerData := range DefaultOAuthProviders {
provider := &OAuthProvider{
Name: providerData.Name,
DisplayName: providerData.DisplayName,
AuthURL: providerData.AuthURL,
TokenURL: providerData.TokenURL,
UserInfoURL: providerData.UserInfoURL,
Scope: providerData.Scope,
IsActive: false, // 默认不激活需要配置ClientID和ClientSecret后激活
}
// 如果不存在则创建
var existingProvider OAuthProvider
result := db.Where("name = ?", provider.Name).First(&existingProvider)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(provider).Error; err != nil {
return err
}
}
}
return nil
}
func createDefaultPermissions(db *gorm.DB) error {
oauthPermissions := []struct {
Name string
DisplayName string
Description string
Resource string
Action string
IsSystem bool
}{
{
Name: "oauth.client.create",
DisplayName: "创建OAuth客户端",
Description: "允许创建新的OAuth客户端应用",
Resource: "oauth_client",
Action: "create",
IsSystem: true,
},
{
Name: "oauth.client.read",
DisplayName: "查看OAuth客户端",
Description: "允许查看OAuth客户端信息",
Resource: "oauth_client",
Action: "read",
IsSystem: true,
},
{
Name: "oauth.client.update",
DisplayName: "更新OAuth客户端",
Description: "允许更新OAuth客户端信息",
Resource: "oauth_client",
Action: "update",
IsSystem: true,
},
{
Name: "oauth.client.delete",
DisplayName: "删除OAuth客户端",
Description: "允许删除OAuth客户端",
Resource: "oauth_client",
Action: "delete",
IsSystem: true,
},
{
Name: "oauth.token.manage",
DisplayName: "管理OAuth令牌",
Description: "允许管理用户的OAuth令牌",
Resource: "oauth_token",
Action: "manage",
IsSystem: true,
},
{
Name: "oauth.scope.manage",
DisplayName: "管理OAuth作用域",
Description: "允许管理OAuth作用域",
Resource: "oauth_scope",
Action: "manage",
IsSystem: true,
},
{
Name: "oauth.provider.manage",
DisplayName: "管理OAuth提供商",
Description: "允许管理第三方OAuth提供商",
Resource: "oauth_provider",
Action: "manage",
IsSystem: true,
},
}
for _, permData := range oauthPermissions {
permission := &Permission{
Name: permData.Name,
DisplayName: permData.DisplayName,
Description: permData.Description,
Resource: permData.Resource,
Action: permData.Action,
IsSystem: permData.IsSystem,
}
// 如果不存在则创建
var existingPerm Permission
result := db.Where("name = ?", permission.Name).First(&existingPerm)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(permission).Error; err != nil {
return err
}
}
}
return nil
}
// CreateDefaultOAuthClient 创建默认的OAuth客户端用于测试
func CreateDefaultOAuthClient(db *gorm.DB, ownerID string) (*OAuthClient, error) {
client := &OAuthClient{
ClientID: "default-client-id",
ClientSecret: "default-client-secret", // 实际使用时应该使用加密存储
ClientName: "Default Test Client",
ClientURI: "http://localhost:3000",
RedirectURIs: `["http://localhost:3000/callback", "http://localhost:8080/callback"]`,
ResponseTypes: "code",
GrantTypes: "authorization_code,refresh_token",
Scope: "profile read write",
IsPublic: false,
IsActive: true,
OwnerID: ownerID,
}
// 检查是否已存在
var existingClient OAuthClient
result := db.Where("client_id = ?", client.ClientID).First(&existingClient)
if result.Error == gorm.ErrRecordNotFound {
if err := db.Create(client).Error; err != nil {
return nil, err
}
return client, nil
}
return &existingClient, nil
}

@ -1,60 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
)
// RevokeRequest 撤销令牌请求参数
type RevokeRequest struct {
Token string `src:"form" desc:"令牌" binding:"required"`
TokenTypeHint string `src:"form" desc:"令牌类型提示"` // access_token 或 refresh_token
ClientID string `src:"form" desc:"客户端ID"`
ClientSecret string `src:"form" desc:"客户端密钥"`
}
// RevokeResponse 撤销令牌响应
type RevokeResponse struct {
Message string `json:"message"`
Success bool `json:"success"`
}
// handleRevoke 处理OAuth撤销请求
func handleRevoke(x *vigo.X, args *RevokeRequest) (*RevokeResponse, error) {
if args.Token == "" {
return nil, vigo.NewError("令牌不能为空").WithCode(400)
}
db := cfg.DB()
// 根据OAuth 2.0规范,撤销令牌应该是幂等操作
// 即使令牌不存在也应该返回成功,这是为了防止信息泄露
var revoked = false
// 尝试撤销访问令牌
result := db.Model(&OAuthAccessToken{}).Where("token = ?", args.Token).Update("revoked", true)
if result.Error == nil && result.RowsAffected > 0 {
revoked = true
}
// 如果没有找到访问令牌,尝试撤销刷新令牌
if !revoked {
result = db.Model(&OAuthRefreshToken{}).Where("token = ?", args.Token).Update("revoked", true)
if result.Error == nil && result.RowsAffected > 0 {
revoked = true
}
}
// 根据OAuth 2.0规范,即使令牌不存在也返回成功
// 这样可以防止攻击者通过响应差异推断令牌是否存在
return &RevokeResponse{
Message: "令牌撤销成功",
Success: true,
}, nil
}

@ -1,290 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/veypi/OneAuth/cfg"
"github.com/vyes-ai/vigo"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// TokenRequest 令牌请求参数
type TokenRequest struct {
GrantType string `json:"grant_type" src:"form" desc:"授权类型"`
Code string `json:"code" src:"form" desc:"授权码"`
RedirectURI string `json:"redirect_uri" src:"form" desc:"重定向URI"`
ClientID string `json:"client_id" src:"form" desc:"客户端ID"`
ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"`
RefreshToken string `json:"refresh_token" src:"form" desc:"刷新令牌"`
CodeVerifier string `json:"code_verifier" src:"form" desc:"PKCE验证码"`
Username string `json:"username" src:"form" desc:"用户名"` // for password grant
Password string `json:"password" src:"form" desc:"密码"` // for password grant
Scope string `json:"scope" src:"form" desc:"权限范围"` // for password grant
}
// TokenResponse 令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// handleToken 处理OAuth令牌请求
func handleToken(x *vigo.X, args *TokenRequest) (*TokenResponse, error) {
db := cfg.DB()
switch args.GrantType {
case GrantTypeAuthorizationCode:
return handleAuthorizationCodeGrant(db, x, args)
case GrantTypeRefreshToken:
return handleRefreshTokenGrant(db, x, args)
case GrantTypePassword:
return handlePasswordGrant(db, x, args)
default:
return nil, vigo.NewError("不支持的授权类型").WithCode(400)
}
}
// handleAuthorizationCodeGrant 处理授权码授权类型
func handleAuthorizationCodeGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) (*TokenResponse, error) {
// 1. 验证授权码
var authCode OAuthAuthorizationCode
if err := db.Where("code = ? AND used = ?", args.Code, false).First(&authCode).Error; err != nil {
return nil, vigo.NewError("无效的授权码").WithCode(400)
}
// 2. 检查授权码是否过期
if authCode.IsExpired() {
return nil, vigo.NewError("授权码已过期").WithCode(400)
}
// 3. 验证客户端
var client OAuthClient
if err := db.Where("id = ? AND client_id = ?", authCode.ClientID, args.ClientID).First(&client).Error; err != nil {
return nil, vigo.NewError("无效的客户端").WithCode(400)
}
// 4. 验证客户端密钥(对于机密客户端)
if !client.IsPublic && client.ClientSecret != args.ClientSecret {
return nil, vigo.NewError("无效的客户端凭据").WithCode(400)
}
// 5. 验证重定向URI
if authCode.RedirectURI != args.RedirectURI {
return nil, vigo.NewError("重定向URI不匹配").WithCode(400)
}
// 6. 验证PKCE如果使用
if authCode.CodeChallenge != "" {
if err := validatePKCE(authCode.CodeChallenge, authCode.CodeChallengeMethod, args.CodeVerifier); err != nil {
return nil, vigo.NewError("PKCE验证失败").WithError(err).WithCode(400)
}
}
// 7. 标记授权码为已使用
if err := db.Model(&authCode).Update("used", true).Error; err != nil {
return nil, vigo.NewError("授权码更新失败").WithError(err).WithCode(500)
}
// 8. 生成访问令牌
accessToken, err := generateAccessToken(db, &client, authCode.UserID, authCode.Scope)
if err != nil {
return nil, vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 9. 生成刷新令牌
refreshToken, err := generateRefreshToken(db, accessToken, &client, authCode.UserID, authCode.Scope)
if err != nil {
return nil, vigo.NewError("刷新令牌生成失败").WithError(err).WithCode(500)
}
return &TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: authCode.Scope,
}, nil
}
// handleRefreshTokenGrant 处理刷新令牌授权类型
func handleRefreshTokenGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) (*TokenResponse, error) {
// 1. 验证刷新令牌
var refreshToken OAuthRefreshToken
if err := db.Where("token = ? AND revoked = ?", args.RefreshToken, false).First(&refreshToken).Error; err != nil {
return nil, vigo.NewError("无效的刷新令牌").WithCode(400)
}
// 2. 检查刷新令牌是否过期
if refreshToken.IsExpired() {
return nil, vigo.NewError("刷新令牌已过期").WithCode(400)
}
// 3. 验证客户端
var client OAuthClient
if err := db.Where("id = ? AND client_id = ?", refreshToken.ClientID, args.ClientID).First(&client).Error; err != nil {
return nil, vigo.NewError("无效的客户端").WithCode(400)
}
// 4. 撤销旧的访问令牌
if err := db.Model(&OAuthAccessToken{}).Where("id = ?", refreshToken.AccessTokenID).Update("revoked", true).Error; err != nil {
return nil, vigo.NewError("旧令牌撤销失败").WithError(err).WithCode(500)
}
// 5. 生成新的访问令牌
accessToken, err := generateAccessToken(db, &client, refreshToken.UserID, refreshToken.Scope)
if err != nil {
return nil, vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 6. 更新刷新令牌关联
if err := db.Model(&refreshToken).Update("access_token_id", accessToken.ID).Error; err != nil {
return nil, vigo.NewError("刷新令牌更新失败").WithError(err).WithCode(500)
}
return &TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: refreshToken.Scope,
}, nil
}
// handlePasswordGrant 处理密码授权类型
func handlePasswordGrant(db *gorm.DB, x *vigo.X, args *TokenRequest) (*TokenResponse, error) {
// 1. 验证必要参数
if args.Username == "" || args.Password == "" {
return nil, vigo.NewError("用户名和密码不能为空").WithCode(400)
}
// 2. 验证客户端
var client OAuthClient
if err := db.Where("client_id = ?", args.ClientID).First(&client).Error; err != nil {
return nil, vigo.NewError("无效的客户端").WithCode(400)
}
// 3. 验证客户端密钥(对于机密客户端)
if !client.IsPublic && client.ClientSecret != args.ClientSecret {
return nil, vigo.NewError("无效的客户端凭据").WithCode(400)
}
// 4. 验证用户凭据
var user User
if err := db.Where("username = ?", args.Username).First(&user).Error; err != nil {
return nil, vigo.NewError("用户名或密码错误").WithCode(400)
}
// 5. 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(args.Password)); err != nil {
return nil, vigo.NewError("用户名或密码错误").WithCode(400)
}
// 6. 处理权限范围
scope := args.Scope
if scope == "" {
// scope = DefaultScope // 默认权限范围
}
// 7. 生成访问令牌
accessToken, err := generateAccessToken(db, &client, user.ID, scope)
if err != nil {
return nil, vigo.NewError("访问令牌生成失败").WithError(err).WithCode(500)
}
// 8. 生成刷新令牌
refreshToken, err := generateRefreshToken(db, accessToken, &client, user.ID, scope)
if err != nil {
return nil, vigo.NewError("刷新令牌生成失败").WithError(err).WithCode(500)
}
return &TokenResponse{
AccessToken: accessToken.Token,
TokenType: TokenTypeBearer,
ExpiresIn: int64(DefaultAccessTokenExpiry.Seconds()),
RefreshToken: refreshToken.Token,
Scope: scope,
}, nil
}
// 辅助函数
func generateAccessToken(db *gorm.DB, client *OAuthClient, userID, scope string) (*OAuthAccessToken, error) {
token, err := generateRandomString(64)
if err != nil {
return nil, err
}
accessToken := &OAuthAccessToken{
Token: token,
ClientID: client.ID,
UserID: userID,
Scope: scope,
ExpiresAt: time.Now().Add(DefaultAccessTokenExpiry),
Revoked: false,
}
if err := db.Create(accessToken).Error; err != nil {
return nil, err
}
return accessToken, nil
}
func generateRefreshToken(db *gorm.DB, accessToken *OAuthAccessToken, client *OAuthClient, userID, scope string) (*OAuthRefreshToken, error) {
token, err := generateRandomString(64)
if err != nil {
return nil, err
}
refreshToken := &OAuthRefreshToken{
Token: token,
AccessTokenID: accessToken.ID,
ClientID: client.ID,
UserID: userID,
Scope: scope,
ExpiresAt: time.Now().Add(DefaultRefreshTokenExpiry),
Revoked: false,
}
if err := db.Create(refreshToken).Error; err != nil {
return nil, err
}
return refreshToken, nil
}
func validatePKCE(codeChallenge, method, codeVerifier string) error {
if codeVerifier == "" {
return fmt.Errorf("code verifier required")
}
switch method {
case CodeChallengeMethodPlain:
if codeChallenge != codeVerifier {
return fmt.Errorf("invalid code verifier")
}
case CodeChallengeMethodS256:
h := sha256.Sum256([]byte(codeVerifier))
expected := base64.RawURLEncoding.EncodeToString(h[:])
if codeChallenge != expected {
return fmt.Errorf("invalid code verifier")
}
default:
return fmt.Errorf("unsupported code challenge method")
}
return nil
}

@ -1,397 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"time"
"github.com/vyes-ai/vigo"
"gorm.io/gorm"
)
// User 用户表
type User struct {
vigo.Model
Username string `json:"username" gorm:"uniqueIndex;not null;size:50;comment:用户名""`
Email string `json:"email" gorm:"uniqueIndex;size:100;comment:邮箱地址"`
Phone string `json:"phone" gorm:"uniqueIndex;size:20;comment:手机号码"`
PasswordHash string `json:"-" gorm:"not null;size:255;comment:密码哈希"`
Nickname string `json:"nickname" gorm:"size:50;comment:昵称"`
Avatar string `json:"avatar" gorm:"size:255;comment:头像URL"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
IsSuperuser bool `json:"is_superuser" gorm:"default:false;comment:是否为超级用户"`
LastLoginAt *time.Time `json:"last_login_at" gorm:"comment:最后登录时间"`
EmailVerified bool `json:"email_verified" gorm:"default:false;comment:邮箱是否已验证"`
PhoneVerified bool `json:"phone_verified" gorm:"default:false;comment:手机是否已验证"`
TwoFactorAuth bool `json:"two_factor_auth" gorm:"default:false;comment:是否启用双因素认证"`
Locale string `json:"locale" gorm:"size:10;default:zh-CN;comment:语言偏好"`
Timezone string `json:"timezone" gorm:"size:50;default:Asia/Shanghai;comment:时区"`
Bio string `json:"bio" gorm:"type:text;comment:个人简介"`
// 关联关系
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
UserRoles []UserRole `json:"-"`
OAuthAccounts []OAuthAccount `json:"oauth_accounts"`
Tokens []UserToken `json:"-"`
MetaData []byte `json:"meta_data" gorm:"type:jsonb;comment:用户元数据"` // 存储用户自定义的元数据
}
// Role 角色表
type Role struct {
vigo.Model
Name string `json:"name" gorm:"uniqueIndex;not null;size:50;comment:角色名称" validate:"required"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:角色描述"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统角色"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
Users []User `json:"-" gorm:"many2many:user_roles;"`
UserRoles []UserRole `json:"-"`
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}
// Permission 权限表
type Permission struct {
vigo.Model
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:权限名称" validate:"required"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:权限描述"`
Resource string `json:"resource" gorm:"not null;size:50;comment:资源名称" validate:"required"`
Action string `json:"action" gorm:"not null;size:50;comment:操作类型" validate:"required"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统权限"`
// 关联关系
Roles []Role `json:"-" gorm:"many2many:role_permissions;"`
}
// UserRole 用户角色关联表
type UserRole struct {
UserID string `json:"user_id" gorm:"primaryKey;type:varchar(32);comment:用户ID"`
RoleID string `json:"role_id" gorm:"primaryKey;type:varchar(32);comment:角色ID"`
GrantedBy string `json:"granted_by" gorm:"type:varchar(32);comment:授权人ID"`
GrantedAt time.Time `json:"granted_at" gorm:"autoCreateTime;comment:授权时间"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Role *Role `json:"role" gorm:"foreignKey:RoleID"`
GrantedByUser *User `json:"granted_by_user" gorm:"foreignKey:GrantedBy"`
}
// RolePermission 角色权限关联表
type RolePermission struct {
RoleID string `json:"role_id" gorm:"primaryKey;type:varchar(32);comment:角色ID"`
PermissionID string `json:"permission_id" gorm:"primaryKey;type:varchar(32);comment:权限ID"`
GrantedBy string `json:"granted_by" gorm:"type:varchar(32);comment:授权人ID"`
GrantedAt time.Time `json:"granted_at" gorm:"autoCreateTime;comment:授权时间"`
// 关联关系
Role *Role `json:"role" gorm:"foreignKey:RoleID"`
Permission *Permission `json:"permission" gorm:"foreignKey:PermissionID"`
GrantedByUser *User `json:"granted_by_user" gorm:"foreignKey:GrantedBy"`
}
// UserLoginLog 用户登录日志表
type UserLoginLog struct {
vigo.Model
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
IPAddress string `json:"ip_address" gorm:"size:45;comment:IP地址"`
UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"`
LoginAt time.Time `json:"login_at" gorm:"autoCreateTime;comment:登录时间"`
Success bool `json:"success" gorm:"default:true;comment:是否成功"`
FailReason string `json:"fail_reason" gorm:"size:255;comment:失败原因"`
Location string `json:"location" gorm:"size:100;comment:地理位置"` // 地理位置
// 关联关系
User User `json:"user" gorm:"foreignKey:UserID"`
}
// GORM Hooks
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Locale == "" {
u.Locale = "zh-CN"
}
if u.Timezone == "" {
u.Timezone = "Asia/Shanghai"
}
return nil
}
// 用户方法
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
if role.Name == roleName {
return true
}
}
return false
}
func (u *User) HasPermission(resource, action string) bool {
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if permission.Resource == resource && permission.Action == action {
return true
}
}
}
return false
}
func (u *User) GetPermissions() []Permission {
var permissions []Permission
permissionMap := make(map[string]bool)
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if !permissionMap[permission.ID] {
permissions = append(permissions, permission)
permissionMap[permission.ID] = true
}
}
}
return permissions
}
// 角色方法
func (r *Role) HasPermission(resource, action string) bool {
for _, permission := range r.Permissions {
if permission.Resource == resource && permission.Action == action {
return true
}
}
return false
}
// ===== OAuth 服务器相关模型 =====
// OAuthClient OAuth客户端表
type OAuthClient struct {
vigo.Model
ClientID string `json:"client_id" gorm:"uniqueIndex;not null;size:255;comment:客户端ID"`
ClientSecret string `json:"-" gorm:"not null;size:255;comment:客户端密钥"`
ClientName string `json:"client_name" gorm:"not null;size:255;comment:客户端名称"`
ClientURI string `json:"client_uri" gorm:"size:500;comment:客户端主页"`
LogoURI string `json:"logo_uri" gorm:"size:500;comment:客户端Logo"`
TermsOfServiceURI string `json:"tos_uri" gorm:"size:500;comment:服务条款URL"`
PolicyURI string `json:"policy_uri" gorm:"size:500;comment:隐私政策URL"`
RedirectURIs string `json:"redirect_uris" gorm:"type:text;comment:重定向URI列表,JSON格式"`
ResponseTypes string `json:"response_types" gorm:"size:255;default:'code';comment:响应类型"`
GrantTypes string `json:"grant_types" gorm:"size:255;default:'authorization_code,refresh_token';comment:授权类型"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
Contacts string `json:"contacts" gorm:"type:text;comment:联系人邮箱,JSON格式"`
IsPublic bool `json:"is_public" gorm:"default:false;comment:是否为公开客户端"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
OwnerID string `json:"owner_id" gorm:"type:varchar(32);comment:客户端拥有者ID"`
// 关联关系
Owner *User `json:"owner" gorm:"foreignKey:OwnerID"`
AuthorizationCodes []OAuthAuthorizationCode `json:"-"`
AccessTokens []OAuthAccessToken `json:"-"`
RefreshTokens []OAuthRefreshToken `json:"-"`
}
// OAuthAuthorizationCode 授权码表
type OAuthAuthorizationCode struct {
vigo.Model
Code string `json:"code" gorm:"uniqueIndex;not null;size:255;comment:授权码"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
RedirectURI string `json:"redirect_uri" gorm:"not null;size:500;comment:重定向URI"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
CodeChallenge string `json:"code_challenge" gorm:"size:255;comment:PKCE代码挑战"`
CodeChallengeMethod string `json:"code_challenge_method" gorm:"size:50;comment:PKCE挑战方法"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Used bool `json:"used" gorm:"default:false;comment:是否已使用"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// OAuthAccessToken 访问令牌表
type OAuthAccessToken struct {
vigo.Model
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:访问令牌"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Revoked bool `json:"revoked" gorm:"default:false;comment:是否已撤销"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
RefreshToken *OAuthRefreshToken `json:"refresh_token"`
}
// OAuthRefreshToken 刷新令牌表
type OAuthRefreshToken struct {
vigo.Model
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:刷新令牌"`
AccessTokenID string `json:"access_token_id" gorm:"type:varchar(32);uniqueIndex;comment:访问令牌ID"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
Revoked bool `json:"revoked" gorm:"default:false;comment:是否已撤销"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
User *User `json:"user" gorm:"foreignKey:UserID"`
AccessToken *OAuthAccessToken `json:"access_token" gorm:"foreignKey:AccessTokenID"`
}
// OAuthScope OAuth授权范围表
type OAuthScope struct {
vigo.Model
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:范围名称"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
Description string `json:"description" gorm:"type:text;comment:范围描述"`
IsDefault bool `json:"is_default" gorm:"default:false;comment:是否为默认范围"`
IsSystem bool `json:"is_system" gorm:"default:false;comment:是否为系统范围"`
}
// OAuthClientScope 客户端授权范围关联表
type OAuthClientScope struct {
ClientID string `json:"client_id" gorm:"primaryKey;type:varchar(32);comment:客户端ID"`
ScopeID string `json:"scope_id" gorm:"primaryKey;type:varchar(32);comment:范围ID"`
// 关联关系
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
Scope *OAuthScope `json:"scope" gorm:"foreignKey:ScopeID"`
}
// OAuthProvider 第三方OAuth提供商表用于OAuth客户端模式
type OAuthProvider struct {
vigo.Model
Name string `json:"name" gorm:"uniqueIndex;not null;size:100;comment:提供商名称"`
DisplayName string `json:"display_name" gorm:"size:100;comment:显示名称"`
ClientID string `json:"client_id" gorm:"not null;size:255;comment:客户端ID"`
ClientSecret string `json:"-" gorm:"not null;size:255;comment:客户端密钥"`
AuthURL string `json:"auth_url" gorm:"not null;size:500;comment:授权URL"`
TokenURL string `json:"token_url" gorm:"not null;size:500;comment:令牌URL"`
UserInfoURL string `json:"user_info_url" gorm:"size:500;comment:用户信息URL"`
Scope string `json:"scope" gorm:"type:text;comment:默认授权范围"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
OAuthAccounts []OAuthAccount `json:"oauth_accounts"`
}
// OAuthAccount 用户OAuth账户表第三方登录
type OAuthAccount struct {
vigo.Model
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
ProviderID string `json:"provider_id" gorm:"not null;type:varchar(32);index;comment:提供商ID"`
ProviderUserID string `json:"provider_user_id" gorm:"not null;size:255;comment:提供商用户ID"`
Email string `json:"email" gorm:"size:255;comment:邮箱"`
Username string `json:"username" gorm:"size:255;comment:用户名"`
Nickname string `json:"nickname" gorm:"size:255;comment:昵称"`
Avatar string `json:"avatar" gorm:"size:500;comment:头像URL"`
AccessToken string `json:"-" gorm:"type:text;comment:访问令牌"`
RefreshToken string `json:"-" gorm:"type:text;comment:刷新令牌"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:令牌过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Provider *OAuthProvider `json:"provider" gorm:"foreignKey:ProviderID"`
}
// UserToken 用户令牌表API令牌等
type UserToken struct {
vigo.Model
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
TokenType string `json:"token_type" gorm:"not null;size:50;comment:令牌类型"` // api, session, etc.
Token string `json:"token" gorm:"uniqueIndex;not null;size:500;comment:令牌值"`
Name string `json:"name" gorm:"size:100;comment:令牌名称"`
Description string `json:"description" gorm:"type:text;comment:令牌描述"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:过期时间"`
LastUsedAt *time.Time `json:"last_used_at" gorm:"comment:最后使用时间"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// UserSession 用户会话表
type UserSession struct {
vigo.Model
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
SessionID string `json:"session_id" gorm:"uniqueIndex;not null;size:255;comment:会话ID"`
IPAddress string `json:"ip_address" gorm:"size:45;comment:IP地址"`
UserAgent string `json:"user_agent" gorm:"type:text;comment:用户代理"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;comment:过期时间"`
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
LastActivity time.Time `json:"last_activity" gorm:"comment:最后活动时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
}
// OAuthUserConsent 用户授权同意表
type OAuthUserConsent struct {
vigo.Model
UserID string `json:"user_id" gorm:"not null;type:varchar(32);index;comment:用户ID"`
ClientID string `json:"client_id" gorm:"not null;type:varchar(32);index;comment:客户端ID"`
Scope string `json:"scope" gorm:"type:text;comment:授权范围"`
ConsentAt time.Time `json:"consent_at" gorm:"autoCreateTime;comment:同意时间"`
ExpiresAt *time.Time `json:"expires_at" gorm:"comment:同意过期时间"`
// 关联关系
User *User `json:"user" gorm:"foreignKey:UserID"`
Client *OAuthClient `json:"client" gorm:"foreignKey:ClientID"`
}
// ===== OAuth 模型方法 =====
// OAuthClient 方法
func (c *OAuthClient) IsRedirectURIValid(uri string) bool {
// 这里应该解析 RedirectURIs JSON 并验证
// 简化实现,实际使用时需要完善
return true
}
func (c *OAuthClient) HasScope(scope string) bool {
// 这里应该解析 Scope 并检查
// 简化实现,实际使用时需要完善
return true
}
// OAuthAuthorizationCode 方法
func (code *OAuthAuthorizationCode) IsExpired() bool {
return time.Now().After(code.ExpiresAt)
}
// OAuthAccessToken 方法
func (token *OAuthAccessToken) IsExpired() bool {
return time.Now().After(token.ExpiresAt)
}
func (token *OAuthAccessToken) IsValid() bool {
return !token.Revoked && !token.IsExpired()
}
// OAuthRefreshToken 方法
func (token *OAuthRefreshToken) IsExpired() bool {
return time.Now().After(token.ExpiresAt)
}
func (token *OAuthRefreshToken) IsValid() bool {
return !token.Revoked && !token.IsExpired()
}
// UserSession 方法
func (s *UserSession) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
}
func (s *UserSession) IsValid() bool {
return s.IsActive && !s.IsExpired()
}

@ -1,49 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2025-07-24 15:27:31
// Distributed under terms of the MIT license.
//
package oauth
import (
"crypto/rand"
"encoding/hex"
"net/url"
)
// generateRandomString 生成指定长度的随机字符串
func generateRandomString(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes)[:length], nil
}
// BuildRedirectURI 构建成功重定向URI
func BuildRedirectURI(baseURI, code, state string) string {
u, _ := url.Parse(baseURI)
q := u.Query()
q.Set("code", code)
if state != "" {
q.Set("state", state)
}
u.RawQuery = q.Encode()
return u.String()
}
// BuildErrorRedirectURI 构建错误重定向URI
func BuildErrorRedirectURI(baseURI, errorCode, errorDesc, state string) string {
u, _ := url.Parse(baseURI)
q := u.Query()
q.Set("error", errorCode)
if errorDesc != "" {
q.Set("error_description", errorDesc)
}
if state != "" {
q.Set("state", state)
}
u.RawQuery = q.Encode()
return u.String()
}

@ -1,4 +1,4 @@
/* 用于定义vyes-ui库的样式变量 */
/* 用于定义vhtml-ui库的样式变量 */
body {
--v-color-primary: var(--color-primary);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,61 +1,62 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="VyesJS Framework" details="下一代Web组件框架">
<title>VyesJS - 现代Web组件框架</title>
<link rel="stylesheet" href="https://unpkg.com/animations@latest/css/animate.min.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="vhtmlJS Framework" details="下一代Web组件框架">
<title>vhtmlJS - 现代Web组件框架</title>
<link rel="stylesheet" href="https://unpkg.com/animations@latest/css/animate.min.css">
</head>
<style>
body {
body {
font-family: var(--font-family);
line-height: 1.6;
overflow-x: hidden;
margin: 0;
color: var(--text-color);
}
}
.navbar {
.navbar {
position: fixed;
width: 100%;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
z-index: 50;
box-shadow: var(--shadow-sm);
}
}
.nav-container {
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
}
}
.logo {
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--color-primary);
}
}
.nav-links {
.nav-links {
display: flex;
gap: 1.5rem;
}
}
.nav-links a {
.nav-links a {
text-decoration: none;
color: var(--text-color);
transition: color 0.3s;
}
}
.nav-links a:hover {
.nav-links a:hover {
color: var(--color-primary);
}
}
.hero {
.hero {
min-height: 100vh;
display: flex;
align-items: center;
@ -63,36 +64,36 @@ body {
position: relative;
overflow: hidden;
color: white;
}
}
.hero-content {
.hero-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
position: relative;
z-index: 20;
}
}
.hero-title {
.hero-title {
font-size: 3.75rem;
font-weight: bold;
margin-bottom: 1.5rem;
line-height: 1.2;
}
}
.hero-subtitle {
.hero-subtitle {
font-size: 1.25rem;
opacity: 0.9;
margin-bottom: 2rem;
max-width: 48rem;
}
}
.hero-buttons {
.hero-buttons {
display: flex;
gap: 1rem;
}
}
.btn-start {
.btn-start {
background-color: white;
color: var(--color-primary);
padding: 0.75rem 1.5rem;
@ -101,14 +102,14 @@ body {
border: none;
cursor: pointer;
transition: all 0.3s;
}
}
.btn-start:hover {
.btn-start:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
}
.btn-github {
.btn-github {
background-color: transparent;
border: 1px solid white;
color: white;
@ -117,53 +118,57 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
}
.btn-github:hover {
.btn-github:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
@media (max-width: 768px) {
@media (max-width: 768px) {
.hero {
padding-top: 6rem;
text-align: center;
padding-top: 6rem;
text-align: center;
}
.hero-title {
font-size: 2.5rem;
font-size: 2.5rem;
}
.hero-buttons {
justify-content: center;
justify-content: center;
}
}
}
</style>
<body>
<nav class="navbar">
<div class="nav-container">
<div class="logo">VyesJS</div>
<div class="nav-links">
<a href="#features">特性</a>
<a href="#docs">文档</a>
<a href="#examples">示例</a>
</div>
</div>
</nav>
<section class="hero">
<div class="hero-content">
<h1 class="hero-title animate__animated animate__fadeInUp">
下一代Web组件框架
</h1>
<p class="hero-subtitle animate__animated animate__fadeInUp animate__delay-1s">
用熟悉的HTML语法构建现代Web应用。无需构建工具开箱即用。
</p>
<div class="hero-buttons animate__animated animate__fadeInUp animate__delay-2s">
<button class="btn-start">快速开始</button>
<button class="btn-github">GitHub</button>
</div>
</div>
</section>
<nav class="navbar">
<div class="nav-container">
<div class="logo">vhtmlJS</div>
<div class="nav-links">
<a href="#features">特性</a>
<a href="#docs">文档</a>
<a href="#examples">示例</a>
</div>
</div>
</nav>
<section class="hero">
<div class="hero-content">
<h1 class="hero-title animate__animated animate__fadeInUp">
下一代Web组件框架
</h1>
<p class="hero-subtitle animate__animated animate__fadeInUp animate__delay-1s">
用熟悉的HTML语法构建现代Web应用。无需构建工具开箱即用。
</p>
<div class="hero-buttons animate__animated animate__fadeInUp animate__delay-2s">
<button class="btn-start">快速开始</button>
<button class="btn-github">GitHub</button>
</div>
</div>
</section>
</body>
<script setup>
// Interactive logic can be added here
</script>
</html>
</html>

@ -2,10 +2,10 @@
<html>
<head>
<title>OneAuth</title>
<title>vbase</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="OneAuth Landing Page">
<meta name="description" content="vbase Landing Page">
</head>
<style>
body {
@ -33,20 +33,20 @@
max-width: 600px;
line-height: 1.6;
}
.features {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
}
.feature-icon {
font-size: 2rem;
color: var(--color-primary);
@ -58,16 +58,16 @@
justify-content: center;
border-radius: 50%;
}
.feature-text {
font-weight: 600;
}
</style>
<body>
<h1>OneAuth</h1>
<h1>vbase</h1>
<p>Secure, Simple, Scalable Authentication Service for your applications.</p>
<div class="features">
<div class="feature-item">
<div class="feature-icon"><i class="fa-solid fa-shield-halved"></i></div>
@ -82,8 +82,9 @@
<div class="feature-text">Developer Friendly</div>
</div>
</div>
<v-btn round size="large" :click="goLogin">Get Started <i class="fa-solid fa-arrow-right" style="margin-left: 8px;"></i></v-btn>
<v-btn round size="large" :click="goLogin">Get Started <i class="fa-solid fa-arrow-right"
style="margin-left: 8px;"></i></v-btn>
</body>
<script setup>
@ -92,4 +93,4 @@
}
</script>
</html>
</html>

@ -1,47 +1,54 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Vyes.js Features" details="Vyes.js 框架特性介绍">
<title>Vyes.js - 现代前端框架</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="vhtml.js Features" details="vhtml 框架特性介绍">
<title>vhtml - 现代前端框架</title>
</head>
<style>
body {
body {
margin: 0;
font-family: var(--font-family);
background-color: var(--bg-color-secondary);
color: var(--text-color);
}
header {
}
header {
background: linear-gradient(120deg, var(--color-primary), color-mix(in srgb, var(--color-primary), #000 20%));
color: white;
padding: 40px 20px;
text-align: center;
box-shadow: var(--shadow-md);
}
header h1 {
}
header h1 {
font-size: 3rem;
margin: 0;
}
header p {
}
header p {
font-size: 1.2rem;
margin-top: 10px;
opacity: 0.9;
}
.container {
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.features {
}
.features {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 40px;
}
.feature-card {
}
.feature-card {
background: var(--bg-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
@ -50,21 +57,25 @@ header p {
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid var(--border-color);
}
.feature-card:hover {
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-lg);
}
.feature-card h2 {
}
.feature-card h2 {
font-size: 1.5rem;
margin-bottom: 10px;
color: var(--color-primary);
}
.feature-card p {
}
.feature-card p {
font-size: 1rem;
color: var(--text-color-secondary);
}
.cta-button {
}
.cta-button {
display: inline-block;
background: var(--color-primary);
color: white;
@ -76,58 +87,65 @@ header p {
transition: background 0.3s ease;
border: none;
cursor: pointer;
}
.cta-button:hover {
}
.cta-button:hover {
background: color-mix(in srgb, var(--color-primary), black 10%);
}
footer {
}
footer {
background: var(--bg-color);
color: var(--text-color);
text-align: center;
padding: 20px;
margin-top: 40px;
border-top: 1px solid var(--border-color);
}
@keyframes fadeIn {
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
}
.fade-in {
animation: fadeIn 1s ease-out;
}
}
</style>
<body>
<header>
<h1 class="fade-in">Vyes.js</h1>
<p class="fade-in">现代化、轻量级的前端框架</p>
<a href="#" class="cta-button fade-in">立即开始</a>
</header>
<div class="container">
<div class="features">
<div class="feature-card fade-in" v-for="feature in features">
<h2>{{ feature.title }}</h2>
<p>{{ feature.desc }}</p>
</div>
</div>
<header>
<h1 class="fade-in">vhtml.js</h1>
<p class="fade-in">现代化、轻量级的前端框架</p>
<a href="#" class="cta-button fade-in">立即开始</a>
</header>
<div class="container">
<div class="features">
<div class="feature-card fade-in" v-for="feature in features">
<h2>{{ feature.title }}</h2>
<p>{{ feature.desc }}</p>
</div>
</div>
<footer>
<p>&copy; 2024 Vyes.js Team. All rights reserved.</p>
</footer>
</div>
<footer>
<p>&copy; 2024 vhtml Team. All rights reserved.</p>
</footer>
</body>
<script setup>
features = [
{ title: "轻量级", desc: "核心库仅 10KB加载速度飞快。" },
{ title: "组件化", desc: "基于 Web Components支持原生组件复用。" },
{ title: "响应式", desc: "内置响应式数据绑定,状态管理更简单。" },
{ title: "零配置", desc: "无需复杂的构建工具,引入即可使用。" },
{ title: "高性能", desc: "虚拟 DOM 优化,渲染性能卓越。" },
{ title: "易扩展", desc: "丰富的插件系统,满足各种开发需求。" }
]
features = [
{title: "轻量级", desc: "核心库仅 10KB加载速度飞快。"},
{title: "组件化", desc: "基于 Web Components支持原生组件复用。"},
{title: "响应式", desc: "内置响应式数据绑定,状态管理更简单。"},
{title: "零配置", desc: "无需复杂的构建工具,引入即可使用。"},
{title: "高性能", desc: "虚拟 DOM 优化,渲染性能卓越。"},
{title: "易扩展", desc: "丰富的插件系统,满足各种开发需求。"}
]
</script>
</html>
</html>

@ -359,26 +359,27 @@
<div class="subtitle" style="margin: 0 0 15px; font-size: 12px;">或使用手机/邮箱注册</div>
<div class="input-group">
<v-input v:value="signUpForm.username" placeholder="用户名"></v-input>
<v-input v:value="signUpForm.username" placeholder="用户名" :validate="validateUsername"></v-input>
<!-- 手机号输入框带区域选择 -->
<div v-if='$G.cfg.sms' class="phone-row">
<v-input type="select" v:value="signUpForm.region" :opts="{options: regions}"></v-input>
<v-input v:value="signUpForm.phone" placeholder="手机号"></v-input>
<v-input v:value="signUpForm.phone" placeholder="手机号" :validate="validatePhone"></v-input>
</div>
<div v-if='$G.cfg.sms' class="verify-row">
<v-input v:value="signUpForm.verifyCode" placeholder="验证码"></v-input>
<v-input v:value="signUpForm.verifyCode" placeholder="验证码" :validate="validateCode"></v-input>
<v-btn variant="outline" :disabled="smsCountdown > 0 || smsLoading" :click="() => sendVerifyCode('signup')"
style="min-width: 100px;">
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</v-btn>
</div>
<v-input type="password" v:value="signUpForm.password" placeholder="密码"></v-input>
<v-input type="password" v:value="signUpForm.password" placeholder="密码"
:validate="validatePassword"></v-input>
</div>
<div class="error-message">{{ signUpError }}</div>
<div class="error-message" v-if="signUpError">{{ signUpError }}</div>
<v-btn round block size="lg" :loading="signUpLoading" :click="handleSignUp">立即注册</v-btn>
</div>
@ -408,32 +409,34 @@
</div>
</div>
<!-- 用户名登录 -->
<div v-if="loginType === 'username'" class="input-group">
<v-input v:value="signInForm.username" placeholder="用户名"></v-input>
<v-input type="password" v:value="signInForm.password" placeholder="密码"></v-input>
</div>
<!-- 用户名登录 (使用 v-form) -->
<v-form v-if="loginType === 'username'" :data="signInForm" :items="signInUsernameItems" @submit="handleSignIn">
<div vslot="actions" style="width: 100%;">
<a href="#" class="forgot-password"
style="display: block; text-align: right; margin-bottom: 15px;">忘记密码?</a>
<div class="error-message" v-if="signInError">{{ signInError }}</div>
<v-btn round block size="lg" :loading="signInLoading" type="submit">登 录</v-btn>
</div>
</v-form>
<!-- 手机号登录 -->
<!-- 手机号登录 (保持自定义布局,使用 v-input validate) -->
<div v-if="loginType === 'phone'" class="input-group">
<div class="phone-row">
<v-input type="select" v:value="signInForm.region" :opts="{options: regions}"></v-input>
<v-input v:value="signInForm.phone" placeholder="手机号"></v-input>
<v-input v:value="signInForm.phone" placeholder="手机号" :validate="validatePhone"></v-input>
</div>
<div class="verify-row">
<v-input v:value="signInForm.verifyCode" placeholder="验证码"></v-input>
<v-input v:value="signInForm.verifyCode" placeholder="验证码" :validate="validateCode"></v-input>
<v-btn variant="outline" :disabled="smsCountdown > 0 || smsLoading" :click="() => sendVerifyCode('signin')"
style="min-width: 100px;">
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</v-btn>
</div>
</div>
<a href="#" class="forgot-password">忘记密码?</a>
<div class="error-message">{{ signInError }}</div>
<v-btn round block size="lg" :loading="signInLoading" :click="handleSignIn">登 录</v-btn>
<div class="error-message" v-if="signInError">{{ signInError }}</div>
<v-btn round block size="lg" :loading="signInLoading" :click="handleSignIn">登 录</v-btn>
</div>
</div>
</div>
@ -471,6 +474,39 @@
signUpLoading = false; // 注册按钮加载状态
signInLoading = false; // 登录按钮加载状态
// 验证规则函数
validateUsername = (val) => {
if (!val || val.length < 5) return '5';
return true;
};
validatePassword = (val) => {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_]).{9,}$/;
if (!regex.test(val)) return '密码必须大于8位且包含大小写字母、下划线和数字';
return true;
};
validatePhone = (val) => {
const region = isSignUp ? signUpForm.region : signInForm.region;
let regex = /^\d{7,15}$/;
if (region === '+86') regex = /^1[3-9]\d{9}$/;
else if (region === '+1') regex = /^\d{10}$/;
if (!regex.test(val)) return '手机号格式不正确';
return true;
};
validateCode = (val) => {
if (!val || val.length < 4) return '';
return true;
};
// v-form 配置项 (用户名登录)
signInUsernameItems = [
{name: 'username', placeholder: '用户名', required: true, validate: validateUsername},
{name: 'password', placeholder: '密码', type: 'password', required: true, validate: validatePassword}
];
// 常用国家/地区代码 - 转换为 v-select 格式
regions = [
{value: '+86', label: '+86 中国'},
@ -496,7 +532,8 @@
];
// 验证手机号格式(根据不同地区调整)
validatePhone = (phone, region) => {
// 已迁移至 validatePhone 函数,保留此函数供 sendVerifyCode 使用
checkPhone = (phone, region) => {
if (region === '+86') {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
@ -554,7 +591,7 @@
return;
}
if (!validatePhone(phone, region)) {
if (!checkPhone(phone, region)) {
const errorMsg = '请输入正确的手机号格式';
type === 'signup' ? signUpError = errorMsg : signInError = errorMsg;
return;
@ -639,12 +676,13 @@
try {
let loginData = {};
if (loginType === 'username') {
if (!signInForm.username) {signInError = '请输入用户名'; return;}
if (!signInForm.password) {signInError = '请输入密码'; return;}
// v-form 提交时已经通过了基本验证,但 double check 也没坏处
if (validateUsername(signInForm.username) !== true) return;
if (validatePassword(signInForm.password) !== true) return;
loginData = {username: signInForm.username, code: btoa(signInForm.password), type: 'username'};
} else {
if (!signInForm.phone) {signInError = '请输入手机号'; return;}
if (!signInForm.verifyCode) {signInError = '请输入验证码'; return;}
if (validatePhone(signInForm.phone) !== true) return;
if (validateCode(signInForm.verifyCode) !== true) return;
loginData = {phone: signInForm.phone, region: signInForm.region, verify_code: signInForm.verifyCode, type: 'phone'};
}

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oa</title>
<script type="module" key='vyes' src="/assets/vyes.min.js"></script>
<script type="module" key='vhtml' src="/assets/vhtml.min.js"></script>
<link rel="stylesheet" href="/assets/common.css">
<link rel="stylesheet" href="/v/common.css">
<link href="/assets/libs/animate/animate.min.css" rel="stylesheet">

Loading…
Cancel
Save