Compare commits

...

3 Commits

Author SHA1 Message Date
veypi
bc3f5e0b0c home 2021-11-05 23:46:21 +08:00
veypi
d7aea82ced rename view name 2021-10-28 17:23:59 +08:00
veypi
c74c332e6a change vue2 to vue3 2021-10-28 17:22:10 +08:00
79 changed files with 5965 additions and 9969 deletions

View File

@ -2,6 +2,7 @@
统一验证服务
## 用户验证思路
![](https://public.veypi.com/img/screenshot/20211012194238.png)

View File

@ -3,22 +3,25 @@ package api
import (
"OneAuth/api/app"
"OneAuth/api/role"
"OneAuth/api/token"
"OneAuth/api/user"
"OneAuth/api/wx"
"OneAuth/libs/base"
"github.com/veypi/OneBD"
"github.com/veypi/OneBD/core"
)
func Router(r OneBD.Router) {
r.SetNotFoundFunc(func(m core.Meta) {
m.Write([]byte("{\"status\": 0}"))
base.JSONResponse(m, nil, nil)
})
r.SetInternalErrorFunc(func(m core.Meta) {
m.Write([]byte("{\"status\": 0}"))
base.JSONResponse(m, nil, nil)
})
user.Router(r.SubRouter("/user"))
wx.Router(r.SubRouter("wx"))
app.Router(r.SubRouter("app"))
token.Router(r.SubRouter("token"))
role.Router(r)
//message.Router(r.SubRouter("/message"))

View File

@ -8,15 +8,17 @@ import (
"OneAuth/models"
"github.com/veypi/OneBD"
"github.com/veypi/OneBD/rfc"
"github.com/veypi/utils"
)
func Router(r OneBD.Router) {
r.Set("/", appHandlerP, rfc.MethodPost, rfc.MethodGet)
r.Set("/:id", appHandlerP, rfc.MethodGet)
}
var appHandlerP = OneBD.NewHandlerPool(func() OneBD.Handler {
h := &appHandler{}
h.Ignore(rfc.MethodGet)
h.Ignore(rfc.MethodGet, rfc.MethodPost)
return h
})
@ -31,7 +33,7 @@ func (h *appHandler) Get() (interface{}, error) {
isSelf := h.Meta().Query("is_self")
if isSelf != "" {
// 无权限可以获取本系统基本信息
h.query.ID = cfg.CFG.APPID
h.query.UUID = cfg.CFG.APPUUID
err := cfg.DB().Where(h.query).First(h.query).Error
return h.query, err
}
@ -64,3 +66,35 @@ func (h *appHandler) Get() (interface{}, error) {
err = cfg.DB().Find(&list).Error
return list, err
}
func (h *appHandler) Post() (interface{}, error) {
data := &struct {
Name string `json:"name"`
UUID string `json:"uuid"`
}{}
err := h.Meta().ReadJson(data)
if err != nil {
return nil, err
}
if data.Name == "" {
return nil, oerr.ApiArgsMissing.AttachStr("name")
}
_ = h.ParsePayload(h.Meta())
a := &models.App{
UUID: data.UUID,
Name: data.Name,
Key: utils.RandSeq(32),
Creator: h.Payload.ID,
}
a.Key = utils.RandSeq(32)
if data.UUID != "" {
err = cfg.DB().Where("uuid = ?", data.UUID).FirstOrCreate(a).Error
} else {
data.UUID = utils.RandSeq(16)
err = cfg.DB().Create(a).Error
}
if err != nil {
return nil, err
}
return a, nil
}

69
api/token/token.go Normal file
View File

@ -0,0 +1,69 @@
package token
import (
"OneAuth/cfg"
"OneAuth/libs/app"
"OneAuth/libs/base"
"OneAuth/libs/oerr"
"OneAuth/libs/token"
"OneAuth/models"
"errors"
"github.com/veypi/OneBD"
"github.com/veypi/OneBD/rfc"
"gorm.io/gorm"
)
func Router(r OneBD.Router) {
p := OneBD.NewHandlerPool(func() OneBD.Handler {
return &tokenHandler{}
})
r.Set("/:uuid", p, rfc.MethodGet)
}
type tokenHandler struct {
base.ApiHandler
}
func (h *tokenHandler) Get() (interface{}, error) {
uuid := h.Meta().Params("uuid")
if uuid == "" {
return nil, oerr.ApiArgsMissing.AttachStr("uuid")
}
a := &models.App{}
a.UUID = uuid
err := cfg.DB().Where("uuid = ?", uuid).First(a).Error
if err != nil {
return nil, err
}
au := &models.AppUser{
UserID: h.Payload.ID,
AppID: a.ID,
}
err = cfg.DB().Where(au).First(au).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if a.EnableRegister {
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
return app.AddUser(cfg.DB(), au.AppID, au.UserID, a.InitRoleID, models.AUOK)
})
if err != nil {
return nil, err
}
au.Status = models.AUOK
} else {
return nil, oerr.AppNotJoin.AttachStr(a.Name)
}
}
return nil, oerr.DBErr.Attach(err)
}
if au.Status != models.AUOK {
return nil, oerr.NoAuth.AttachStr(string(au.Status))
}
u := &models.User{}
err = cfg.DB().Preload("Auths").Preload("Roles.Auths").Where("id = ?", h.Payload.ID).First(u).Error
if err != nil {
return nil, err
}
t, err := token.GetToken(u, a.ID, a.Key)
return t, err
}

View File

