[2024-07-12](INIT): session登录
This commit is contained in:
parent
6e7b9cc89c
commit
90b4e437fe
111
cmd/picgo.go
Normal file
111
cmd/picgo.go
Normal file
@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"picgo/configs"
|
||||
"picgo/corelib"
|
||||
"picgo/model"
|
||||
"picgo/router"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "root",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
fmt.Println("args:")
|
||||
fmt.Println(strings.Join(args, " "))
|
||||
if len(os.Args) < 1 {
|
||||
log.Panic("请指定配置文件路径")
|
||||
return
|
||||
}
|
||||
configFile := args[0]
|
||||
fmt.Println("配置文件路径: ", configFile)
|
||||
// 初始化配置文件
|
||||
configs.NewConfig(configFile)
|
||||
// 初始化日志
|
||||
corelib.NewLogger()
|
||||
// 初始化redis
|
||||
corelib.NewRedis()
|
||||
// 初始化mysql
|
||||
corelib.NewMysql()
|
||||
// 初始化session store
|
||||
corelib.NewSessionStore()
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
var picgoCmd = &cobra.Command{
|
||||
Use: "picgo",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// 创建路由
|
||||
r := router.InitRouter()
|
||||
// 启动HTTP服务器
|
||||
log.Println("Serving on http://localhost:8082")
|
||||
err := http.ListenAndServe(":8082", r)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var MigrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
// 自动迁移数据库
|
||||
if err = corelib.DbMysql.AutoMigrate(&model.SysUser{}); err != nil {
|
||||
panic("SysUser 迁移数据库失败")
|
||||
}
|
||||
if err = corelib.DbMysql.AutoMigrate(&model.Domain{}); err != nil {
|
||||
panic("Domain 迁移数据库失败")
|
||||
}
|
||||
if err = corelib.DbMysql.AutoMigrate(&model.Upload{}); err != nil {
|
||||
panic("Upload 迁移数据库失败")
|
||||
}
|
||||
// 初始化管理员用户
|
||||
err = initSysUser()
|
||||
if err != nil {
|
||||
fmt.Println("初始化管理员用户失败", err)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(picgoCmd)
|
||||
rootCmd.AddCommand(MigrateCmd)
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化超级管理员
|
||||
func initSysUser() error {
|
||||
var (
|
||||
hashPassword string
|
||||
err error
|
||||
)
|
||||
userName := "admin001"
|
||||
password := "Admin#123456"
|
||||
salt := corelib.GenerateSalt()
|
||||
if hashPassword, err = corelib.HashPassword(password, salt); err != nil {
|
||||
corelib.Logger.Errorln("initSystem 生成密码失败: ", err)
|
||||
return err
|
||||
}
|
||||
user := model.SysUser{Username: userName, Password: hashPassword, Salt: salt, IsSuper: 1}
|
||||
if err = corelib.DbMysql.Create(&user).Error; err != nil {
|
||||
corelib.Logger.Errorln("initSystem 初始化管理员用户失败: ", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
@ -7,6 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type Configs struct {
|
||||
Server `yaml:"server"`
|
||||
Redis `yaml:"redis"`
|
||||
Mysql `yaml:"mysql"`
|
||||
Captcha `json:"captcha"`
|
||||
}
|
||||
|
||||
// Settings 定义配置结构体
|
||||
|
34
corelib/encryption.go
Normal file
34
corelib/encryption.go
Normal file
@ -0,0 +1,34 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Md5Hash md5加密
|
||||
func Md5Hash(data string) string {
|
||||
md5Hash := md5.New()
|
||||
md5Hash.Write([]byte(data))
|
||||
return hex.EncodeToString(md5Hash.Sum(nil))
|
||||
}
|
||||
|
||||
// HashPassword 使用Bcrypt哈希密码和盐值
|
||||
func HashPassword(password, salt string) (string, error) {
|
||||
var (
|
||||
err error
|
||||
hashedPassword []byte
|
||||
)
|
||||
passwordWithSalt := append([]byte(password), []byte(salt)...)
|
||||
if hashedPassword, err = bcrypt.GenerateFromPassword(passwordWithSalt, bcrypt.DefaultCost); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
// ComparePasswords 验证密码是否匹配哈希值和盐值
|
||||
func ComparePasswords(hashedPassword, userInput, salt string) bool {
|
||||
passwordWithSalt := append([]byte(userInput), []byte(salt)...)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), passwordWithSalt)
|
||||
return err == nil
|
||||
}
|
53
corelib/log.go
Normal file
53
corelib/log.go
Normal file
@ -0,0 +1,53 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"picgo/configs"
|
||||
)
|
||||
|
||||
var Logger *zap.SugaredLogger
|
||||
|
||||
func NewLogger() {
|
||||
writeSyncer := getLogWriter()
|
||||
encoder := getEncoder()
|
||||
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
|
||||
logger := zap.New(core, zap.AddCaller())
|
||||
Logger = logger.Sugar()
|
||||
}
|
||||
|
||||
func getEncoder() zapcore.Encoder {
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
func getLogWriter() zapcore.WriteSyncer {
|
||||
var (
|
||||
err error
|
||||
currentDir string
|
||||
)
|
||||
// 获取当前工作目录
|
||||
if currentDir, err = os.Getwd(); err != nil {
|
||||
log.Panic("获取当前工作目录失败: ", err)
|
||||
}
|
||||
logPath := path.Join(currentDir, configs.Settings.Server.LogPath)
|
||||
// Filename: 日志文件的位置
|
||||
// MaxSize:在进行切割之前,日志文件的最大大小(以 MB 为单位)
|
||||
// MaxBackups:保留旧文件的最大个数
|
||||
// MaxAges:保留旧文件的最大天数
|
||||
// Compress:是否压缩 / 归档旧文件
|
||||
lumberJackLogger := &lumberjack.Logger{
|
||||
Filename: logPath,
|
||||
MaxSize: 50,
|
||||
MaxBackups: 10,
|
||||
MaxAge: 1,
|
||||
Compress: false,
|
||||
}
|
||||
return zapcore.AddSync(lumberJackLogger)
|
||||
}
|
49
corelib/msyql.go
Normal file
49
corelib/msyql.go
Normal file
@ -0,0 +1,49 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
"picgo/configs"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
DbMysql *gorm.DB
|
||||
)
|
||||
|
||||
// NewMysql 创建数据库连接
|
||||
func NewMysql() {
|
||||
var (
|
||||
err error
|
||||
db *gorm.DB
|
||||
sqlDB *sql.DB
|
||||
dsn string
|
||||
)
|
||||
dsn = configs.Settings.Mysql.MysqlDNS
|
||||
|
||||
if db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
// 配置日志级别,打印出所有的sql
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
//TablePrefix: configs.Setting.TablePrefix, // 表前缀
|
||||
SingularTable: true, // 设置全局表名禁用复数
|
||||
},
|
||||
}); err != nil {
|
||||
Logger.Panicln("数据库连接失败: ", err)
|
||||
}
|
||||
|
||||
if sqlDB, err = db.DB(); err != nil {
|
||||
Logger.Panicln("数据库连接失败: ", err)
|
||||
}
|
||||
|
||||
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
|
||||
sqlDB.SetMaxIdleConns(configs.Settings.Mysql.MaxIdleConns)
|
||||
// SetMaxOpenConns 设置打开数据库连接的最大数量。
|
||||
sqlDB.SetMaxOpenConns(configs.Settings.Mysql.MaxOpenConns)
|
||||
// SetConnMaxLifetime 设置了连接可复用的最大时间。
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
DbMysql = db
|
||||
}
|
48
corelib/rand.go
Normal file
48
corelib/rand.go
Normal file
@ -0,0 +1,48 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func randomString(n int, allowedChars []rune) string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = allowedChars[r.Intn(len(allowedChars))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 生成随机字符串
|
||||
func generateRandom(randType int, length int) string {
|
||||
var res string
|
||||
const special = "~!@#$%^&*()_+=-"
|
||||
const number = "0123456789"
|
||||
const capitalLetter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const lowerLetter = "abcdefghijklmnopqrstuvwxyz"
|
||||
switch {
|
||||
case randType == 1: // 特俗符号
|
||||
res = special
|
||||
case randType == 2: // 数字
|
||||
res = number
|
||||
case randType == 3: // 大写字母
|
||||
res = capitalLetter
|
||||
case randType == 4: // 小写字母
|
||||
res = lowerLetter
|
||||
case randType == 5: // 大写字母+小写字母
|
||||
res = capitalLetter + lowerLetter
|
||||
case randType == 6: // 大写字母+小写字母+数字
|
||||
res = capitalLetter + lowerLetter + number
|
||||
default: // 大写字母+小写字母+数字+特殊符号
|
||||
res = capitalLetter + lowerLetter + number + special
|
||||
|
||||
}
|
||||
return randomString(length, []rune(res))
|
||||
}
|
||||
|
||||
// GenerateSalt 生成一个随机盐
|
||||
func GenerateSalt() string {
|
||||
const saltSize = 16 // 16字节的随机盐
|
||||
return generateRandom(0, saltSize)
|
||||
}
|
29
corelib/redis.go
Normal file
29
corelib/redis.go
Normal file
@ -0,0 +1,29 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"picgo/configs"
|
||||
)
|
||||
|
||||
var (
|
||||
RdbClient *redis.Client
|
||||
AdminCaptchaKey = "admin:captcha:" // 验证码存储key
|
||||
)
|
||||
|
||||
func NewRedis() {
|
||||
RdbClient = redis.NewClient(&redis.Options{
|
||||
Addr: configs.Settings.Redis.Addr, // Redis服务器地址
|
||||
Password: configs.Settings.Redis.Password, // 密码,如果没有密码可以为空
|
||||
DB: configs.Settings.Redis.DB, // 使用默认的数据库
|
||||
PoolSize: configs.Settings.Redis.PoolSize, // 连接池大小
|
||||
MinIdleConns: configs.Settings.Redis.MinidleConns, // 最小空闲连接数
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
_, err := RdbClient.Ping(context.Background()).Result()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to connect to Redis: %v", err))
|
||||
}
|
||||
}
|
38
corelib/regexp.go
Normal file
38
corelib/regexp.go
Normal file
@ -0,0 +1,38 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
// 执行正则
|
||||
func regexpRun(rx string, txt string) (ok bool) {
|
||||
var (
|
||||
err error
|
||||
compile *regexp2.Regexp
|
||||
matchString bool
|
||||
)
|
||||
if compile, err = regexp2.Compile(rx, 0); err != nil {
|
||||
Logger.Error("regexp compile error:", err)
|
||||
return false
|
||||
}
|
||||
if matchString, err = compile.MatchString(txt); err != nil {
|
||||
Logger.Error("regexp compile error:", err)
|
||||
return false
|
||||
}
|
||||
return matchString
|
||||
}
|
||||
|
||||
// IsCaptcha 验证码正则
|
||||
func IsCaptcha(code string) (ok bool) {
|
||||
return regexpRun("^[0-9a-zA-Z]{1,5}$", code)
|
||||
}
|
||||
|
||||
// CheckUsername 用户名正则
|
||||
func CheckUsername(username string) (ok bool) {
|
||||
return regexpRun("^[a-zA-Z0-9_-]{6,16}$", username)
|
||||
}
|
||||
|
||||
// CheckPassword 用户名正则
|
||||
func CheckPassword(password string) (ok bool) {
|
||||
return regexpRun("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!#%*?&])[A-Za-z\\d$@#$!%*?&]{8,16}", password)
|
||||
}
|
18
corelib/session.go
Normal file
18
corelib/session.go
Normal file
@ -0,0 +1,18 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"gopkg.in/boj/redistore.v1"
|
||||
"picgo/configs"
|
||||
)
|
||||
|
||||
var SessionStore *redistore.RediStore
|
||||
|
||||
func NewSessionStore() {
|
||||
// Fetch new store.
|
||||
store, err := redistore.NewRediStore(10, "tcp", configs.Settings.Redis.Addr, "", []byte(configs.Settings.Redis.Password))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
SessionStore = store
|
||||
defer store.Close()
|
||||
}
|
21
corelib/template.go
Normal file
21
corelib/template.go
Normal file
@ -0,0 +1,21 @@
|
||||
package corelib
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func TemplateHandler(w http.ResponseWriter, r *http.Request, data any, filenames ...string) {
|
||||
// 解析模板文件
|
||||
tmpl, err := template.ParseFiles(filenames...)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 渲染模板并写入响应
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
33
go.mod
33
go.mod
@ -2,4 +2,35 @@ module picgo
|
||||
|
||||
go 1.22
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
require (
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
github.com/dlclark/regexp2 v1.11.2
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195
|
||||
github.com/redis/go-redis/v9 v9.5.4
|
||||
github.com/spf13/cobra v1.8.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/garyburd/redigo v1.6.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b // indirect
|
||||
)
|
||||
|
70
go.sum
70
go.sum
@ -1,3 +1,73 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68=
|
||||
github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
|
||||
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
|
||||
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195 h1:Vdz2cBh5Fw2MYHWi3ED2PraDQaWEUhNCr1XFHrP4N5A=
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195/go.mod h1:1Vk0LDW6jG5cGc2D9RQUxHaE0vYhTvIwSo9mOL6K4/U=
|
||||
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8=
|
||||
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.5.4 h1:vOFYDKKVgrI5u++QvnMT7DksSMYg7Aw/Np4vLJLKLwY=
|
||||
github.com/redis/go-redis/v9 v9.5.4/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b h1:U/Uqd1232+wrnHOvWNaxrNqn/kFnr4yu4blgPtQt0N8=
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b/go.mod h1:fgfIZMlsafAHpspcks2Bul+MWUNw/2dyQmjC2faKjtg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
|
85
handler/captcha.go
Normal file
85
handler/captcha.go
Normal file
@ -0,0 +1,85 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"net"
|
||||
"net/http"
|
||||
"picgo/corelib"
|
||||
"picgo/corelib/captcha"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
res []byte
|
||||
err error
|
||||
)
|
||||
captchaId := getCaptchaId(r)
|
||||
if res, err = generateCaptcha(captchaId); err != nil {
|
||||
corelib.WriteJsonResponse(w, 1030, "生产验证码失败", nil)
|
||||
return
|
||||
}
|
||||
// 设置响应头
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
// 将图片文件内容写入响应
|
||||
_, err = w.Write(res)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to serve image.", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// user agent + client ip md5 生成验证码key
|
||||
func getCaptchaId(r *http.Request) string {
|
||||
clientIP := GetRealIP(r)
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
return corelib.Md5Hash(clientIP + userAgent)
|
||||
}
|
||||
|
||||
// 生成验证码图片
|
||||
func generateCaptcha(captchaId string) ([]byte, error) {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
img *image.RGBA
|
||||
err error
|
||||
)
|
||||
if img, err = captcha.GenerateCaptcha(captchaId); err != nil {
|
||||
corelib.Logger.Fatalln("生成验证码错误", err)
|
||||
return nil, err
|
||||
//return nil, corelib.CAPTCHA_GENERATE_ERROR
|
||||
}
|
||||
if err = png.Encode(&b, img); err != nil {
|
||||
corelib.Logger.Fatalln("验证码encode错误", err)
|
||||
return nil, err
|
||||
//return nil, corelib.CAPTCHA_GENERATE_ERROR
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetRealIP 获取用户的真实IP地址
|
||||
func GetRealIP(r *http.Request) string {
|
||||
// 检查 X-Forwarded-For 头
|
||||
xff := r.Header.Get("X-Forwarded-For")
|
||||
if xff != "" {
|
||||
// X-Forwarded-For 头可以包含多个IP地址,取第一个
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 X-Real-IP 头
|
||||
xri := r.Header.Get("X-Real-IP")
|
||||
if xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// 如果没有这些头,则使用远程地址
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
@ -1 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"picgo/corelib"
|
||||
)
|
||||
|
||||
func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
data := struct {
|
||||
Title string
|
||||
Active string
|
||||
}{
|
||||
Title: "Admin Dashboard",
|
||||
Active: r.URL.Path,
|
||||
}
|
||||
corelib.TemplateHandler(w, r, data, "view/layout.html", "view/index.html")
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,66 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"picgo/configs"
|
||||
"picgo/corelib"
|
||||
"picgo/corelib/captcha"
|
||||
"picgo/model"
|
||||
)
|
||||
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
|
||||
corelib.TemplateHandler(w, r, nil, "view/login.html")
|
||||
case http.MethodPost:
|
||||
loginService(w, r)
|
||||
default:
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func loginService(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
res model.LoginRequest
|
||||
user model.SysUser
|
||||
)
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&res)
|
||||
if err != nil {
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
cid := getCaptchaId(r)
|
||||
if ok := captcha.Verify(cid, res.Captcha); !ok {
|
||||
corelib.WriteJsonResponse(w, 1040, "验证码错误", nil)
|
||||
return
|
||||
}
|
||||
if user, err = sysUserSelectDataByUsername(res.Username); err != nil {
|
||||
corelib.WriteJsonResponse(w, 1041, "用户不存在", nil)
|
||||
return
|
||||
}
|
||||
// 验证用户名密码
|
||||
if !corelib.ComparePasswords(user.Password, res.Password, user.Salt) {
|
||||
corelib.WriteJsonResponse(w, 1042, "用户名或密码错误", nil)
|
||||
return
|
||||
}
|
||||
session, _ := corelib.SessionStore.Get(r, configs.Settings.Server.SessionName)
|
||||
session.Values["username"] = user.Username
|
||||
if err = session.Save(r, w); err != nil {
|
||||
corelib.WriteJsonResponse(w, 1043, "回话保存失败", nil)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
corelib.WriteJsonResponse(w, 200, "登录成功", nil)
|
||||
}
|
||||
|
||||
// sysUserSelectDataByUsername 通过用户名查询用户
|
||||
func sysUserSelectDataByUsername(userName string) (model.SysUser, error) {
|
||||
var user model.SysUser
|
||||
if err := corelib.DbMysql.Model(model.SysUser{Username: userName}).First(&user).Error; err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
19
handler/profile.go
Normal file
19
handler/profile.go
Normal file
@ -0,0 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"picgo/corelib"
|
||||
)
|
||||
|
||||
func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
data := struct {
|
||||
Title string
|
||||
Active string
|
||||
}{
|
||||
Title: "Admin Dashboard",
|
||||
Active: r.URL.Path,
|
||||
}
|
||||
corelib.TemplateHandler(w, r, data, "view/layout.html", "view/profile.html")
|
||||
}
|
||||
}
|
19
handler/settings.go
Normal file
19
handler/settings.go
Normal file
@ -0,0 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"picgo/corelib"
|
||||
)
|
||||
|
||||
func SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
data := struct {
|
||||
Title string
|
||||
Active string
|
||||
}{
|
||||
Title: "Admin Dashboard",
|
||||
Active: r.URL.Path,
|
||||
}
|
||||
corelib.TemplateHandler(w, r, data, "view/layout.html", "view/settings.html")
|
||||
}
|
||||
}
|
@ -6,13 +6,14 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"picgo/configs"
|
||||
"picgo/corelib"
|
||||
)
|
||||
|
||||
func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
// 解析表单数据,限制最大内存为32MB
|
||||
err := r.ParseMultipartForm(32 << 20)
|
||||
// 解析表单数据,限制最大内存为3MB 1024*1024*3
|
||||
err := r.ParseMultipartForm(configs.Settings.Server.UploadMaxSize << 20)
|
||||
if err != nil {
|
||||
corelib.WriteJsonResponse(w, 1020, "文件大于32MB", nil)
|
||||
return
|
||||
@ -28,7 +29,7 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer file.Close()
|
||||
|
||||
// 保存文件到本地
|
||||
f, err := os.OpenFile(filepath.Join("./static/pic", handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
f, err := os.OpenFile(filepath.Join(configs.Settings.Server.UploadPath, handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
fmt.Println("Error saving file")
|
||||
fmt.Println(err)
|
||||
@ -46,7 +47,8 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
//fmt.Fprintf(w, "File %s uploaded successfully!", handler.Filename)
|
||||
corelib.Success(w)
|
||||
corelib.WriteJsonResponse(w, 200, "文件上传成功", nil)
|
||||
//corelib.Success(w)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
15
main.go
15
main.go
@ -1,18 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"picgo/router"
|
||||
)
|
||||
import "picgo/cmd"
|
||||
|
||||
func main() {
|
||||
// 创建路由
|
||||
r := router.InitRouter()
|
||||
// 启动HTTP服务器
|
||||
log.Println("Serving on http://localhost:8082")
|
||||
err := http.ListenAndServe(":8082", r)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
cmd.Execute()
|
||||
}
|
||||
|
23
middleware/auth.go
Normal file
23
middleware/auth.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoginMiddleware 登录 // 添加日志中间件到路由器 使用:r.Handle("/", LoginMiddleware(http.HandlerFunc(handler)))
|
||||
func LoginMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 记录请求时间和路径
|
||||
start := time.Now()
|
||||
log.Printf("Started %s %s", r.Method, r.URL.Path)
|
||||
|
||||
// 调用下一个处理程序
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// 记录请求处理时间
|
||||
duration := time.Since(start)
|
||||
log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, duration)
|
||||
})
|
||||
}
|
26
model/base.go
Normal file
26
model/base.go
Normal file
@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/snowflake"
|
||||
"gorm.io/gorm"
|
||||
"picgo/configs"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
ID int64 `gorm:"primary_key;unique;not null;comment:'主键'" json:"id"`
|
||||
CreatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP;comment:'创建时间'"`
|
||||
UpdatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate 在创建用户之前为其生成分布式ID
|
||||
func (base *BaseModel) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
var node *snowflake.Node
|
||||
nodeId := configs.Settings.Server.NodeId
|
||||
if node, err = snowflake.NewNode(nodeId); err != nil {
|
||||
return err
|
||||
}
|
||||
base.ID = node.Generate().Int64()
|
||||
return nil
|
||||
}
|
13
model/domin.go
Normal file
13
model/domin.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
// Domain cdn访问域名
|
||||
type Domain struct {
|
||||
BaseModel
|
||||
Name string `gorm:"size:64;unique;not null;column:name;comment:'域名'" json:"name"`
|
||||
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
|
||||
}
|
||||
|
||||
// TableName sets the custom table name for the User model.
|
||||
func (Domain) TableName() string {
|
||||
return "domain"
|
||||
}
|
7
model/login_request.go
Normal file
7
model/login_request.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"`
|
||||
}
|
13
model/upload.go
Normal file
13
model/upload.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
// Upload cdn访问域名
|
||||
type Upload struct {
|
||||
BaseModel
|
||||
FileKey string `gorm:"size:64;unique;not null;column:filekey;comment:'文件路径'" json:"fileKey"`
|
||||
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
|
||||
}
|
||||
|
||||
// TableName sets the custom table name for the User model.
|
||||
func (Upload) TableName() string {
|
||||
return "upload"
|
||||
}
|
16
model/user.go
Normal file
16
model/user.go
Normal file
@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
// SysUser represents a user in the database.
|
||||
type SysUser struct {
|
||||
BaseModel
|
||||
Username string `gorm:"size:32;unique;not null;column:username;comment:'用户名'" json:"username"`
|
||||
Password string `gorm:"size:64;not null;column:password;comment:'密码'" json:"password"`
|
||||
Salt string `gorm:"size:16;not null;column:salt;comment:'盐'" json:"salt"`
|
||||
IsSuper int `gorm:"type:TINYINT(1);default:0;column:is_super;comment:'是否是超级管理员-0:否,1:是'" json:"is_super"`
|
||||
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
|
||||
}
|
||||
|
||||
// TableName sets the custom table name for the User model.
|
||||
func (SysUser) TableName() string {
|
||||
return "sys_user"
|
||||
}
|
@ -9,7 +9,12 @@ func InitRouter() *http.ServeMux {
|
||||
var mux *http.ServeMux
|
||||
// 创建新的路由器
|
||||
mux = http.NewServeMux()
|
||||
mux.HandleFunc("/", handler.IndexHandler)
|
||||
mux.HandleFunc("/settings", handler.SettingsHandler)
|
||||
mux.HandleFunc("/profile", handler.ProfileHandler)
|
||||
mux.HandleFunc("/static/", handler.StaticHandler)
|
||||
mux.HandleFunc("/login", handler.LoginHandler)
|
||||
mux.HandleFunc("/api/v1/upload", handler.UploadFileHandler)
|
||||
mux.HandleFunc("/captcha", handler.CaptchaHandler)
|
||||
return mux
|
||||
}
|
||||
|
19
static/css/custom.css
Normal file
19
static/css/custom.css
Normal file
@ -0,0 +1,19 @@
|
||||
.navbar-nav .nav-link {
|
||||
font-size: 1.1em;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-size: 1.1em;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #e9ecef;
|
||||
color: #007bff;
|
||||
}
|
37
static/js/login.js
Normal file
37
static/js/login.js
Normal file
@ -0,0 +1,37 @@
|
||||
function Login() {
|
||||
this.captchaUrl = "/captcha"
|
||||
this.userName = ""
|
||||
this.password = ""
|
||||
this.captcha = ""
|
||||
}
|
||||
|
||||
Login.prototype.RefreshCaptcha = function () {
|
||||
let self = this;
|
||||
$("#captcha-img").click(function () {
|
||||
let timestamp = new Date().getTime();
|
||||
self.captchaUrl = "/captcha" + "?t=" + timestamp
|
||||
$("#captcha-img").attr('src', self.captchaUrl); //显示图片
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Login.prototype.run = function () {
|
||||
this.RefreshCaptcha()
|
||||
}
|
||||
|
||||
// 构造执行入口
|
||||
$(function () {
|
||||
feather.replace()
|
||||
// 模板过滤方法
|
||||
// if (window.template) {
|
||||
// template.defaults.imports.domainSubstring = function (dateValue) {
|
||||
// if (dateValue.length > 40 ) {
|
||||
// return dateValue.substring(0, 37) + "..."
|
||||
// } else {
|
||||
// return dateValue
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
let login = new Login()
|
||||
login.run()
|
||||
})
|
221
view/index.html
221
view/index.html
@ -1,217 +1,4 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="icon" href="../static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="../static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="../static/css/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="../static/css/sweetalert.css">
|
||||
<title>域名监控</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid" style="padding-left: 0; padding-right: 0">
|
||||
<nav class="navbar justify-content-center navbar-dark bg-dark">
|
||||
<span class="navbar-brand mb-0 h1">域名监控</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="shadow p-3 bg-white rounded mt-3">
|
||||
<div class="alert alert-primary form-inline" role="alert">
|
||||
<button type="button" id="add-domain" class="btn btn-primary" data-toggle="modal"
|
||||
data-target="#DomainModal"><i
|
||||
class="bi bi-plus-lg"></i> 添加域名
|
||||
</button>
|
||||
<div class="input-group mr-3" style="position: absolute; right: 0">
|
||||
<input type="text" id="search-input" class="form-control" placeholder="域名" aria-label="域名" aria-describedby="searc-btn">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" id="search-btn" type="button"><i class="bi bi-search"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bg-light">
|
||||
<table class="table table-bordered text-nowrap">
|
||||
<thead>
|
||||
<tr class="table-primary">
|
||||
<th scope="col">编号</th>
|
||||
<th scope="col">域名</th>
|
||||
<th scope="col">Boce</th>
|
||||
<th scope="col">备注</th>
|
||||
<th scope="col">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody-domin">
|
||||
<script id="tpl-table-tr" type="text/html">
|
||||
{{each domainList value index }}
|
||||
<tr>
|
||||
<td>{{index}}</td>
|
||||
<td>
|
||||
<span class="d-inline-block" tabindex="0" data-toggle="tooltip" data-placement="top"
|
||||
title="{{value.name}}">
|
||||
{{value.name|domainSubstring}}
|
||||
</span>
|
||||
</td>
|
||||
{{if value.boce}}
|
||||
<td><span class="badge rounded-pill bg-success">True</span></td>
|
||||
{{else}}
|
||||
<td><span class="badge rounded-pill bg-secondary">False</span></td>
|
||||
{{/if}}
|
||||
<td>{{value.remark}}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="window.open('{{value.name}}')"><i
|
||||
class="bi bi-link-45deg"></i> 打开
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-warning edit-domain" data-index="{{index}}"
|
||||
data-toggle="modal"
|
||||
data-target="#DomainModal"><i
|
||||
class="bi bi-pencil-square"></i> 编辑
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger delete-domain"
|
||||
data-index="{{index}}"><i
|
||||
class="bi bi-trash3"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</script>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 分页 -->
|
||||
<nav>
|
||||
<ul class="pagination justify-content-end" id="page-li">
|
||||
<script id="tpl-page-li" type="text/html">
|
||||
<!-- 上一页 -->
|
||||
<li class="page-item{{if page_data.is_first}} disabled{{/if}}">
|
||||
<a class="page-link page-btn" data-p="{{page_data.current_page-1}}"
|
||||
href="javascript:void(0)">上一页</a>
|
||||
</li>
|
||||
<!-- 当前页左边是否显示 ... -->
|
||||
{{if page_data.left_has_more }}
|
||||
<li class="page-item"><a class="page-link page-btn" data-p="1" href="javascript:void(0)">1</a>
|
||||
</li>
|
||||
<li class="page-item"><span class="page-link">...</span></li>
|
||||
{{/if}}
|
||||
<!-- 当前页左边显示按钮 -->
|
||||
{{each page_data.left_pages lp}}
|
||||
<li class="page-item"><a class="page-link page-btn" data-p="{{lp}}" href="javascript:void(0)">{{lp}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
<!-- 当前页 -->
|
||||
<li class="page-item active"><span class="page-link">{{page_data.current_page}}</span></li>
|
||||
<!-- 当前页右边是否显示 ... -->
|
||||
{{each page_data.right_pages rp}}
|
||||
<li class="page-item"><a class="page-link page-btn" data-p="{{rp}}" href="javascript:void(0)">{{rp}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
<!-- 当前页右边显示按钮 -->
|
||||
{{if page_data.right_has_more }}
|
||||
<li class="page-item"><span class="page-link">...</span></li>
|
||||
<li class="page-item"><a class="page-link page-btn" data-p="{{page_data.num_pages}}"
|
||||
href="javascript:void(0)">{{page_data.num_pages}}</a></li>
|
||||
{{/if}}
|
||||
<!-- 下一页 -->
|
||||
<li class="page-item{{if page_data.is_finally}} disabled{{/if}}">
|
||||
<a class="page-link page-btn" data-p="{{page_data.current_page+1}}"
|
||||
href="javascript:void(0)">下一页</a>
|
||||
</li>
|
||||
</script>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="DomainModal" tabindex="-1" role="dialog" aria-labelledby="DomainModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="DomainModalLabel" modal-data="0">添加域名</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="col-auto">
|
||||
<div class="form-group row">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text alert-primary">域名:</div>
|
||||
</div>
|
||||
<input type="text" id="form-domain" class="form-control" placeholder="域名">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text alert-primary">备注:</div>
|
||||
</div>
|
||||
<input type="text" id="form-remark" class="form-control" placeholder="备注">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text alert-primary">秘钥:</div>
|
||||
</div>
|
||||
<input type="text" id="form-key" class="form-control" placeholder="cdn鉴权秘钥">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text alert-primary">CDN: </div>
|
||||
</div>
|
||||
<div class="btn-group" data-toggle="buttons-cdn">
|
||||
<label class="btn btn-light" style="margin-bottom: 0">
|
||||
<input type="radio" name="options-cdn" id="cdn-t" autocomplete="off">True
|
||||
</label>
|
||||
<label class="btn btn-light" style="margin-bottom: 0">
|
||||
<input type="radio" name="options-cdn" id="cdn-f" autocomplete="off" checked>False
|
||||
</label>
|
||||
<label style="margin-left:10px;color: red;height: 30px;line-height: 38px;">是否需要鉴权</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text alert-primary">Boce: </div>
|
||||
</div>
|
||||
<div class="btn-group" data-toggle="buttons-boce">
|
||||
<label class="btn btn-light" style="margin-bottom: 0">
|
||||
<input type="radio" name="options-boce" id="boce-t" autocomplete="off">True
|
||||
</label>
|
||||
<label class="btn btn-light" style="margin-bottom: 0">
|
||||
<input type="radio" name="options-boce" id="boce-f" autocomplete="off" checked>False
|
||||
</label>
|
||||
<label style="margin-left:10px;color: red;height: 30px;line-height: 38px;">是否加到boce、17ce测试</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" id="submit-data">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../static/js/jquery-3.3.1.min.js"></script>
|
||||
<script src="../static/js/popper.min.js"></script>
|
||||
<script src="../static/js/bootstrap.min.js"></script>
|
||||
<script src="../static/js/template-web.js"></script>
|
||||
<script src="../static/js/sweetalert.min.js"></script>
|
||||
<script src="../static/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{define "content"}}
|
||||
<h1 class="h2">Dashboard</h1>
|
||||
<p>Welcome to the Admin Panel.</p>
|
||||
{{end}}
|
||||
|
73
view/layout.html
Normal file
73
view/layout.html
Normal file
@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>{{.Title}}</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/css/custom.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="#">Admin Panel</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item {{if eq .Active "/"}}active{{end}}">
|
||||
<a class="nav-link" href="/">Dashboard <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item {{if eq .Active "/settings"}}active{{end}}">
|
||||
<a class="nav-link" href="/settings">Settings</a>
|
||||
</li>
|
||||
<li class="nav-item {{if eq .Active "/profile"}}active{{end}}">
|
||||
<a class="nav-link" href="/profile">Profile</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
|
||||
<div class="sidebar-sticky">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item {{if eq .Active "/"}}active{{end}}">
|
||||
<a class="nav-link" href="/">
|
||||
<span data-feather="home"></span>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item {{if eq .Active "/orders"}}active{{end}}">
|
||||
<a class="nav-link" href="/orders">
|
||||
<span data-feather="file"></span>
|
||||
Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item {{if eq .Active "/products"}}active{{end}}">
|
||||
<a class="nav-link" href="/products">
|
||||
<span data-feather="shopping-cart"></span>
|
||||
Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item {{if eq .Active "/customers"}}active{{end}}">
|
||||
<a class="nav-link" href="/customers">
|
||||
<span data-feather="users"></span>
|
||||
Customers
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4">
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/feather-icons.js"></script>
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -3,9 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Login Page</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
|
||||
<title>登录</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
@ -13,11 +12,13 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 15px;
|
||||
margin: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -25,30 +26,48 @@
|
||||
<div class="login-container">
|
||||
<div class="login-form card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-center">Login</h3>
|
||||
<h3 class="card-title text-center">后台登录</h3>
|
||||
<form action="/login" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="Enter email" required>
|
||||
{{ .csrfField }}
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="username-addon">
|
||||
<span data-feather="user"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
|
||||
<input type="text" class="form-control" name="username" placeholder="用户名" aria-label="用户名"
|
||||
aria-describedby="username-addon" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Login</button>
|
||||
<p class="text-center mt-3">
|
||||
<a href="#">Forgot your password?</a>
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<a href="#">Register</a>
|
||||
</p>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="password-addon">
|
||||
<span data-feather="pocket"></span>
|
||||
</span>
|
||||
</div>
|
||||
<input type="password" class="form-control" name="password" placeholder="密码" aria-label="密码"
|
||||
aria-describedby="password-addon" required>
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"> <span data-feather="image"></span></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="code" name="code" placeholder="验证码" required>
|
||||
<div class="input-group-append" style="padding-left: 4px">
|
||||
<img src="/captcha" style="width: 96px;border: 1px solid #ced4da;border-radius: 4px;" alt="验证码" id="captcha-img">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bootstrap JS and dependencies -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/jquery-3.3.1.min.js"></script>
|
||||
<script src="/static/js/popper.min.js"></script>
|
||||
<script src="/static/js/bootstrap.min.js"></script>
|
||||
<script src="/static/js/feather-icons.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
4
view/profile.html
Normal file
4
view/profile.html
Normal file
@ -0,0 +1,4 @@
|
||||
{{define "content"}}
|
||||
<h1 class="h2">Profile</h1>
|
||||
<p>Profile page content goes here.</p>
|
||||
{{end}}
|
4
view/settings.html
Normal file
4
view/settings.html
Normal file
@ -0,0 +1,4 @@
|
||||
{{define "content"}}
|
||||
<h1 class="h2">Settings</h1>
|
||||
<p>Settings page content goes here.</p>
|
||||
{{end}}
|
Loading…
Reference in New Issue
Block a user