登录功能完成
This commit is contained in:
7
pkg/common/errcode.go
Normal file
7
pkg/common/errcode.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
LoginErrorCode = 2001
|
||||
NoPermission = 2002
|
||||
LoginExpired = 2003
|
||||
)
|
||||
85
pkg/conf/conf.go
Normal file
85
pkg/conf/conf.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/mcuadros/go-defaults"
|
||||
"mc-client-updater-server/pkg/log"
|
||||
"mc-client-updater-server/pkg/util"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Common Common `toml:"common"`
|
||||
Database Database `toml:"database"`
|
||||
Security Security `toml:"security"`
|
||||
}
|
||||
|
||||
type Common struct {
|
||||
Host string `toml:"host" default:"0.0.0.0"`
|
||||
Port uint16 `toml:"port" default:"8080"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Host string `toml:"host" default:"localhost"`
|
||||
Port uint16 `toml:"port" default:"5432"`
|
||||
User string `toml:"user" default:"user"`
|
||||
Password string `toml:"password" default:"password"`
|
||||
DB string `toml:"db" default:"mc_client_updater"`
|
||||
}
|
||||
|
||||
type Security struct {
|
||||
PasswordEncoder string `toml:"password_encoder" default:"argon2id"`
|
||||
JWTSecret string `toml:"jwt_secret"`
|
||||
}
|
||||
|
||||
var (
|
||||
Conf *Config
|
||||
)
|
||||
|
||||
// 释放默认配置文件
|
||||
func releaseConfig(file string, config *Config) {
|
||||
f, err := util.CreateNestedFile(file)
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("创建默认配置文件失败: %s", err.Error())
|
||||
}
|
||||
|
||||
if config.Security.JWTSecret == "" {
|
||||
config.Security.JWTSecret = util.RandStr(32)
|
||||
}
|
||||
|
||||
encoder := toml.NewEncoder(f)
|
||||
err = encoder.Encode(config)
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("创建默认配置文件失败: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func InitConfig(file string) {
|
||||
Conf = &Config{}
|
||||
defaults.SetDefaults(Conf)
|
||||
|
||||
if util.NotExists(file) {
|
||||
// 默认配置文件不存在则创建
|
||||
if file == "config.toml" {
|
||||
releaseConfig(file, Conf)
|
||||
log.Logger.Infof("已创建默认配置文件: %s,请修改配置文件内容后重新启动", file)
|
||||
os.Exit(0)
|
||||
} else {
|
||||
log.Logger.Fatalf("配置文件不存在: %s", file)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析配置文件
|
||||
_, err := toml.DecodeFile(file, &Conf)
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("配置文件解析出错: %s", err.Error())
|
||||
}
|
||||
|
||||
// 配置预检查
|
||||
|
||||
jsonConfig, err := json.Marshal(Conf)
|
||||
if err == nil {
|
||||
log.Logger.Debugf("配置文件已加载: %s", string(jsonConfig))
|
||||
}
|
||||
}
|
||||
@@ -1 +1,76 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"mc-client-updater-server/pkg/conf"
|
||||
"mc-client-updater-server/pkg/dao/entity"
|
||||
"mc-client-updater-server/pkg/log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogrusWriter struct {
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
func (w *LogrusWriter) Printf(format string, v ...interface{}) {
|
||||
logStr := fmt.Sprintf(format, v...)
|
||||
w.log.WithField("method", "GORM").Warn(logStr)
|
||||
}
|
||||
|
||||
func NewLogger() *LogrusWriter {
|
||||
return &LogrusWriter{log: log.Logger}
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
db *gorm.DB
|
||||
)
|
||||
|
||||
func DB() *gorm.DB {
|
||||
once.Do(NewDBConn)
|
||||
return db
|
||||
}
|
||||
|
||||
func NewDBConn() {
|
||||
var err error
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||
conf.Conf.Database.Host,
|
||||
conf.Conf.Database.User,
|
||||
conf.Conf.Database.Password,
|
||||
conf.Conf.Database.DB,
|
||||
conf.Conf.Database.Port,
|
||||
)
|
||||
|
||||
// >= 1s SQL慢查询
|
||||
slowLogger := logger.New(NewLogger(), logger.Config{
|
||||
SlowThreshold: time.Second * 1,
|
||||
LogLevel: logger.Warn,
|
||||
})
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: slowLogger,
|
||||
})
|
||||
if err != nil {
|
||||
log.Logger.Fatal("创建数据库连接失败:", err)
|
||||
}
|
||||
|
||||
migrate()
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
var err error
|
||||
err = db.AutoMigrate(&entity.User{})
|
||||
err = db.AutoMigrate(&entity.Instance{})
|
||||
err = db.AutoMigrate(&entity.Update{})
|
||||
err = db.AutoMigrate(&entity.Grant{})
|
||||
err = db.AutoMigrate(&entity.Token{})
|
||||
|
||||
if err != nil {
|
||||
log.Logger.Fatal("关联数据表失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
13
pkg/dao/entity/grant.go
Normal file
13
pkg/dao/entity/grant.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Grant Grant是授权给实例的,给予实例访问权限
|
||||
type Grant struct {
|
||||
gorm.Model `json:"model"`
|
||||
Token string `gorm:"unique;not null" json:"token,omitempty"`
|
||||
TTL int `gorm:"not null;default:0" json:"ttl,omitempty"`
|
||||
GrantTo uint `gorm:"not null;default:0;comment:instances(id) 授权给实例,0表示无指定(所有)" json:"grant_to,omitempty"`
|
||||
}
|
||||
11
pkg/dao/entity/instance.go
Normal file
11
pkg/dao/entity/instance.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
gorm.Model `json:"model"`
|
||||
Name string `gorm:"unique;not null" json:"name,omitempty"`
|
||||
UpdateURL string `gorm:"column:update_url;not null;default:'';comment:更新URL,未指定使用默认" json:"update_url,omitempty"`
|
||||
}
|
||||
11
pkg/dao/entity/token.go
Normal file
11
pkg/dao/entity/token.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package entity
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// Token Token是授权给用户的,给予用户登录权限
|
||||
type Token struct {
|
||||
gorm.Model `json:"model"`
|
||||
Token string `gorm:"unique;not null" json:"token,omitempty"`
|
||||
GrantTo string `gorm:"index;not null;default:''" json:"grant_to,omitempty"`
|
||||
TTL int `gorm:"not null;default:0" json:"ttl,omitempty"`
|
||||
}
|
||||
10
pkg/dao/entity/update.go
Normal file
10
pkg/dao/entity/update.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package entity
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Update struct {
|
||||
gorm.Model `json:"model"`
|
||||
HashID string `gorm:"index;not null" json:"hash_id,omitempty"`
|
||||
Comment string `gorm:"not null;default:'';comment:更新内容或注释" json:"comment,omitempty"`
|
||||
Changes string `gorm:"not null;comment:更改的文件列表,逗号分隔,引用files(hash_id)" json:"changes,omitempty"`
|
||||
}
|
||||
@@ -3,9 +3,8 @@ package entity
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `gorm:"unique;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
Instance string `gorm:"not null;default:''"`
|
||||
Role string `gorm:"not null;default:0"`
|
||||
gorm.Model `json:"model"`
|
||||
Username string `gorm:"unique;not null" json:"username,omitempty"`
|
||||
Password string `gorm:"not null" json:"password,omitempty"`
|
||||
Roles string `gorm:"not null;default:''" json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
18
pkg/dto/user.go
Normal file
18
pkg/dto/user.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"mc-client-updater-server/pkg/dao/entity"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func UserEntity2DTO(user *entity.User) *User {
|
||||
return &User{
|
||||
Username: user.Username,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
}
|
||||
}
|
||||
28
pkg/log/logger.go
Normal file
28
pkg/log/logger.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
nested "github.com/antonfisher/nested-logrus-formatter"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Logger *logrus.Logger
|
||||
|
||||
func InitLogger(debug bool) {
|
||||
log := logrus.New()
|
||||
log.SetFormatter(&nested.Formatter{
|
||||
FieldsOrder: []string{"method", "url", "statusCode", "spendTime"},
|
||||
HideKeys: true,
|
||||
NoFieldsColors: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05.000",
|
||||
})
|
||||
|
||||
var lvl logrus.Level
|
||||
switch debug {
|
||||
case true:
|
||||
lvl = logrus.DebugLevel
|
||||
case false:
|
||||
lvl = logrus.InfoLevel
|
||||
}
|
||||
log.SetLevel(lvl)
|
||||
Logger = log
|
||||
}
|
||||
10
pkg/param/authorize.go
Normal file
10
pkg/param/authorize.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package param
|
||||
|
||||
type AuthorizeQueryParam struct {
|
||||
ClientId uint `form:"client_id" binding:"required"`
|
||||
ResponseType string `form:"response_type" binding:"required"`
|
||||
State string `form:"state" binding:"required"`
|
||||
Scope string `form:"scope" binding:"required"`
|
||||
CodeChallenge string `form:"code_challenge" binding:"required"`
|
||||
CodeChallengeMethod string `form:"code_challenge_method" binding:"required"`
|
||||
}
|
||||
6
pkg/param/login.go
Normal file
6
pkg/param/login.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package param
|
||||
|
||||
type LoginParam struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
16
pkg/password/argon2id.go
Normal file
16
pkg/password/argon2id.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package password
|
||||
|
||||
import "github.com/alexedwards/argon2id"
|
||||
|
||||
type Argon2id struct {
|
||||
}
|
||||
|
||||
func (a *Argon2id) Create(password string) (hash string, err error) {
|
||||
hash, err = argon2id.CreateHash(password, argon2id.DefaultParams)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Argon2id) Verify(password, hash string) (match bool, err error) {
|
||||
match, err = argon2id.ComparePasswordAndHash(password, hash)
|
||||
return
|
||||
}
|
||||
29
pkg/password/password.go
Normal file
29
pkg/password/password.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"mc-client-updater-server/pkg/conf"
|
||||
"mc-client-updater-server/pkg/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
encoder Encoder
|
||||
)
|
||||
|
||||
func Password() Encoder {
|
||||
once.Do(func() {
|
||||
switch conf.Conf.Security.PasswordEncoder {
|
||||
case "argon2id":
|
||||
encoder = &Argon2id{}
|
||||
default:
|
||||
log.Logger.Fatal("非法的PasswordEncoder类型:", conf.Conf.Security.PasswordEncoder)
|
||||
}
|
||||
})
|
||||
return encoder
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
Create(string) (string, error)
|
||||
Verify(password, hash string) (bool, error)
|
||||
}
|
||||
74
pkg/result/base.go
Normal file
74
pkg/result/base.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"mc-client-updater-server/pkg/common"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
ctx *gin.Context
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func NewResult(c *gin.Context) *Result {
|
||||
return &Result{ctx: c}
|
||||
}
|
||||
|
||||
func (r *Result) Success(data interface{}) {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
res := Root{
|
||||
Code: http.StatusOK,
|
||||
Msg: "",
|
||||
Data: data,
|
||||
}
|
||||
r.ctx.JSON(http.StatusOK, res)
|
||||
r.ctx.Next()
|
||||
}
|
||||
|
||||
func (r *Result) Fail(code int, msg string) {
|
||||
res := Root{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Data: gin.H{},
|
||||
}
|
||||
r.ctx.JSON(http.StatusOK, res)
|
||||
r.ctx.Abort()
|
||||
}
|
||||
|
||||
//func (r *Result) Redirect302(location string) {
|
||||
// r.ctx.Header("Location", location)
|
||||
// r.ctx.Status(http.StatusFound)
|
||||
// r.ctx.Next()
|
||||
//}
|
||||
|
||||
func (r *Result) InternalServerError(msg string) {
|
||||
r.Fail(http.StatusInternalServerError, msg)
|
||||
}
|
||||
|
||||
func (r *Result) BadRequest() {
|
||||
r.Fail(http.StatusBadRequest, "请求参数错误")
|
||||
}
|
||||
|
||||
func (r *Result) LoginError() {
|
||||
r.Fail(common.LoginErrorCode, "账号或密码错误")
|
||||
}
|
||||
|
||||
func (r *Result) Unauthorized() {
|
||||
r.Fail(http.StatusUnauthorized, "未登录")
|
||||
}
|
||||
|
||||
func (r *Result) NoPermission() {
|
||||
r.Fail(common.NoPermission, "权限不足")
|
||||
}
|
||||
|
||||
func (r *Result) LoginExpired() {
|
||||
r.Fail(common.LoginExpired, "登录过期")
|
||||
}
|
||||
25
pkg/token/jwt.go
Normal file
25
pkg/token/jwt.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"mc-client-updater-server/pkg/conf"
|
||||
"mc-client-updater-server/pkg/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewToken(aud string) string {
|
||||
auds := make([]string, 1)
|
||||
auds = append(auds, aud)
|
||||
now := time.Now()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"aud": auds,
|
||||
"iat": now,
|
||||
"nbf": now,
|
||||
})
|
||||
tokenStr, err := token.SignedString([]byte(conf.Conf.Security.JWTSecret))
|
||||
if err != nil {
|
||||
log.Logger.Error("生成Token失败:", err)
|
||||
return ""
|
||||
}
|
||||
return tokenStr
|
||||
}
|
||||
30
pkg/util/file.go
Normal file
30
pkg/util/file.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func NotExists(name string) bool {
|
||||
return !Exists(name)
|
||||
}
|
||||
|
||||
func CreateNestedFile(path string) (*os.File, error) {
|
||||
basePath := filepath.Dir(path)
|
||||
if !Exists(basePath) {
|
||||
err := os.MkdirAll(basePath, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return os.Create(path)
|
||||
}
|
||||
36
pkg/util/rand.go
Normal file
36
pkg/util/rand.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
const (
|
||||
// 6 bits to represent a letter index
|
||||
letterIdBits = 6
|
||||
// All 1-bits as many as letterIdBits
|
||||
letterIdMask = 1<<letterIdBits - 1
|
||||
letterIdMax = 63 / letterIdBits
|
||||
)
|
||||
|
||||
func RandStr(n int) string {
|
||||
b := make([]byte, n)
|
||||
// A rand.Int63() generates 63 random bits, enough for letterIdMax letters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdMax
|
||||
}
|
||||
if idx := int(cache & letterIdMask); idx < len(letters) {
|
||||
b[i] = letters[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdBits
|
||||
remain--
|
||||
}
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
21
pkg/util/slice.go
Normal file
21
pkg/util/slice.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package util
|
||||
|
||||
func InStringSlice(sl []string, ele string) bool {
|
||||
slMap := convertStrSlice2Map(sl)
|
||||
return inMap(slMap, ele)
|
||||
}
|
||||
|
||||
// ConvertStrSlice2Map 将字符串 slice 转为 map[string]struct{}。
|
||||
func convertStrSlice2Map(sl []string) map[string]struct{} {
|
||||
set := make(map[string]struct{}, len(sl))
|
||||
for _, v := range sl {
|
||||
set[v] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// InMap 判断字符串是否在 map 中。
|
||||
func inMap(m map[string]struct{}, s string) bool {
|
||||
_, ok := m[s]
|
||||
return ok
|
||||
}
|
||||
5
pkg/util/token.go
Normal file
5
pkg/util/token.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package util
|
||||
|
||||
func GenSessionID() string {
|
||||
return RandStr(32)
|
||||
}
|
||||
Reference in New Issue
Block a user