@ -3,13 +3,11 @@ package user
import (
"OneAuth/cfg"
"OneAuth/libs/app"
"OneAuth/libs/auth"
"OneAuth/libs/base"
"OneAuth/libs/oerr"
"OneAuth/libs/token"
"OneAuth/models"
"errors"
//"OneAuth/ws"
"encoding/base64"
"fmt"
"github.com/veypi/OneBD"
@ -43,10 +41,13 @@ type handler struct {
// Get get user data
func (h *handler) Get() (interface{}, error) {
if !h.Payload.GetAuth(auth.User, "").CanRead() {
return nil, oerr.NoAuth.AttachStr("to read user list")
}
username := h.Meta().Query("username")
if username != "" {
users := make([]*models.User, 0, 10)
err := cfg.DB().Preload("Scores").Preload("Roles.Auths").Where("username LIKE ? OR nickname LIKE ?", "%"+username+"%", "%"+username+"%").Find(&users).Error
err := cfg.DB().Where("username LIKE ? OR nickname LIKE ?", "%"+username+"%", "%"+username+"%").Find(&users).Error
if err != nil {
return nil, err
}
@ -56,14 +57,14 @@ func (h *handler) Get() (interface{}, error) {
if userID != 0 {
user := &models.User{}
user.ID = uint(userID)
return user, cfg.DB().Where(user).Preload("Scores").Preload("Roles.Auths").Preload("Favorites").First(user).Error
return user, cfg.DB().Where(user).First(user).Error
} else {
users := make([]models.User, 10)
skip, err := strconv.Atoi(h.Meta().Query("skip"))
if err != nil || skip < 0 {
skip = 0
}
if err := cfg.DB().Preload("Scores").Preload("Roles.Auths").Offset(skip).Find(&users).Error; err != nil {
if err := cfg.DB().Offset(skip).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
@ -73,16 +74,15 @@ func (h *handler) Get() (interface{}, error) {
// Post register user
func (h *handler) Post() (interface{}, error) {
self := &models.App{}
self.ID = cfg.CFG.APPID
self.UUID = cfg.CFG.APPUUID
err := cfg.DB().Where(self).First(self).Error
if err != nil {
return nil, oerr.DBErr.Attach(err)
}
if !self.EnableRegister {
if !self.EnableRegister && !h.Payload.GetAuth(auth.User, "").CanCreate() {
return nil, oerr.NoAuth.AttachStr("register disabled")
}
var userdata = struct {
UUID string `json:"uuid"`
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
@ -120,27 +120,10 @@ func (h *handler) Post() (interface{}, error) {
if err := tx.Create(&h.User).Error; err != nil {
return oerr.ResourceDuplicated
}
err := app.AddUser(tx, self.ID, h.User.ID, self.InitRoleID)
err := app.AddUser(tx, self.ID, h.User.ID, self.InitRoleID, models.AUOK)
if err != nil {
return err
}
if userdata.UUID != self.UUID && userdata.UUID != "" {
target := &models.App{}
target.UUID = userdata.UUID
err = tx.Where(target).First(target).Error
if err != nil {
return err
}
if target.EnableRegister {
err := app.AddUser(tx, target.ID, h.User.ID, target.InitRoleID)
if err != nil {
return err
}
} else {
// TODO
}
return err
}
return nil
})
if err != nil {
@ -169,11 +152,10 @@ func (h *handler) Patch() (interface{}, error) {
} else {
target.ID = uint(tempID)
}
tx := cfg.DB().Begin()
if err := cfg.DB().Where(&target).First(&target).Error; err != nil {
return nil, err
}
if target.ID != h.Payload.ID {
if target.ID != h.Payload.ID && h.Payload.GetAuth(auth.User, strconv.Itoa(int(target.ID))).CanUpdate() {
return nil, oerr.NoAuth
}
if len(opts.Password) >= 6 {
@ -197,11 +179,9 @@ func (h *handler) Patch() (interface{}, error) {
if opts.Status != "" {
target.Status = opts.Status
}
if err := tx.Updates(&target).Error; err != nil {
tx.Rollback()
if err := cfg.DB().Updates(&target).Error; err != nil {
return nil, err
}
tx.Commit()
return nil, nil
}
@ -222,10 +202,6 @@ func (h *handler) Head() (interface{}, error) {
if len(uid) == 0 || len(password) == 0 {
return nil, oerr.ApiArgsError
}
appUUID := h.Meta().Query("uuid")
if appUUID == "" {
return nil, oerr.ApiArgsMissing.AttachStr("uuid")
}
h.User = new(models.User)
uidType := h.Meta().Query("uid_type")
switch uidType {
@ -239,7 +215,7 @@ func (h *handler) Head() (interface{}, error) {
h.User.Username = uid
}
target := &models.App{}
target.UUID = appUUID
target.UUID = cfg.CFG.APPUUID
err = cfg.DB().Where(target).Find(target).Error
if err != nil {
return nil, oerr.DBErr.Attach(err)
@ -261,17 +237,12 @@ func (h *handler) Head() (interface{}, error) {
au.AppID = target.ID
err = cfg.DB().Where(au).First(au).Error
appID := target.ID
h.Meta().SetHeader("content", target.UserRefreshUrl)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
appID = cfg.CFG.APPID
h.Meta().SetHeader("content", "/app/"+target.UUID)
} else if au.Disabled {
return nil, err
} else if au.Status != models.AUOK {
return nil, oerr.DisableLogin
}
tokenStr, err := token.GetToken(h.User, appID)
tokenStr, err := token.GetToken(h.User, appID, cfg.CFG.APPKey)
if err != nil {
log.HandlerErrs(err)
return nil, oerr.Unknown.Attach(err)

View File

@ -15,11 +15,11 @@ var CFG = &struct {
Host string
LoggerPath string
LoggerLevel string
APPID uint
APPUUID string
APPKey string
TimeFormat string
Debug bool
EXEDir string
MediaDir string
DB struct {
Type string
Addr string
@ -28,14 +28,15 @@ var CFG = &struct {
DB string
}
}{
APPID: 1,
APPUUID: "jU5Jo5hM",
APPKey: "cB43wF94MLTksyBK",
AdminUser: "admin",
Host: "0.0.0.0:4001",
LoggerPath: "",
LoggerLevel: "debug",
TimeFormat: "2006/01/02 15:04:05",
Debug: true,
EXEDir: "./",
MediaDir: "/Users/light/test/media/",
DB: struct {
Type string
Addr string

3
go.mod
View File

@ -6,9 +6,10 @@ require (
github.com/json-iterator/go v1.1.10
github.com/urfave/cli/v2 v2.2.0
github.com/veypi/OneBD v0.4.1
github.com/veypi/utils v0.2.2
github.com/veypi/utils v0.3.1
gorm.io/driver/mysql v1.0.5
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.3
)
replace github.com/veypi/OneBD v0.4.1 => ../OceanCurrent/OneBD

8
go.sum
View File

@ -43,11 +43,11 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/veypi/OneBD v0.4.1 h1:IkuV2tqay3fyX+FsM9XrKiKS9N7pymUJnrscGx2T0mc=
github.com/veypi/OneBD v0.4.1/go.mod h1:9IoMOBzwIGyv6IZGF7ZnTYwTcHltLKicDgcwha66G0U=
github.com/veypi/utils v0.1.5/go.mod h1:oKcwTDfvE1qtuhJuCcDcfvGquv9bHdFaCGA42onVMC4=
github.com/veypi/utils v0.2.2 h1:BRxu0mYJJpuubPjmIIrRVr0XEq9NMp//KUCrVTkFums=
github.com/veypi/utils v0.2.2/go.mod h1:rAkC6Fbk5cBa3u+8pyCpsVcnXw74EhEQJGmPND9FvRg=
github.com/veypi/utils v0.3.0 h1:vCi0jqMsAMBPblFCmneUw3Wet5y1XHZLA5ZP9c/2owI=
github.com/veypi/utils v0.3.0/go.mod h1:rAkC6Fbk5cBa3u+8pyCpsVcnXw74EhEQJGmPND9FvRg=
github.com/veypi/utils v0.3.1 h1:QL4Q/+iXNFXNVENiUeEttSwNwkeqrorSpTBpCs7fXBI=
github.com/veypi/utils v0.3.1/go.mod h1:rAkC6Fbk5cBa3u+8pyCpsVcnXw74EhEQJGmPND9FvRg=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@ -8,7 +8,10 @@ import (
"gorm.io/gorm"
)
func AddUser(tx *gorm.DB, appID uint, userID uint, roleID uint) error {
func AddUser(tx *gorm.DB, appID uint, userID uint, roleID uint, status models.AUStatus) error {
if appID == 0 || userID == 0 {
return oerr.FuncArgsError
}
au := &models.AppUser{}
au.AppID = appID
au.UserID = userID
@ -17,19 +20,25 @@ func AddUser(tx *gorm.DB, appID uint, userID uint, roleID uint) error {
return oerr.ResourceDuplicated
}
if errors.Is(err, gorm.ErrRecordNotFound) {
au.Status = status
err = tx.Create(au).Error
if err != nil {
return err
}
err = auth.BindUserRole(tx, userID, roleID)
if err != nil {
return err
if roleID > 0 {
err = auth.BindUserRole(tx, userID, roleID)
if err != nil {
return err
}
}
return tx.Model(&models.App{}).Where("id = ?", appID).Update("user_count", gorm.Expr("user_count + ?", 1)).Error
}
return err
}
func EnableUser(tx *gorm.DB, appID uint, userID uint) error {
if appID == 0 || userID == 0 {
return oerr.FuncArgsError
}
au := &models.AppUser{}
au.AppID = appID
au.UserID = userID
@ -37,12 +46,18 @@ func EnableUser(tx *gorm.DB, appID uint, userID uint) error {
if err != nil {
return err
}
return tx.Where(au).Update("disabled", false).Error
if au.Status != models.AUOK {
return tx.Where(au).Update("status", models.AUOK).Error
}
return nil
}
func DisableUser(tx *gorm.DB, appID uint, userID uint) error {
if appID == 0 || userID == 0 {
return oerr.FuncArgsError
}
au := &models.AppUser{}
au.AppID = appID
au.UserID = userID
return tx.Where(au).Update("disabled", true).Error
return tx.Where(au).Update("status", models.AUDisable).Error
}

View File

@ -3,10 +3,12 @@ package base
import (
"OneAuth/libs/oerr"
"OneAuth/libs/tools"
"errors"
"github.com/json-iterator/go"
"github.com/veypi/OneBD"
"github.com/veypi/OneBD/rfc"
"github.com/veypi/utils/log"
"gorm.io/gorm"
"strconv"
"sync"
"time"
@ -14,6 +16,41 @@ import (
var json = jsoniter.ConfigFastest
func JSONResponse(m OneBD.Meta, data interface{}, err error) {
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = oerr.ResourceNotExist
}
}
if m.Method() == rfc.MethodHead {
if err != nil {
m.SetHeader("status", "0")
m.SetHeader("code", strconv.Itoa(int(oerr.OfType(err.Error()))))
m.SetHeader("err", err.Error())
} else {
m.SetHeader("status", "1")
}
return
}
res := map[string]interface{}{
"status": 1,
}
if err != nil {
res["status"] = 0
res["code"] = oerr.OfType(err.Error())
res["err"] = err.Error()
} else {
res["status"] = 1
res["content"] = data
}
p, err := json.Marshal(res)
if err != nil {
log.Warn().Err(err).Msg("encode json data error")
return
}
_, _ = m.Write(p)
}
type ApiHandler struct {
OneBD.BaseHandler
UserHandler
@ -24,29 +61,12 @@ func (h *ApiHandler) Init(m OneBD.Meta) error {
}
func (h *ApiHandler) OnResponse(data interface{}) {
if h.Meta().Method() == rfc.MethodHead {
h.Meta().SetHeader("status", "1")
return
}
p, err := json.Marshal(map[string]interface{}{"status": 1, "content": data})
if err != nil {
log.Warn().Err(err).Msg("encode json data error")
return
}
h.Meta().Write(p)
JSONResponse(h.Meta(), data, nil)
}
func (h *ApiHandler) OnError(err error) {
log.WithNoCaller.Warn().Err(err).Msg(h.Meta().RequestPath())
msg := err.Error()
if h.Meta().Method() == rfc.MethodHead {
h.Meta().SetHeader("status", "0")
h.Meta().SetHeader("code", strconv.Itoa(int(oerr.OfType(msg))))
h.Meta().SetHeader("err", msg)
} else {
p, _ := json.Marshal(map[string]interface{}{"status": 0, "code": oerr.OfType(msg), "err": msg})
h.Meta().Write(p)
}
JSONResponse(h.Meta(), nil, err)
}
var ioNumLimit = make(map[string]time.Time)

View File

@ -1,6 +1,7 @@
package base
import (
"OneAuth/cfg"
"OneAuth/libs/oerr"
"OneAuth/libs/token"
"OneAuth/models"
@ -26,7 +27,7 @@ func (a *UserHandler) ParsePayload(m OneBD.Meta) error {
if tokenStr == "" {
return oerr.NotLogin
}
ok, err := token.ParseToken(tokenStr, a.Payload)
ok, err := token.ParseToken(tokenStr, a.Payload, cfg.CFG.APPKey)
if ok {
return nil
}

View File

@ -1,7 +1,6 @@
package oerr
import (
"errors"
"gorm.io/gorm"
"strconv"
)
@ -42,11 +41,13 @@ type Code uint
- 9 : 本不可能发生的错误例如被人攻击导致数据异常产生的逻辑错误
*/
// Unknown error
const (
Unknown Code = 0
)
const (
// 2 数据库错误
// DBErr 2 数据库错误
// -1 系统错误
// -2 数据读写错误
DBErr Code = 20001
@ -56,10 +57,13 @@ const (
)
const (
// 3
// LogicErr 3 系统内逻辑错误
LogicErr Code = 30000
AppNotJoin Code = 30001
)
const (
// NotLogin
// 4 权限类型错误
// 1: 登录权限
// 2: 资源操作权限
@ -130,6 +134,8 @@ var codeMap = map[Code]string{
NoAuth: "no auth to access",
AccessErr: "access error",
AccessTooFast: "access too fast",
LogicErr: "logic error",
AppNotJoin: "not join in app",
}
func (c Code) Error() string {
@ -144,7 +150,7 @@ func (c Code) String() string {
return codeMap[Unknown]
}
// 附加错误详细原因
// Attach 附加错误详细原因
func (c Code) Attach(errs ...error) (e error) {
e = c
for _, err := range errs {
@ -195,16 +201,3 @@ func (w *wrapErr) Error() string {
func (w *wrapErr) UnWrap() error {
return w.err
}
func CheckMultiErr(errs ...error) error {
msg := ""
for _, e := range errs {
if e != nil {
msg += e.Error() + "\n"
}
}
if msg != "" {
return errors.New(msg)
}
return nil
}

View File

@ -1,20 +1,8 @@
package token
import (
"OneAuth/libs/key"
"OneAuth/models"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"strings"
"time"
)
var (
InvalidToken = errors.New("invalid token")
ExpiredToken = errors.New("expired token")
"github.com/veypi/utils/jwt"
)
type simpleAuth struct {
@ -26,11 +14,9 @@ type simpleAuth struct {
// TODO:: roles 是否会造成token过大 ?
type PayLoad struct {
ID uint `json:"id"`
AppID uint `json:"app_id"`
Iat int64 `json:"iat"` //token time
Exp int64 `json:"exp"`
Auth map[uint]*simpleAuth `json:"auth"`
jwt.Payload
ID uint `json:"id"`
Auth map[uint]*simpleAuth `json:"auth"`
}
// GetAuth resource_uuid 缺省或仅第一个有效 权限会被更高权限覆盖
@ -61,19 +47,10 @@ func (p *PayLoad) GetAuth(ResourceID string, ResourceUUID ...string) models.Auth
return res
}
func GetToken(u *models.User, appID uint) (string, error) {
header := map[string]string{
"typ": "JWT",
"alg": "HS256",
}
//header := "{\"typ\": \"JWT\", \"alg\": \"HS256\"}"
now := time.Now().Unix()
payload := PayLoad{
ID: u.ID,
AppID: appID,
Iat: now,
Exp: now + 60*60*24,
Auth: map[uint]*simpleAuth{},
func GetToken(u *models.User, appID uint, key string) (string, error) {
payload := &PayLoad{
ID: u.ID,
Auth: map[uint]*simpleAuth{},
}
for _, a := range u.GetAuths() {
if appID == a.AppID {
@ -84,44 +61,9 @@ func GetToken(u *models.User, appID uint) (string, error) {
}
}
}
a, err := json.Marshal(header)
if err != nil {
return "", err
}
b, err := json.Marshal(payload)
if err != nil {
return "", err
}
A := base64.StdEncoding.EncodeToString(a)
B := base64.StdEncoding.EncodeToString(b)
hmacCipher := hmac.New(sha256.New, []byte(key.User(payload.ID, payload.AppID)))
hmacCipher.Write([]byte(A + "." + B))
C := hmacCipher.Sum(nil)
return A + "." + B + "." + base64.StdEncoding.EncodeToString(C), nil
return jwt.GetToken(payload, []byte(key))
}
func ParseToken(token string, payload *PayLoad) (bool, error) {
var A, B, C string
if seqs := strings.Split(token, "."); len(seqs) == 3 {
A, B, C = seqs[0], seqs[1], seqs[2]
} else {
return false, InvalidToken
}
tempPayload, err := base64.StdEncoding.DecodeString(B)
if err != nil {
return false, err
}
if err := json.Unmarshal(tempPayload, payload); err != nil {
return false, err
}
hmacCipher := hmac.New(sha256.New, []byte(key.User(payload.ID, payload.AppID)))
hmacCipher.Write([]byte(A + "." + B))
tempC := hmacCipher.Sum(nil)
if !hmac.Equal([]byte(C), []byte(base64.StdEncoding.EncodeToString(tempC))) {
return false, nil
}
if time.Now().Unix() > payload.Exp {
return false, ExpiredToken
}
return true, nil
func ParseToken(token string, payload *PayLoad, key string) (bool, error) {
return jwt.ParseToken(token, payload, []byte(key))
}

21
main.go
View File

@ -7,7 +7,6 @@ import (
"github.com/veypi/utils/cmd"
"github.com/veypi/utils/log"
"os"
"path/filepath"
)
const Version = "v0.1.0"
@ -35,21 +34,6 @@ func main() {
Value: cfg.CFG.LoggerPath,
Destination: &cfg.CFG.LoggerPath,
},
&cli.UintFlag{
Name: "id",
Value: cfg.CFG.APPID,
Destination: &cfg.CFG.APPID,
},
&cli.StringFlag{
Name: "key",
Value: cfg.CFG.APPKey,
Destination: &cfg.CFG.APPKey,
},
&cli.StringFlag{
Name: "exe_dir",
Value: cfg.CFG.EXEDir,
Destination: &cfg.CFG.EXEDir,
},
&cli.StringFlag{
Name: "host",
Value: cfg.CFG.Host,
@ -72,11 +56,6 @@ func main() {
srv.SetStopFunc(func() {
})
app.Before = func(c *cli.Context) error {
var err error
cfg.CFG.EXEDir, err = filepath.Abs(cfg.CFG.EXEDir)
if err != nil {
return err
}
if cfg.CFG.Debug {
cfg.CFG.LoggerLevel = "debug"
}

View File

@ -29,22 +29,35 @@ type App struct {
UserRefreshUrl string `json:"user_refresh_url"`
// app 校验用户token时使用
Key string `json:"-"`
// 是否允许用户自主注册
EnableRegister bool `json:"enable_register"`
EnableUserKey bool `json:"enable_user_key"`
EnableUser bool `json:"enable_user"`
EnableWx bool `json:"enable_wx"`
EnablePhone bool `json:"enable_phone"`
EnableEmail bool `json:"enable_email"`
Wx *Wechat `json:"wx" gorm:"foreignkey:AppID;references:ID"`
// 是否允许用户自动加入应用
EnableRegister bool `json:"enable_register"`
//
EnableUserKey bool `json:"enable_user_key"`
UserKeyUrl string `json:"user_key_url"`
// 允许登录方式
EnableUser bool `json:"enable_user"`
EnableWx bool `json:"enable_wx"`
EnablePhone bool `json:"enable_phone"`
EnableEmail bool `json:"enable_email"`
Wx *Wechat `json:"wx" gorm:"foreignkey:AppID;references:ID"`
}
type AUStatus string
const (
AUOK AUStatus = "ok"
AUDisable AUStatus = "disabled"
AUApply AUStatus = "apply"
AUDeny AUStatus = "deny"
)
type AppUser struct {
BaseModel
AppID uint `json:"app_id"`
UserID uint `json:"user_id"`
Disabled bool `json:"disabled"`
Status string `json:"status"`
AppID uint `json:"app_id"`
APP *App `json:"app"`
UserID uint `json:"user_id"`
User *User `json:"user"`
Status AUStatus `json:"status"`
}
type Wechat struct {

11
models/message.go Normal file
View File

@ -0,0 +1,11 @@
package models
type Message struct {
BaseModel
UserID uint `json:"user_id"`
User *User `json:"user"`
Title string `json:"title"`
Redirect string `json:"redirect"`
Content string `json:"content"`
From string `json:"from"`
}

View File

@ -1,3 +0,0 @@
> 1%
last 2 versions
not dead

View File

@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1 +0,0 @@
src/libs/wwLogin.js

View File

@ -1,22 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'object-curly-spacing': 0,
'space-before-function-paren': 0,
'@typescript-eslint/camelcase': 0,
'@typescript-eslint/no-empty-function': 0,
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

26
oaf/.gitignore vendored
View File

@ -1,23 +1,5 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store
dist
dist-ssr
*.local

3
oaf/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

View File

@ -1,24 +1,11 @@
# oaf
# Vue 3 + Typescript + Vite
## Project setup
```
yarn install
```
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
### Compiles and hot-reloads for development
```
yarn serve
```
## Recommended IDE Setup
### Compiles and minifies for production
```
yarn build
```
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
### Lints and fixes files
```
yarn lint
```
## Type Support For `.vue` Imports in TS
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

13
oaf/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2793
oaf/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,31 @@
{
"name": "oaf",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"@veypi/one-icon": "^1.0.1",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"js-base64": "^3.6.0",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-m-message": "^3.1.0",
"vue-property-decorator": "^9.1.2",
"vue-router": "^3.2.0",
"vuetify": "^2.4.0",
"vuex": "^3.4.0"
"@veypi/one-icon": "2.0.5",
"animate.css": "^4.1.1",
"axios": "^0.24.0",
"js-base64": "^3.7.2",
"vue": "^3.2.16",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"sass": "^1.32.0",
"sass-loader": "^10.0.0",
"typescript": "~3.9.3",
"vue-cli-plugin-vuetify": "^2.2.2",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.7.0"
"@tailwindcss/postcss7-compat": "^2.1.0",
"@vitejs/plugin-vue": "^1.9.3",
"autoprefixer": "^9.8.8",
"less": "^4.1.2",
"naive-ui": "^2.19.11",
"postcss": "^7.0.39",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
"typescript": "^4.4.3",
"vfonts": "^0.1.0",
"vite": "^2.6.4",
"vue-tsc": "^0.3.0"
}
}

6
oaf/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico">-->
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,39 +1,137 @@
<template>
<v-app>
<v-app-bar
app
color="primary"
dark
>
<div class="d-flex align-center">
<one-icon style="color: aqua;font-size: 56px">glassdoor</one-icon>
<span class="font-italic font-weight-bold" style="font-size: 20px">统一认证</span>
</div>
<v-spacer></v-spacer>
</v-app-bar>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
<n-config-provider :theme-overrides="Theme.overrides" :locale="zhCN" :date-locale="dateZhCN"
:theme="Theme">
<n-layout class="font-sans select-none">
<n-layout>
<n-layout-header class="pr-5" bordered style="height: 64px;line-height: 64px;">
<div class="inline-block float-left h-full">
<one-icon color="#000" class="inline-block" @click="$router.push('/')" style="font-size: 48px;margin:8px;color:aqua">
glassdoor
</one-icon>
</div>
<div class="inline-block float-left h-full" style="margin-left: 10px">
<n-h6 prefix="bar" align-text><n-text type="primary">统一认证系统</n-text></n-h6>
</div>
<div v-if="store.state.user.ready" class="inline-block h-full float-right flex justify-center items-center">
<avatar></avatar>
</div>
<div class="inline-block float-right h-full px-3">
<fullscreen v-model="isFullScreen" class="header-icon">fullscreen</fullscreen>
<div class="header-icon">
<one-icon @click="ChangeTheme">
{{ IsDark ? 'Daytimemode' : 'nightmode-fill' }}
</one-icon>
</div>
</div>
</n-layout-header>
<n-layout has-sider style="height: calc(100vh - 88px)">
<n-layout-sider
collapse-mode="transform"
:collapsed-width="0"
:width="120"
show-trigger="bar"
content-style="padding: 24px;"
bordered
default-collapsed
:native-scrollbar="false"
>
-
</n-layout-sider>
<n-layout class="main" :native-scrollbar="false">
<n-message-provider>
<router-view v-slot="{ Component }">
<transition mode="out-in" enter-active-class="animate__fadeInLeft" leave-active-class="animate__fadeOutRight">
<component class="animate__animated animate__400ms" :is="Component" style="margin: 10px; min-height: calc(100vh - 108px)"
></component>
</transition>
</router-view>
</n-message-provider>
</n-layout>
</n-layout>
</n-layout>
<n-layout-footer bordered style="height: 24px;line-height: 24px"
class="flex justify-around px-3 text-gray-500 text-xs">
<span class="hover:text-black cursor-pointer" @click="$router.push({name: 'about'})">关于OA</span>
<span class="hover:text-black cursor-pointer">使用须知</span>
<span class="hover:text-black cursor-pointer" @click="goto('https://veypi.com')">
©2021 veypi
</span>
</n-layout-footer>
</n-layout>
</n-config-provider>
</template>
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
import {onBeforeMount, ref} from 'vue'
import util from './libs/util'
import {useStore} from "./store";
import {Theme, IsDark, ChangeTheme} from "./theme";
import {zhCN, dateZhCN} from 'naive-ui'
import avatar from "./components/avatar";
import fullscreen from './components/fullscreen'
import Fullscreen from "./components/fullscreen/fullscreen.vue";
<script lang="ts">
import Vue from 'vue'
import util from '@/libs/util'
let isFullScreen = ref(false)
let store = useStore()
export default Vue.extend({
name: 'App',
components: {},
data: () => ({
//
}),
beforeCreate() {
util.title('统一认证')
this.$store.dispatch('fetchSelf')
}
onBeforeMount(() => {
util.title("统一认证")
store.dispatch('fetchSelf')
store.dispatch('user/fetchUserData')
})
let goto = (url: any) => {
window.open(url, "_blank")
}
let collapsed = ref(true)
</script>
<style lang="less">
.animate__400ms {
--animate-duration: 400ms;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
/* 周围滑动留白 */
html {
overflow: hidden;
height: 100%;
}
body {
overflow: auto;
height: 100%;
}
.header-icon {
display: inline-block;
font-size: 24px;
margin: 20px 10px 20px 10px;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
width: 100%;
height: 100%;
}
.main {
}
::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
</style>

View File

@ -1,65 +1,66 @@
// @ts-ignore
import axios from 'axios'
import store from '@/store'
import {store} from '../store'
function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function) {
return axios({
url: url,
params: query,
data: data,
method: method,
headers: {
auth_token: localStorage.auth_token
}
})
.then((res: any) => {
if ('auth_token' in res.headers) {
localStorage.auth_token = res.headers.auth_token
}
if (method === 'HEAD') {
success(res.headers)
} else {
success(res.data)
}
})
.catch((e: any) => {
if (e.response && e.response.status === 401) {
console.log(e)
store.dispatch('handleLogOut')
return
}
console.log(e)
if (e.response && e.response.status === 500) {
return
}
if (typeof fail === 'function') {
fail(e.response)
} else if (e.response && e.response.status === 400) {
console.log(400)
} else {
console.log(e.request)
}
return axios({
url: url,
params: query,
data: data,
method: method,
headers: {
auth_token: localStorage.auth_token
}
}).then((res: any) => {
if ('auth_token' in res.headers) {
localStorage.auth_token = res.headers.auth_token
}
if (method === 'HEAD') {
success(res.headers)
} else {
success(res.data)
}
})
.catch((e: any) => {
if (e.response && e.response.status === 401) {
console.log(e)
store.commit('user/logout')
return
}
console.log(e)
if (e.response && e.response.status === 500) {
return
}
if (typeof fail === 'function') {
fail(e.response)
} else if (e.response && e.response.status === 400) {
console.log(400)
} else {
console.log(e.request)
}
})
}
const ajax = {
get(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'GET', data, {}, success, fail)
},
head(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'HEAD', data, {}, success, fail)
},
delete(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'DELETE', data, {}, success, fail)
},
post(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'POST', {}, data, success, fail)
},
put(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'PUT', {}, data, success, fail)
},
patch(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'PATCH', {}, data, success, fail)
}
get(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'GET', data, {}, success, fail)
},
head(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'HEAD', data, {}, success, fail)
},
delete(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'DELETE', data, {}, success, fail)
},
post(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'POST', {}, data, success, fail)
},
put(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'PUT', {}, data, success, fail)
},
patch(url: '', data = {}, success = {}, fail?: Function) {
return baseRequests(url, 'PATCH', {}, data, success, fail)
}
}
export default ajax

View File

@ -4,213 +4,115 @@
* Distributed under terms of the MIT license.
*/
import Vue from 'vue'
import {Base64} from 'js-base64'
import {App} from 'vue'
import ajax from './ajax'
import store from '@/store'
import {store} from '../store'
import {Base64} from 'js-base64'
export type SuccessFunction<T> = (e: any) => void;
export type FailedFunction<T> = (e: any) => void;
const Code = {
42011: '无操作权限',
22031: '资源不存在 或 您无权操作该资源'
42011: '无操作权限',
22031: '资源不存在 或 您无权操作该资源'
}
class Interface {
private readonly method: Function
private readonly api: string
private readonly data: any
private readonly method: Function
private readonly api: string
private readonly data: any
constructor(method: Function, api: string, data?: any) {
this.method = method
this.api = api
this.data = data
}
Start(success: SuccessFunction<any>, fail?: FailedFunction<any>) {
const newFail = function (data: any) {
if (data && data.code === 40001) {
// no login
store.dispatch('handleLogout')
return
}
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
if (data && data.code > 0 && Code[data.code]) {
}
if (fail) {
fail(data)
}
constructor(method: Function, api: string, data?: any) {
this.method = method
this.api = api
this.data = data
}
const newSuccess = function (data: any) {
if (Number(data.status) === 1) {
if (success) {
success(data.content)
Start(success: SuccessFunction<any>, fail?: FailedFunction<any>) {
const newFail = function (data: any) {
if (data && data.code === 40001) {
// no login
store.commit('user/logout')
return
}
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
if (data && data.code && Code[data.code]) {
}
if (fail) {
fail(data.err)
}
}
} else {
newFail(data)
if (data.code === 41001) {
store.dispatch('handleLogout')
// bus.$emit('log_out')
const newSuccess = function (data: any) {
if (Number(data.status) === 1) {
if (success) {
success(data.content)
}
} else {
newFail(data)
if (data.code === 41001) {
store.commit('user/logout')
// bus.$emit('log_out')
}
}
}
}
this.method(this.api, this.data, newSuccess, newFail)
}
this.method(this.api, this.data, newSuccess, newFail)
}
}
const message = {
count() {
return new Interface(ajax.get, '/api/message/', {
count: true,
status: 'UnRead'
})
},
get_content(id: number) {
return new Interface(ajax.get, '/api/message/' + Number(id))
},
list(status: string) {
return new Interface(ajax.get, '/api/message/', {status})
},
update(id: number, status: string) {
return new Interface(ajax.patch, '/api/message/' + Number(id), {status})
}
}
const role = {
local: '/api/role/',
get(id: number) {
return new Interface(ajax.get, this.local + id)
},
list() {
return new Interface(ajax.get, this.local)
},
update(id: number, props: any) {
return new Interface(ajax.patch, this.local + id, props)
},
create(props: any) {
return new Interface(ajax.post, this.local, props)
},
del(id: number) {
return new Interface(ajax.delete, this.local + id)
},
bind(id: number, aid: number) {
return new Interface(ajax.get, this.local + id + '/bind/' + aid)
},
unbind(id: number, aid: number) {
return new Interface(ajax.get, this.local + id + '/unbind/' + aid)
}
}
const app = {
local: '/api/app/',
self() {
return new Interface(ajax.get, this.local, {is_self: true})
},
get(id: string) {
return new Interface(ajax.get, this.local + id)
},
list() {
return new Interface(ajax.get, this.local)
}
local: '/api/app/',
self() {
return new Interface(ajax.get, this.local, {is_self: true})
},
get(id: string) {
return new Interface(ajax.get, this.local + id)
},
list() {
return new Interface(ajax.get, this.local)
}
}
const user = {
local: '/api/user/',
register(username: string, password: string, uuid: string, prop?: any) {
const data = Object.assign({
username: username,
uuid: uuid,
password: Base64.encode(password)
}, prop)
return new Interface(ajax.post, this.local, data)
},
login(username: string, password: string, uuid: string) {
return new Interface(ajax.head, this.local + username, {
uid_type: 'username',
uuid: uuid,
password: Base64.encode(password)
})
}
}
const api = {
role: role,
app: app,
user: user,
admin: {
auths() {
return new Interface(ajax.get, '/api/auth/')
local: '/api/user/',
register(username: string, password: string, uuid: string, prop?: any) {
const data = Object.assign({
username: username,
uuid: uuid,
password: Base64.encode(password)
}, prop)
return new Interface(ajax.post, this.local, data)
},
user: {
create(props: any) {
const p = Object.assign({}, props)
p.password = Base64.encode(props.password)
return new Interface(ajax.post, '/api/user/', p)
},
update(user_id: number, props: any) {
return new Interface(ajax.patch, '/api/user/' + user_id, props)
},
enable(user_id: number) {
return new Interface(ajax.patch, '/api/user/' + user_id, {
status: 'ok'
login(username: string, password: string, uuid: string) {
return new Interface(ajax.head, this.local + username, {
uid_type: 'username',
uuid: uuid,
password: Base64.encode(password)
})
},
disable(user_id: number) {
return new Interface(ajax.patch, '/api/user/' + user_id, {
status: 'disabled'
})
},
attach_role(user_id: number, props: any) {
return new Interface(ajax.post, '/api/user/' + user_id + '/role/', props)
},
detach_role(user_id: number, id: any) {
return new Interface(ajax.delete, '/api/user/' + user_id + '/role/' + id)
},
reset_pass(user_id: number, password: string) {
return new Interface(ajax.patch, '/api/user/' + user_id, {password})
}
}
},
auth: {
event() {
return {
local: '/api/user/event/',
list() {
return new Interface(ajax.get, this.local)
},
create(title: string, tag: string, start_date: any, end_date: any) {
return new Interface(ajax.post, this.local, {title, tag, start_date, end_date})
},
del(id: number) {
return new Interface(ajax.delete, this.local + id)
}
}
},
favorite(name: string, tag: string, ok: boolean) {
if (ok) {
return new Interface(ajax.post, '/api/user/favorite', {name, tag})
}
return new Interface(ajax.delete, '/api/user/favorite', {name, tag})
},
get(id: number) {
return new Interface(ajax.get, '/api/user/' + id)
return new Interface(ajax.get, this.local + id)
},
search(username: string) {
return new Interface(ajax.get, '/api/user/', {
username
})
list() {
return new Interface(ajax.get, this.local)
},
update(id: number, props: any) {
return new Interface(ajax.patch, this.local + id, props)
}
},
message: message
}
const api = {
user: user,
app: app
}
const Api = {
install(vue: typeof Vue): void {
vue.prototype.$api = api
}
install(vue: App): void {
vue.config.globalProperties.$api = api
}
}
export {Api}
export default api

1
oaf/src/assets/icon.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

Before

Width:  |  Height:  |  Size: 539 B

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Docs
</a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>

View File

@ -1,43 +1,32 @@
<template>
<div id="wx_reg"></div>
</template>
<script lang='ts'>
import {Component, Vue, Prop} from 'vue-property-decorator'
import '@/libs/wwLogin.js'
<script setup lang='ts'>
import {onMounted} from 'vue'
@Component({
components: {}
})
export default class WxLogin extends Vue {
goto(id: string, app: string, url: string, state?: number, href?: string) {
// eslint-disable-next-line
// @ts-ignore
window.WwLogin({
id: 'wx_reg',
appid: id,
agentid: app,
redirect_uri: encodeURIComponent(url),
state: state,
href: href
})
}
@Prop({default: ''})
aid = ''
@Prop({default: ''})
app = ''
@Prop({default: ''})
url = ''
mounted() {
this.goto(this.aid, this.app, this.url, new Date().getTime())
}
created() {
}
function goto(id: string, app: string, url: string, state?: number, href?: string) {
// eslint-disable-next-line
// @ts-ignore
window.WwLogin({
id: 'wx_reg',
appid: id,
agentid: app,
redirect_uri: encodeURIComponent(url),
state: state,
href: href
})
}
let aid = ''
let app = ''
let url = ''
onMounted(() => {
goto(aid, app, url, new Date().getTime())
})
</script>
<style scoped>

View File

@ -0,0 +1,29 @@
<template>
<div class="core rounded-2xl p-3">
<div class="grid gap-4 grid-cols-5">
<div class="col-span-2">
<n-avatar @click="$router.push({name: 'app', params: {uuid: core.uuid}})" round :size="80" :src="core.icon">
{{ core.icon ? '' : core.name }}
</n-avatar>
</div>
<div class="col-span-3 grid grid-cols-1 items-center text-left">
<div class="h-10 flex items-center text-2xl italic font-bold">{{ core.name }}</div>
<div class="select-all">{{ core.uuid }}</div>
</div>
</div>
<textarea disabled style="background: none;border: none" class="focus:outline-none w-full">{{core.des}}</textarea>
</div>
</template>
<script setup lang='ts'>
import {defineProps} from "vue";
let props = defineProps<{
core: any
}>()
</script>
<style scoped>
.core {
width: 256px;
background: #2c3e50;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<base_frame style="line-height:40px" v-model="shown" :isDark="IsDark">
<div class="flex">
<n-avatar :src="$store.state.user.icon" round></n-avatar>
</div>
<template v-slot:main>
<div style="height: 100%">
<div style="height: calc(100% - 50px)">
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="color: #777">我的账户</span>
<span @click="$router.push({name: 'user_setting'});shown=false" class="cursor-pointer"
style="color:#f36828">账户中心</span>
</div>
<div class="grid grid-cols-4 gap-4 h-20">
<div class="flex items-center justify-center">
<n-avatar size="50" :src="$store.state.user.icon" round></n-avatar>
</div>
<div class="col-span-2 text-xs grid grid-cols-1 items-center" style="">
<span>昵称: &ensp;&ensp; {{ $store.state.user.nickname }}</span>
<span>账户: &ensp;&ensp; {{ $store.state.user.username }}</span>
<span>邮箱: &ensp;&ensp; {{ $store.state.user.email }}</span>
</div>
<div class="">123</div>
</div>
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
</div>
</div>
<hr style="border:none;border-top:2px solid #777;">
<div style="height: 48px">
<div @click="$store.commit('user/logout')"
class="w-full h-full flex justify-center items-center cursor-pointer transition duration-500 ease-in-out transform hover:scale-125">
<one-icon :color="IsDark?'#eee': '#333'" class="inline-block" style="font-size: 24px;">
logout
</one-icon>
<div>
退出登录
</div>
</div>
</div>
</div>
</template>
</base_frame>
</template>
<script lang="ts" setup>
import base_frame from './frame.vue'
import {IsDark} from '../../theme'
import {ref} from "vue";
let shown = ref(false)
function asd(e) {
console.log([e, shown.value])
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,82 @@
<template>
<div>
<div @click="setValue(true)">
<slot>
</slot>
</div>
<div @click.self="setValue(false)" class="core" style="height: 100vh;width: 100vw;" v-if="props.modelValue">
<div style="height: 100%; width: 300px" class="core-right">
<transition appear enter-active-class="animate__slideInRight">
<div class="right-title animate__animated animate__faster">
<slot name="title"></slot>
<div class="flex items-center float-right h-full px-1">
<one-icon @click="setValue(false)" color="#fff" style="font-size: 24px">close</one-icon>
</div>
</div>
</transition>
<div class="right-main">
<transition appear enter-active-class="animate__slideInDown">
<div class="right-main-core animate__animated animate__faster"
:style="{'background': props.isDark ? '#222': '#eee'}">
<slot name="main"></slot>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, watch} from "vue";
let emits = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let props = defineProps<{
isDark: boolean,
modelValue: boolean
}>()
function setValue(b: boolean) {
emits('update:modelValue', b)
}
</script>
<style scoped>
.core {
position: fixed;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
}
.core-right {
position: absolute;
right: 0;
top: 0;
}
.right-main {
width: 100%;
height: calc(100% - 50px);
overflow: hidden;
}
.right-main-core {
height: 100%;
width: 100%;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
--animate-duration: 400ms;
}
.right-title {
width: 100%;
height: 50px;
line-height: 50px;
background: linear-gradient(90deg, #f74d22, #fa9243);
}
</style>

View File

@ -0,0 +1,3 @@
import avatar from './avatar.vue'
export default avatar

View File

@ -1,20 +0,0 @@
<template>
<div></div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
@Component({
components: {}
})
export default class Demo extends Vue {
mounted() {
}
created() {
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,10 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,71 @@
<template>
<div @click="handleFullscreen">
<one-icon>{{ props.modelValue ? 'fullscreen-exit' : 'fullscreen' }}</one-icon>
</div>
</template>
<script lang="ts" setup>
import {defineEmits, onMounted, defineProps} from "vue";
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let props = defineProps<{
modelValue: boolean
}>()
function handleFullscreen() {
let main = document.body
if (props.modelValue) {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
} else {
if (main.requestFullscreen) {
main.requestFullscreen()
} else if (main.mozRequestFullScreen) {
main.mozRequestFullScreen()
} else if (main.webkitRequestFullScreen) {
main.webkitRequestFullScreen()
} else if (main.msRequestFullscreen) {
main.msRequestFullscreen()
}
}
}
onMounted(() => {
let isFullscreen =
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen
isFullscreen = !!isFullscreen
document.addEventListener('fullscreenchange', () => {
emit('update:modelValue', !props.modelValue)
})
document.addEventListener('mozfullscreenchange', () => {
emit('update:modelValue', !props.modelValue)
})
document.addEventListener('webkitfullscreenchange', () => {
emit('update:modelValue', !props.modelValue)
})
document.addEventListener('msfullscreenchange', () => {
emit('update:modelValue', !props.modelValue)
})
emit('update:modelValue', isFullscreen)
})
</script>
<style>
</style>

View File

@ -0,0 +1,2 @@
import fullscreen from './fullscreen.vue'
export default fullscreen

View File

@ -1,28 +0,0 @@
<template>
<svg class="icon" aria-hidden="true">
<use :xlink:href="'#icon-'+icon"></use>
</svg>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
@Component({
components: {}
})
export default class OneIcon extends Vue {
get icon() {
if (this.$slots.default) return this.$slots.default[0].text?.trim()
console.warn('blank icon name')
return ''
}
}
</script>
<style scoped>
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -1,26 +0,0 @@
import Vue from 'vue'
import OneIcon from './icon.vue'
function loadJS(url: string) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
document.getElementsByTagName('head')[0].appendChild(script)
}
export default {
installed: false,
install(vue: typeof Vue, options?: { href: '' }): void {
if (this.installed) {
return
}
this.installed = true
if (options && options.href) {
console.log(options.href)
loadJS(options.href)
} else {
console.error('not set iconfont href')
}
vue.component('one-icon', OneIcon)
}
}

8
oaf/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

6
oaf/src/index.css Normal file
View File

@ -0,0 +1,6 @@
/* ./src/index.css */
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -27,6 +27,9 @@ const util = {
document.cookie =
name + '=' + escape(value) + ';expires=' + exp.toLocaleString()
},
getToken() {
return localStorage.auth_token
},
checkLogin() {
// return parseInt(this.getCookie('stat')) === 1
return Boolean(localStorage.auth_token)

View File

@ -1,24 +1,19 @@
import Vue from 'vue'
import {createApp} from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
import {Api} from '@/api'
import {store, key} from './store'
import OneIcon from '@veypi/one-icon'
import Message from 'vue-m-message'
import 'vue-m-message/dist/index.css'
import naive from 'naive-ui'
import './index.css'
import {Api} from './api'
import './assets/icon.js'
import 'animate.css'
Vue.use(Message) // will mount `Vue.prototype.$message`
const app = createApp(App)
// Vue.use(OneIcon, {href: 'https://at.alicdn.com/t/font_2872366_7aws02sx9bl.js'})
Vue.use(OneIcon, {href: './icon.js'})
Vue.use(Api)
Vue.config.productionTip = false
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
app.use(Api)
app.use(naive)
app.use(OneIcon)
app.use(router)
app.use(store, key)
app.mount('#app')

View File

@ -1,24 +0,0 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
Vue.use(Vuetify)
const light = {
primary: '#2196f3',
secondary: '#00bcd4',
accent: '#3f51b5',
error: '#f44336',
warning: '#ff5722',
info: '#ff9800',
success: '#4caf50',
reset: '#684bff'
}
export default new Vuetify({
theme: {
dark: false,
themes: {
light: light
}
}
})

View File

@ -1,57 +1,83 @@
import Vue from 'vue'
import VueRouter, {RouteConfig} from 'vue-router'
import Home from '../views/Home.vue'
import Demo from '@/views/demo.vue'
import Login from '@/views/login.vue'
import Register from '@/views/register.vue'
import NotFound from '@/views/404.vue'
import {createRouter, createWebHistory} from 'vue-router'
import util from '../libs/util'
Vue.use(VueRouter)
// 避免push到相同路径报错
// 获取原型对象上的push函数
const originalPush = VueRouter.prototype.push
// 修改原型对象中的push方法
VueRouter.prototype.push = function push(location: any) {
// eslint-disable-next-line
// @ts-ignore
return originalPush.call(this, location).catch(err => err)
declare module 'vue-router' {
interface RouteMeta {
// 是可选的
isAdmin?: boolean
// 每个路由都必须声明
requiresAuth: boolean
}
}
const routes: Array<RouteConfig> = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/app',
name: 'app',
component: Demo
},
{
path: '/login/:uuid?',
name: 'login',
component: Login
},
{
path: '/register/:uuid?',
name: 'register',
component: Register
},
{
path: '/wx',
name: 'wx',
component: () => import('../views/wx.vue')
},
{
path: '*',
name: '404',
component: NotFound
}
]
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
meta: {
requiresAuth: true,
},
component: () => import('../views/home.vue')
},
{
path: '/app/:uuid?',
name: 'app',
meta: {
requiresAuth: true,
},
component: () => import('../views/app.vue')
},
{
path: '/user/setting',
name: 'user_setting',
meta: {
requiresAuth: true
},
component: () => import('../views/user_setting.vue')
},
{
path: '/about',
name: 'about',
component: () => import('../views/about.vue')
},
{
path: '/wx',
name: 'wx',
component: () => import('../views/wx.vue')
},
{
path: '/login/:uuid?',
name: 'login',
component: () => import('../views/login.vue')
},
{
path: '/register/:uuid?',
name: 'register',
component: () => import('../views/register.vue')
},
{
path: '/:path(.*)',
name: '404',
component: () => import('../views/404.vue')
}
//...
],
})
const router = new VueRouter({
routes
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !util.checkLogin()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
name: 'login',
// 保存我们所在的位置,以便以后再来
query: {redirect: to.fullPath},
}
}
})
export default router

View File

@ -1,13 +0,0 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

View File

@ -1,5 +0,0 @@
declare module '*.js'
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

View File

@ -1,4 +0,0 @@
declare module 'vuetify/lib/framework' {
import Vuetify from 'vuetify'
export default Vuetify
}

View File

@ -1,30 +1,49 @@
import Vue from 'vue'
import Vuex from 'vuex'
import api from '@/api'
import router from '@/router'
import {InjectionKey} from 'vue'
import {createStore, useStore as baseUseStore, Store} from 'vuex'
import api from "../api";
import {User, UserState} from './user'
Vue.use(Vuex)
export interface State extends Object {
oauuid: string
user: UserState
apps: []
}
export default new Vuex.Store({
state: {
oauuid: '',
user: null
},
mutations: {
setOA(state: any, data: any) {
state.oauuid = data.uuid
}
},
actions: {
fetchSelf({commit}) {
api.app.self().Start(d => {
commit('setOA', d)
})
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
modules: {
user: User
},
handleLogout() {
localStorage.removeItem('auth_token')
router.push({name: 'login'})
// @ts-ignore
state: {
oauuid: '',
apps: []
},
getters: {},
mutations: {
setOA(state: any, data: any) {
state.oauuid = data.uuid
},
setApps(state: State, data: any) {
state.apps = data
}
},
actions: {
fetchSelf({commit}) {
api.app.self().Start(d => {
commit('setOA', d)
})
},
fetchApps({commit}) {
api.app.list().Start(e => {
commit('setApps', e)
})
}
}
},
modules: {}
})
// 定义自己的 `useStore` 组合式函数
export function useStore() {
return baseUseStore(key)
}

73
oaf/src/store/user.ts Normal file
View File

@ -0,0 +1,73 @@
import {Module} from "vuex";
import api from "../api";
import util from '../libs/util'
import {Base64} from 'js-base64'
import {State} from './index'
import router from "../router";
export interface UserState {
id: number
username: string
nickname: string
phone: string
icon: string
email: string
ready: boolean
auth: [auth?]
[key: string]: any
}
interface auth {
rid: string
ruid: string
level: number
}
export const User: Module<UserState, State> = {
namespaced: true,
state: {
id: 0,
username: '',
nickname: '',
phone: '',
icon: '',
email: '',
auth: [],
ready: false
},
mutations: {
setBase(state: UserState, data: any) {
state.id = data.id
state.icon = data.icon
state.username = data.username
state.nickname = data.nickname
state.phone = data.phone
state.email = data.email
state.ready = true
},
setAuth(state: UserState, data: any) {
state.auth = data
},
logout(state: UserState) {
state.ready = false
localStorage.removeItem('auth_token')
router.push({name: 'login'})
}
},
actions: {
fetchUserData(context) {
let token = util.getToken()?.split('.');
if (!token || token.length !== 3) {
return false
}
let data = JSON.parse(Base64.decode(token[1]))
if (data.id > 0) {
context.commit('setAuth', data.auth)
api.user.get(data.id).Start(e => {
context.commit('setBase', e)
})
}
}
}
}

72
oaf/src/theme/index.ts Normal file
View File

@ -0,0 +1,72 @@
import {darkTheme} from 'naive-ui/lib/themes'
import {BuiltInGlobalTheme} from 'naive-ui/lib/themes/interface'
import {lightTheme} from 'naive-ui/lib/themes/light'
import {ref} from 'vue'
import {useOsTheme, GlobalThemeOverrides} from 'naive-ui'
interface builtIn extends BuiltInGlobalTheme {
overrides: GlobalThemeOverrides
me: {
lightBox: string,
lightBoxShadow: string
}
}
let light = lightTheme as builtIn
let dark = darkTheme as builtIn
let intputNone = {
color: 'url(0) no-repeat',
colorFocus: 'url(0) no-repeat',
colorFocusWarning: 'url(0) no-repeat',
colorFocusError: 'url(0) no-repeat'
}
light.overrides = {
Input: Object.assign({}, intputNone)
}
dark.overrides = {
Input: Object.assign({
border: '1px solid #aaa'
}, intputNone)
}
light.common.cardColor = '#f4f4f4'
light.common.bodyColor = '#eee'
dark.common.bodyColor = '#2e2e2e'
light.me = {
lightBox: '#f4f4f4',
lightBoxShadow: '18px 18px 36px #c6c6c6, -18px -18px 36px #fff'
}
dark.me = {
lightBox: '#2e2e2e',
lightBoxShadow: '21px 21px 42px #272727, -21px -21px 42px #353535'
}
export const OsThemeRef = useOsTheme()
let theme = 'light'
export let Theme = ref(light)
export let IsDark = ref(false)
function change(t: string) {
if (t === 'dark') {
theme = 'dark'
Theme.value = dark
} else {
theme = 'light'
Theme.value = light
}
IsDark.value = theme === 'dark'
}
export function ChangeTheme() {
if (IsDark.value) {
change('light')
} else {
change('dark')
}
}
if (OsThemeRef.value === 'dark') {
change('dark')
}

View File

@ -1,14 +0,0 @@
// 1. 确保在声明补充的类型之前导入 'vue'
import Vue from 'vue'
import api from '@/api'
export type PluginFunction<T> = (Vue: typeof Vue, options?: T) => void;
// 2. 定制一个文件,设置你想要补充的类型
// 在 types/vue.d.ts 里 Vue 有构造函数类型
declare module 'vue/types/vue' {
// 3. 声明为 Vue 补充的东西
interface Vue {
$api: typeof api;
}
}

View File

@ -1,24 +1,32 @@
<style>
</style>
<template>
<div class='home d-flex justify-center align-center'>
<one-icon style="font-size: 100px">404</one-icon>
<div class="flex justify-center items-center">
<div class="text-center text-xl">
<one-icon style="font-size: 200px">404</one-icon>
<span>
路径失效啦! {{count}}
</span>
</div>
</div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
import util from '@/libs/util'
<script lang="ts" setup>
import {useRouter, useRoute} from 'vue-router'
import {onMounted, ref} from "vue";
@Component({
components: {}
const route = useRoute()
const router = useRouter()
let count = ref(5)
onMounted(() => {
console.log([route.path, route.params])
let timer = setInterval(()=> {
count.value--
if (count.value === 0) {
router.push('/')
clearInterval(timer)
}
}, 1000)
})
export default class NotFound extends Vue {
mounted() {
}
created() {
util.title('404')
}
}
</script>
<style scoped>
</style>

View File

@ -1,43 +0,0 @@
<style>
.home {
height: 100%;
width: 100%;
}
</style>
<template>
<div class='home d-flex justify-center align-center'>
<one-icon style="color: aqua;font-size: 50px">glassdoor</one-icon>
</div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
import util from '@/libs/util'
@Component({
components: {}
})
export default class Home extends Vue {
apps = []
getApps() {
this.$api.app.list().Start(d => {
console.log(d)
this.apps = d
})
}
mounted() {
this.getApps()
}
created() {
}
beforeCreate() {
if (!util.checkLogin()) {
this.$router.push({name: 'login', query: this.$route.query, params: this.$route.params})
}
}
}
</script>

12
oaf/src/views/about.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<div>
about
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

28
oaf/src/views/app.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<div>
{{ uuid }}
</div>
</template>
<script lang="ts" setup>
import {useRoute, useRouter} from "vue-router";
import {computed, onMounted} from "vue";
import api from "../api";
let route = useRoute()
let router = useRouter()
let uuid = computed(() => route.params.uuid)
onMounted(() => {
if (uuid.value === '') {
router.push({name: '404', params: {path: route.path}})
return
}
api.app.get(uuid.value as string).Start(e => {
console.log(e)
})
})
</script>
<style scoped>
</style>

View File

@ -1,21 +1,10 @@
<style>
</style>
<template>
<div class='home d-flex justify-center align-center'>
</div>
<div></div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
@Component({
components: {}
})
export default class Demo extends Vue {
mounted() {
}
created() {
}
}
<script lang="ts" setup>
</script>
<style scoped>
</style>

10
oaf/src/views/file.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

33
oaf/src/views/home.vue Normal file
View File

@ -0,0 +1,33 @@
<template>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div class="flex items-center justify-center" v-for="(item, k) in apps" :key="k">
<AppCard :core="item"></AppCard>
</div>
<div class="flex items-center justify-center" v-for="(item) in '1234567890'" :key="item">
<AppCard :core="{}"></AppCard>
</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref} from "vue";
import api from "../api";
import AppCard from '../components/app.vue'
let apps = ref([])
function getApps() {
api.app.list().Start(e => {
apps.value = e
})
}
onMounted(() => {
getApps()
})
</script>
<style scoped>
</style>

View File

@ -1,123 +1,98 @@
<style>
</style>
<template>
<v-row align="center" class="fill-height" justify="center" style="background: #ebebeb">
<v-col cols="12" sm="8" md="6" lg="4" xl="3">
<v-card class="elevation-12 mx-5" style="opacity: 0.8">
<v-row justify="center">
<v-col cols="10">
<v-card class="elevation-1 mt-n12 primary theme--dark">
<v-card-text class="text-center">
<h1 class="display-2 font-weight-bold mb-2">Login</h1>
<v-tooltip left>
<template v-slot:activator="{ on }">
<v-btn icon large v-on="on">
<v-icon>mdi-cellphone</v-icon>
</v-btn>
</template>
<span style="font-family:'Noto Sans Armenian'">手机登录</span>
</v-tooltip>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-btn icon large v-on="on">
<v-icon>mdi-barcode</v-icon>
</v-btn>
</template>
<span>授权码登录</span>
</v-tooltip>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-card-text>
<v-form ref="form">
<v-text-field
v-model="formInline.user"
:counter="16"
:rules="ruleInline.user"
label="账号"
required
prepend-inner-icon="mdi-account-circle"
></v-text-field>
<v-text-field
v-model="formInline.password"
type="password"
:counter="16"
:rules="ruleInline.password"
label="密码"
prepend-inner-icon="mdi-lock"
@keyup.enter="handleSubmit"
required
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn type="primary" @click="handleSubmit">登录</v-btn>
<router-link :to="{name: 'register', query:$route.query, params: $route.params}"
style="text-decoration: none;">
<v-btn type="primary" style="margin-left:8px">注册</v-btn>
</router-link>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<div class="flex items-center justify-center">
<div
:style="{background:Theme.me.lightBox, 'box-shadow': Theme.me.lightBoxShadow}"
class="px-10 pb-9 pt-28 rounded-xl w-96">
<n-form label-width="70px" label-align="left" :model="data" ref="form_ref" label-placement="left" :rules="rules">
<n-form-item required label="用户名" path="username">
<n-input @keydown.enter="divs[1].focus()" :ref="el => {if (el)divs[0]=el}"
v-model:value="data.username"></n-input>
</n-form-item>
<n-form-item required label="密码" path="password">
<n-input @keydown.enter="login" :ref="el => {if (el) divs[1]=el}" v-model:value="data.password"
type="password"></n-input>
</n-form-item>
<div class="flex justify-around mt-4">
<n-button @click="login">登录</n-button>
<n-button @click="router.push({name:'register'})">注册</n-button>
</div>
</n-form>
</div>
</div>
</template>
<script lang="ts" setup>
import {computed, onMounted, ref} from "vue";
import {Theme} from "../theme";
import {useMessage} from 'naive-ui'
import api from "../api"
import {useRoute, useRouter} from "vue-router";
import {store} from "../store";
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
import util from '@/libs/util'
let msg = useMessage()
const route = useRoute()
const router = useRouter()
@Component({
components: {}
const divs = ref([])
let form_ref = ref(null)
let data = ref({
username: '',
password: ''
})
export default class Login extends Vue {
formInline = {
user: '',
password: ''
}
ruleInline = {
user: [
(v: string) => !!v || 'required',
(v: string) => (v && v.length >= 3 && v.length <= 16) || '长度要求3~16'
],
password: [
(v: string) => !!v || 'required',
(v: string) => (v && v.length >= 6 && v.length <= 16) || '长度要求6~16'
]
}
get app_uuid() {
return this.$route.params.uuid || this.$store.state.oauuid
}
handleSubmit() {
// eslint-disable-next-line
// @ts-ignore
if (!this.$refs.form.validate()) {
return
}
this.$api.user.login(this.formInline.user, this.formInline.password, this.app_uuid).Start(
data => {
console.log(data)
if (util.checkLogin()) {
// this.$message.success('')
// EventBus.$emit('login', true)
this.$nextTick(() => {
if (this.$route.query.redirect) {
window.location.href = this.$route.query.redirect as string
}
this.$router.push({name: 'home'})
})
} else {
// this.$message.error('')
}
let rules = {
username: [
{
required: true,
validator(r: any, v: any) {
return (v && v.length >= 3 && v.length <= 16) || new Error('长度要求3~16')
},
() => {
// this.$message.error('')
}
)
}
trigger: ['input', 'blur']
}
],
password: [{
required: true,
validator(r: any, v: any) {
return (v && v.length >= 6 && v.length <= 16) || new Error('长度要求6~16')
}
}]
}
let uuid = computed(() => {
return route.params.uuid || store.state.oauuid
})
function login() {
// @ts-ignore
form_ref.value.validate((e:any) => {
if (!e) {
api.user.login(data.value.username, data.value.password, uuid.value as string).Start((url: string) => {
msg.success('登录成功')
store.dispatch('user/fetchUserData')
let target = url
if (route.query.redirect) {
target = route.query.redirect as string
}
if (target && target.startsWith('http')) {
window.location.href = target
} else if (target) {
router.push(target)
} else {
router.push({name: 'home'})
}
}, e => {
console.log(e)
msg.warning('登录失败:' + e)
})
}
})
}
onMounted(() => {
if (divs.value[0]) {
// @ts-ignore
divs.value[0].focus()
}
})
</script>
<style scoped>
</style>

View File

@ -1,124 +1,10 @@
<style>
</style>
<template>
<v-row class="fill-height" align="center" justify="center" style="background: #ebebeb">
<v-col cols="12" sm="8" md="6" lg="4" xl="3">
<v-card class="elevation-12 mx-5" style="opacity: 0.8">
<v-row justify="center">
<v-card class="elevation-1 mt-n7 primary" style="width: 80%">
<v-card-actions>
<v-row>
<v-icon
style="position: absolute;left: 10px;top:19px;z-index: 1"
@click="$router.back()"
size="36"
>mdi-arrow-left-circle
</v-icon>
<v-col cols="12" class="text-center">
<h1 class="display-2 ">注册</h1>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-row>
<v-card-text class="text-center">
<v-form ref="form">
<v-text-field
type="text"
prepend-inner-icon="mdi-account-circle"
v-model="form.username"
label="账号"
:rules="ruleInline.user"
:counter="16"
>
</v-text-field>
<v-text-field
type="password"
v-model="form.passwd"
label="密码"
prepend-inner-icon="mdi-lock"
:rules="ruleInline.password"
:counter="16"
></v-text-field>
<v-text-field
type="password"
v-model="form.passwdCheck"
label="密码"
prepend-inner-icon="mdi-lock"
:rules="ruleInline.passwordCheck"
:counter="16"
@keyup.enter="handleSubmit"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="primary" @click="handleSubmit">提交</v-btn>
<v-btn @click="handleReset()">重置</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<div></div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
@Component({
components: {}
})
export default class Register extends Vue {
form = {
passwd: '',
passwdCheck: '',
email: '',
username: ''
}
ruleInline = {
user: [
(v: string) => !!v || 'required',
(v: string) => (v && v.length >= 3 && v.length <= 16) || '长度要求3~16'
],
password: [
(v: string) => !!v || 'required',
(v: string) => (v && v.length >= 6 && v.length <= 16) || '长度要求6~16'
],
passwordCheck: [
(v: string) => !!v || 'required',
(v: string) => (v && v === this.form.passwd) || '密码不一致'
]
}
get app_uuid() {
return this.$route.params.uuid || this.$store.state.oauuid
}
handleSubmit() {
// eslint-disable-next-line
// @ts-ignore
if (!this.$refs.form.validate()) {
return
}
this.$api.user.register(this.form.username, this.form.passwd, this.app_uuid).Start(
(data) => {
this.$message.success('注册成功!')
this.$router.push({name: 'login', params: this.$route.params, query: this.$route.query})
},
(data) => {
if (data && data.code === '31011') {
this.$message.error('用户名重复')
} else {
this.$message.error('注册失败')
}
}
)
}
handleReset() {
this.form.username = ''
this.form.passwd = ''
this.form.passwdCheck = ''
}
}
<script lang="ts" setup>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="pt-10">
<div class="flex justify-center">
<div class="relative rounded-xl text-lg text-black" :style="{background: IsDark?'#555': '#d5d5d5'}">
<div @click="ifInfo=true" class="inline-block px-5 rounded-xl" :style="{background: ifInfo ? '#fc0005': ''}">
个人信息
</div>
<div @click="ifInfo=false" class="inline-block px-5 rounded-xl" :style="{background: ifInfo ? '': '#fc0005'}">
账户管理
</div>
</div>
</div>
<div class="inline-block flex justify-center mt-10">
<transition mode="out-in" enter-active-class="animate__fadeInLeft" leave-active-class="animate__fadeOutRight">
<div v-if="ifInfo" class="animate__animated animate__faster">
<n-form label-placement="left" label-width="80px" label-align="left">
<n-form-item label="昵称">
<n-input v-model:value="user.nickname" @blur="update('nickname')"></n-input>
</n-form-item>
<n-form-item label="头像">
<n-upload
action=""
:headers="{'': ''}"
:data="{}"
>
<n-avatar size="large" round :src="user.icon">
</n-avatar>
</n-upload>
</n-form-item>
</n-form>
</div>
<div v-else class="animate__animated animate__faster">
<n-form label-align="left" label-width="80px" label-placement="left">
<n-form-item label="username">
<n-input disabled v-model:value="user.username"></n-input>
</n-form-item>
<n-form-item label="phone">
<n-input v-model:value="user.phone" @blur="update('phone')"></n-input>
</n-form-item>
<n-form-item label="email">
<n-auto-complete :options="emailOptions" v-model:value="user.email"
@blur="update('email')"></n-auto-complete>
</n-form-item>
</n-form>
</div>
</transition>
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, computed} from "vue";
import {IsDark} from "../theme";
import {useStore} from "../store";
import api from "../api";
import {useMessage} from "naive-ui";
let msg = useMessage()
let store = useStore()
let ifInfo = ref(true)
let user = ref({
username: store.state.user.username,
nickname: store.state.user.nickname,
icon: store.state.user.icon,
email: store.state.user.email,
phone: store.state.user.phone,
})
let emailOptions = computed(() => {
return ['@gmail.com', '@163.com', '@qq.com'].map((suffix) => {
const prefix = user.value.email.split('@')[0]
return {
label: prefix + suffix,
value: prefix + suffix
}
})
})
function update(key: string) {
// @ts-ignore
let v = user.value[key]
if (v === store.state.user[key]) {
return
}
api.user.update(store.state.user.id, {[key]: v}).Start(e => {
msg.success('更新成功')
store.state.user[key] = v
}, e => {
msg.error('更新失败: ' + e.err)
})
}
</script>
<style scoped>
</style>

View File

@ -1,64 +1,49 @@
<template>
<div class='home d-flex justify-center align-center'>
<wx-login v-if="enable" :aid="aid" :app="agentID" :url="url"></wx-login>
<v-overlay :value="!enable">
<v-progress-circular
indeterminate
size="64"
></v-progress-circular>
</v-overlay>
</div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
import WxLogin from '@/components/WxLogin.vue'
<script setup lang='ts'>
import WxLogin from '../components/WxLogin.vue'
import {computed, onMounted} from "vue";
import {useRoute} from 'vue-router'
import api from '../api'
@Component({
components: {
WxLogin
let route = useRoute()
let aid = ''
let agentID = ''
let url = ''
let uuid = computed(() => {
return route.query.uuid
})
let enable = computed(() => {
return uuid && aid && agentID && url
})
let code = computed(() => {
return route.query.code
})
let state = computed(() => {
return route.query.state
})
let msg = computed(() => {
return route.query.msg
})
onMounted(() => {
if (msg) {
console.log(msg)
alert(msg)
}
})
export default class Wx extends Vue {
aid = ''
agentID = ''
url = ''
get enable() {
return this.uuid && this.aid && this.agentID && this.url
}
get uuid() {
return this.$route.query.uuid
}
get code() {
return this.$route.query.code
}
get state() {
return this.$route.query.state
}
get msg() {
return this.$route.query.msg
}
mounted() {
if (this.msg) {
console.log(this.msg)
alert(this.msg)
}
}
created() {
if (this.uuid) {
this.$api.app.get(this.uuid as string).Start(e => {
this.url = e.wx.url + '/api/wx/login/' + this.uuid
this.aid = e.wx.corp_id
this.agentID = e.wx.agent_id
})
}
}
if (uuid) {
api.app.get(uuid.value as string).Start(e => {
url = e.wx.url + '/api/wx/login/' + uuid
aid = e.wx.corp_id
agentID = e.wx.agent_id
})
}
</script>
<style scoped>

14
oaf/src/vuex.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import {ComponentCustomProperties} from 'vue'
import {Store} from 'vuex'
import {State as root} from './store'
declare module '@vue/runtime-core' {
// 声明自己的 store state
interface State extends root {
}
// 为 `this.$store` 提供类型声明
interface ComponentCustomProperties {
$store: Store<State>
}
}

11
oaf/tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

View File

@ -1,40 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

36
oaf/vite.config.ts Normal file
View File

@ -0,0 +1,36 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
// host: '0.0.0.0',
host: '127.0.0.1',
port: 8080,
proxy: {
'/api': {
target: 'http://127.0.0.1:4001/',
changeOrigin: true,
ws: true
},
'/media': {
target: 'http://127.0.0.1:4001/',
changeOrigin: true,
ws: true
}
}
},
build: {
outDir: '../sub/static/',
assetsDir: './',
rollupOptions: {
output: {
// 重点在这里哦
entryFileNames: `static/[name].[hash].js`,
chunkFileNames: `static/[name].[hash].js`,
assetFileNames: `static/[name].[hash].[ext]`
}
}
}
})

View File

@ -1,28 +0,0 @@
module.exports = {
transpileDependencies: [
'vuetify'
],
configureWebpack: {
output: {
filename: '[name].[hash].js'
}
},
outputDir: '../sub/static',
devServer: {
host: '0.0.0.0',
port: 19520,
disableHostCheck: true,
proxy: {
'^/api': {
target: 'http://127.0.0.1:4001',
ws: true,
changeOrigin: true
},
'^/media': {
target: 'http://127.0.0.1:4001',
ws: true,
changeOrigin: true
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import (
"OneAuth/libs/auth"
"OneAuth/models"
"github.com/urfave/cli/v2"
"github.com/veypi/utils/cmd"
"github.com/veypi/utils/log"
"strconv"
)
@ -20,6 +19,7 @@ func runInit(c *cli.Context) error {
}
// 初始化项目
var appid uint
func InitSystem() error {
db()
@ -27,15 +27,9 @@ func InitSystem() error {
if err != nil {
return err
}
cfg.CFG.APPID = self.ID
cfg.CFG.APPKey = self.Key
err = cmd.DumpCfg(cfg.Path, cfg.CFG)
// TODO
//if err != nil {
// return err
//}
appid = self.ID
err = role(self.InitRoleID == 0)
return nil
return err
}
func db() {
@ -56,14 +50,14 @@ func selfApp() (*models.App, error) {
self := &models.App{
Name: "OA",
Icon: "",
UUID: "jU5Jo5hM",
UUID: cfg.CFG.APPUUID,
Des: "",
Creator: 0,
UserCount: 0,
Hide: false,
Host: "",
UserRefreshUrl: "/",
Key: "cB43wF94MLTksyBK",
Key: cfg.CFG.APPKey,
EnableRegister: true,
EnableUserKey: true,
EnableUser: true,
@ -86,7 +80,7 @@ func role(reset_init_role bool) error {
}
var err error
adminRole := &models.Role{
AppID: cfg.CFG.APPID,
AppID: appid,
Name: "admin",
IsUnique: false,
}
@ -96,7 +90,7 @@ func role(reset_init_role bool) error {
}
for _, na := range n {
a := &models.Resource{
AppID: cfg.CFG.APPID,
AppID: appid,
Name: na,
Tag: "",
Des: "",
@ -112,7 +106,7 @@ func role(reset_init_role bool) error {
}
}
userRole := &models.Role{
AppID: cfg.CFG.APPID,
AppID: appid,
Name: "user",
IsUnique: false,
}
@ -120,12 +114,12 @@ func role(reset_init_role bool) error {
if err != nil {
return err
}
err = auth.BindRoleAuth(cfg.DB(), userRole.ID, authMap[auth.APP].ID, models.AuthRead, strconv.Itoa(int(cfg.CFG.APPID)))
err = auth.BindRoleAuth(cfg.DB(), userRole.ID, authMap[auth.APP].ID, models.AuthRead, strconv.Itoa(int(appid)))
if err != nil {
return err
}
if reset_init_role {
return cfg.DB().Model(&models.App{}).Where("id = ?", cfg.CFG.APPID).Update("init_role_id", adminRole.ID).Error
return cfg.DB().Model(&models.App{}).Where("id = ?", appid).Update("init_role_id", adminRole.ID).Error
}
return nil
}

View File

@ -1 +1,17 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>oaf</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"><link href="/css/chunk-6cfd0404.487f8d50.css" rel="prefetch"><link href="/js/about.a3055c06.js" rel="prefetch"><link href="/js/chunk-6cfd0404.65d5baaf.js" rel="prefetch"><link href="/app.b131f8d0f62acd99ab8e.js" rel="preload" as="script"><link href="/css/app.dafd5329.css" rel="preload" as="style"><link href="/css/chunk-vendors.dfe6062e.css" rel="preload" as="style"><link href="/js/chunk-vendors.83bac771.js" rel="preload" as="script"><link href="/css/chunk-vendors.dfe6062e.css" rel="stylesheet"><link href="/css/app.dafd5329.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but oaf doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.83bac771.js"></script><script src="/app.b131f8d0f62acd99ab8e.js"></script></body></html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" crossorigin src="/static/index.dddecd43.js"></script>
<link rel="modulepreload" href="/static/vendor.ba3bd51d.js">
<link rel="stylesheet" href="/static/vendor.3a295b6b.css">
<link rel="stylesheet" href="/static/index.c49db26f.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -6,16 +6,15 @@ import (
"embed"
"github.com/urfave/cli/v2"
"github.com/veypi/OneBD"
"github.com/veypi/OneBD/core"
"github.com/veypi/OneBD/rfc"
"github.com/veypi/utils/log"
"net/http"
"os"
)
//go:embed static
//go:embed static/static
var staticFiles embed.FS
//go:embed static/favicon.ico
var icon []byte
//go:embed static/index.html
var indexFile []byte
@ -37,34 +36,15 @@ func RunWeb(c *cli.Context) error {
LoggerPath: cfg.CFG.LoggerPath,
LoggerLevel: ll,
})
app.Router().EmbedFile("/", indexFile)
app.Router().EmbedDir("/", staticFiles, "static/")
// TODO media 文件需要检验权限
//app.Router().SubRouter("/media/").Static("/", cfg.CFG.EXEDir+"/media")
app.Router().SetNotFoundFunc(func(m core.Meta) {
f, err := os.Open(cfg.CFG.EXEDir + "/static/index.html")
if err != nil {
m.WriteHeader(rfc.StatusNotFound)
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
m.WriteHeader(rfc.StatusNotFound)
return
}
if info.IsDir() {
// TODO:: dir list
m.WriteHeader(rfc.StatusNotFound)
return
}
http.ServeContent(m, m.Request(), info.Name(), info.ModTime(), f)
})
api.Router(app.Router().SubRouter("api"))
// TODO media 文件需要检验权限
app.Router().SubRouter("/media/").Static("/", cfg.CFG.MediaDir)
app.Router().EmbedDir("/static", staticFiles, "static/static/")
app.Router().EmbedFile("/favicon.ico", icon)
app.Router().EmbedFile("/*", indexFile)
log.Info().Msg("\nRouting Table\n" + app.Router().String())
return app.Run()
}