[2024-07-12](INIT): session登录

This commit is contained in:
june 2024-07-12 20:32:33 +08:00
parent 6e7b9cc89c
commit 90b4e437fe
33 changed files with 1004 additions and 255 deletions

111
cmd/picgo.go Normal file
View 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
}

View File

@ -7,6 +7,10 @@ import (
) )
type Configs struct { type Configs struct {
Server `yaml:"server"`
Redis `yaml:"redis"`
Mysql `yaml:"mysql"`
Captcha `json:"captcha"`
} }
// Settings 定义配置结构体 // Settings 定义配置结构体

34
corelib/encryption.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -2,4 +2,35 @@ module picgo
go 1.22 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
View File

@ -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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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
View 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
}

View File

@ -1 +1,19 @@
package handler 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")
}
}

View File

@ -1 +1,66 @@
package handler 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
View 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
View 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")
}
}

View File

@ -6,13 +6,14 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"picgo/configs"
"picgo/corelib" "picgo/corelib"
) )
func UploadFileHandler(w http.ResponseWriter, r *http.Request) { func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" { if r.Method == "POST" {
// 解析表单数据限制最大内存为32MB // 解析表单数据限制最大内存为3MB 1024*1024*3
err := r.ParseMultipartForm(32 << 20) err := r.ParseMultipartForm(configs.Settings.Server.UploadMaxSize << 20)
if err != nil { if err != nil {
corelib.WriteJsonResponse(w, 1020, "文件大于32MB", nil) corelib.WriteJsonResponse(w, 1020, "文件大于32MB", nil)
return return
@ -28,7 +29,7 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
defer file.Close() 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 { if err != nil {
fmt.Println("Error saving file") fmt.Println("Error saving file")
fmt.Println(err) fmt.Println(err)
@ -46,7 +47,8 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
//fmt.Fprintf(w, "File %s uploaded successfully!", handler.Filename) //fmt.Fprintf(w, "File %s uploaded successfully!", handler.Filename)
corelib.Success(w) corelib.WriteJsonResponse(w, 200, "文件上传成功", nil)
//corelib.Success(w)
} else { } else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
} }

15
main.go
View File

@ -1,18 +1,7 @@
package main package main
import ( import "picgo/cmd"
"log"
"net/http"
"picgo/router"
)
func main() { func main() {
// 创建路由 cmd.Execute()
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)
}
} }

23
middleware/auth.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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"
}

View File

@ -9,7 +9,12 @@ func InitRouter() *http.ServeMux {
var mux *http.ServeMux var mux *http.ServeMux
// 创建新的路由器 // 创建新的路由器
mux = http.NewServeMux() mux = http.NewServeMux()
mux.HandleFunc("/", handler.IndexHandler)
mux.HandleFunc("/settings", handler.SettingsHandler)
mux.HandleFunc("/profile", handler.ProfileHandler)
mux.HandleFunc("/static/", handler.StaticHandler) mux.HandleFunc("/static/", handler.StaticHandler)
mux.HandleFunc("/login", handler.LoginHandler)
mux.HandleFunc("/api/v1/upload", handler.UploadFileHandler) mux.HandleFunc("/api/v1/upload", handler.UploadFileHandler)
mux.HandleFunc("/captcha", handler.CaptchaHandler)
return mux return mux
} }

19
static/css/custom.css Normal file
View 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
View 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()
})

View File

@ -1,217 +1,4 @@
<!doctype html> {{define "content"}}
<html lang="zh-CN"> <h1 class="h2">Dashboard</h1>
<p>Welcome to the Admin Panel.</p>
<head> {{end}}
<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">&times;</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>

73
view/layout.html Normal file
View 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>

View File

@ -3,9 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Login Page</title> <title>登录</title>
<!-- Bootstrap CSS --> <link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
.login-container { .login-container {
min-height: 100vh; min-height: 100vh;
@ -13,11 +12,13 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.login-form { .login-form {
width: 100%; width: 100%;
max-width: 420px; max-width: 420px;
padding: 15px; padding: 15px;
margin: auto; margin: auto;
border-radius: 12px;
} }
</style> </style>
</head> </head>
@ -25,30 +26,48 @@
<div class="login-container"> <div class="login-container">
<div class="login-form card shadow-sm"> <div class="login-form card shadow-sm">
<div class="card-body"> <div class="card-body">
<h3 class="card-title text-center">Login</h3> <h3 class="card-title text-center">后台登录</h3>
<form action="/login" method="POST"> <form action="/login" method="POST">
<div class="form-group"> {{ .csrfField }}
<label for="email">Email address</label> <div class="input-group mb-3">
<input type="email" class="form-control" id="email" name="email" placeholder="Enter email" required> <div class="input-group-prepend">
<span class="input-group-text" id="username-addon">
<span data-feather="user"></span>
</span>
</div> </div>
<div class="form-group"> <input type="text" class="form-control" name="username" placeholder="用户名" aria-label="用户名"
<label for="password">Password</label> aria-describedby="username-addon" required>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
</div> </div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
<p class="text-center mt-3"> <div class="input-group mb-3">
<a href="#">Forgot your password?</a> <div class="input-group-prepend">
</p> <span class="input-group-text" id="password-addon">
<p class="text-center"> <span data-feather="pocket"></span>
<a href="#">Register</a> </span>
</p> </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> </form>
</div> </div>
</div> </div>
</div> </div>
<!-- Bootstrap JS and dependencies --> <!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="/static/js/jquery-3.3.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js"></script> <script src="/static/js/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.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> </body>
</html> </html>

4
view/profile.html Normal file
View 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
View File

@ -0,0 +1,4 @@
{{define "content"}}
<h1 class="h2">Settings</h1>
<p>Settings page content goes here.</p>
{{end}}