first commit

This commit is contained in:
kenneth
2023-11-28 09:50:42 +00:00
parent d940668996
commit 5a01af92af
33 changed files with 1939 additions and 0 deletions

31
internal/db/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

17
internal/db/models.go Normal file
View File

@@ -0,0 +1,17 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"time"
)
type User struct {
ID string `json:"id"`
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}

20
internal/db/querier.go Normal file
View File

@@ -0,0 +1,20 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
package db
import (
"context"
)
type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteUser(ctx context.Context, id string) error
GetUser(ctx context.Context, id string) (User, error)
GetUserByName(ctx context.Context, username string) (User, error)
ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
}
var _ Querier = (*Queries)(nil)

View File

@@ -0,0 +1,32 @@
-- name: CreateUser :one
INSERT INTO users (
username, hashed_password, email
) VALUES (
$1, $2, $3
)
RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: UpdateUser :one
UPDATE users
SET hashed_password = $2,
email = $3
WHERE id = $1
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
-- name: GetUserByName :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY id
LIMIT $1
OFFSET $2;

View File

@@ -0,0 +1 @@
DROP TABLE "users";

View File

@@ -0,0 +1,7 @@
CREATE TABLE "users" (
"id" varchar NOT NULL PRIMARY KEY,
"username" varchar NOT NULL,
"hashed_password" varchar NOT NULL,
"email" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);

61
internal/db/store.go Normal file
View File

@@ -0,0 +1,61 @@
package db
import (
"context"
"database/sql"
"fmt"
"github.com/lib/pq"
)
type Store interface {
Querier
ExecTx(ctx context.Context, fn func(*Queries) error) error
IsUniqueViolation(err error) bool
IsForeignKeyViolation(err error) bool
IsNoRows(err error) bool
}
type SQLStore struct {
db *sql.DB
*Queries
}
func NewStore(db *sql.DB) Store {
return &SQLStore{
db: db,
Queries: New(db),
}
}
func (store *SQLStore) ExecTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return err
}
q := New(tx)
err = fn(q)
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit()
}
func (store *SQLStore) IsUniqueViolation(err error) bool {
pqErr, ok := err.(*pq.Error)
return ok && pqErr.Code == "23505"
}
func (store *SQLStore) IsForeignKeyViolation(err error) bool {
pqErr, ok := err.(*pq.Error)
return ok && pqErr.Code == "23503"
}
func (store *SQLStore) IsNoRows(err error) bool {
return err == sql.ErrNoRows
}

152
internal/db/user.sql.go Normal file
View File

@@ -0,0 +1,152 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
// source: user.sql
package db
import (
"context"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (
username, hashed_password, email
) VALUES (
$1, $2, $3
)
RETURNING id, username, hashed_password, email, created_at
`
type CreateUserParams struct {
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.HashedPassword, arg.Email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return i, err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, deleteUser, id)
return err
}
const getUser = `-- name: GetUser :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
row := q.db.QueryRowContext(ctx, getUser, id)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE username = $1 LIMIT 1
`
func (q *Queries) GetUserByName(ctx context.Context, username string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByName, username)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return i, err
}
const listUsers = `-- name: ListUsers :many
SELECT id, username, hashed_password, email, created_at FROM users
ORDER BY id
LIMIT $1
OFFSET $2
`
type ListUsersParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) {
rows, err := q.db.QueryContext(ctx, listUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []User{}
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET hashed_password = $2,
email = $3
WHERE id = $1
RETURNING id, username, hashed_password, email, created_at
`
type UpdateUserParams struct {
ID string `json:"id"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUser, arg.ID, arg.HashedPassword, arg.Email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return i, err
}

20
internal/handlers/home.go Normal file
View File

@@ -0,0 +1,20 @@
package handlers
import (
"html/template"
"net/http"
)
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/home.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
)
type response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data any `json:"data"`
}
func Respond(w http.ResponseWriter, message string, v any, statusCode int) {
rsp := response{
Success: true,
Message: message,
Data: v,
}
b, err := json.Marshal(rsp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
_, err = w.Write(b)
if err != nil && !errors.Is(err, context.Canceled) {
log.Printf("could not write http response: %v\n", err)
}
}
func RespondJson(w http.ResponseWriter, v any) {
b, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, err = w.Write(b)
if err != nil && !errors.Is(err, context.Canceled) {
log.Printf("could not write http response: %v\n", err)
}
}

View File

@@ -0,0 +1,97 @@
package handlers
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/zhang2092/mediahls/internal/db"
"github.com/zhang2092/mediahls/internal/pkg/config"
"github.com/zhang2092/mediahls/internal/pkg/logger"
"github.com/zhang2092/mediahls/internal/pkg/token"
)
type Server struct {
conf *config.Config
router *mux.Router
store db.Store
tokenMaker token.Maker
}
func NewServer(conf *config.Config, store db.Store) (*Server, error) {
tokenMaker, err := token.NewPasetoMaker(conf.TokenSymmetricKey)
if err != nil {
return nil, fmt.Errorf("cannot create token maker: %w", err)
}
server := &Server{
conf: conf,
store: store,
tokenMaker: tokenMaker,
}
server.setupRouter()
return server, nil
}
func (server *Server) setupRouter() {
router := mux.NewRouter()
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
router.HandleFunc("/login", server.loginView).Methods(http.MethodGet)
router.HandleFunc("/login", server.login).Methods(http.MethodPost)
router.HandleFunc("/", server.home).Methods(http.MethodGet)
router.HandleFunc("/play/{xid}", server.play).Methods(http.MethodGet)
router.HandleFunc("/upload", server.upload).Methods(http.MethodGet, http.MethodPost)
server.router = router
}
func (server *Server) Start(db *sql.DB) {
srv := &http.Server{
Addr: server.conf.ServerAddress,
Handler: server.router,
}
go func() {
log.Printf("server start on: %s\n", server.conf.ServerAddress)
if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
log.Printf("listen: %s\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.Close(); err != nil {
log.Fatal("Server db to shutdown:", err)
}
if err := logger.Logger.Sync(); err != nil {
log.Fatal("Server log sync:", err)
}
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}

102
internal/handlers/upload.go Normal file
View File

@@ -0,0 +1,102 @@
package handlers
import (
"html/template"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"time"
nanoId "github.com/matoous/go-nanoid"
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
)
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
defer r.Body.Close()
file, fileHeader, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
defer file.Close()
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// filetype := http.DetectContentType(buff)
// if filetype != "image/jpeg" && filetype != "image/png" {
// http.Error(w, "The provided file format is not allowed. Please upload a JPEG or PNG image", http.StatusBadRequest)
// return
// }
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dir := path.Join("upload", time.Now().Format("20060102"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
filename, err := nanoId.Nanoid()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := path.Join("", dir, filename+filepath.Ext(fileHeader.Filename))
f, err := os.Create(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
if err != nil {
log.Printf("%v", err)
}
return
}
t, err := template.ParseFiles("web/templates/upload.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}

146
internal/handlers/user.go Normal file
View File

@@ -0,0 +1,146 @@
package handlers
import (
"database/sql"
"html/template"
"net/http"
"time"
"github.com/zhang2092/mediahls/internal/db"
pwd "github.com/zhang2092/mediahls/internal/pkg/password"
)
func (server *Server) registerView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/register.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
type userResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt,
}
}
func (server *Server) register(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
username := r.PostFormValue("username")
email := r.PostFormValue("email")
password := r.PostFormValue("password")
hashedPassword, err := pwd.BcryptHashPassword(password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
arg := db.CreateUserParams{
Username: username,
HashedPassword: hashedPassword,
Email: email,
}
user, err := server.store.CreateUser(r.Context(), arg)
if err != nil {
if server.store.IsUniqueViolation(err) {
http.Error(w, "数据已经存在", http.StatusInternalServerError)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rsp := newUserResponse(user)
Respond(w, "ok", rsp, http.StatusOK)
}
func (server *Server) loginView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/login.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
type loginUserResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
User userResponse `json:"user"`
}
func (server *Server) login(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
username := r.PostFormValue("username")
password := r.PostFormValue("password")
ctx := r.Context()
user, err := server.store.GetUserByName(ctx, username)
if err != nil {
if server.store.IsNoRows(sql.ErrNoRows) {
http.Error(w, "用户不存在", http.StatusInternalServerError)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = pwd.BcryptComparePassword(password, user.HashedPassword)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
user.ID,
user.Username,
server.conf.AccessTokenDuration,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rsp := loginUserResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiresAt.Time,
User: newUserResponse(user),
}
Respond(w, "ok", rsp, http.StatusOK)
}

View File

@@ -0,0 +1,20 @@
package handlers
import "net/http"
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
}
/*
// 直接播放mp4
video, err := os.Open("web/statics/git.mp4")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("failed to open file"))
return
}
defer video.Close()
http.ServeContent(w, r, "git", time.Now(), video)
*/

View File

@@ -0,0 +1,33 @@
package config
import (
"time"
"github.com/spf13/viper"
)
type Config struct {
DBDriver string `mapstructure:"DB_DRIVER"`
DBSource string `mapstructure:"DB_SOURCE"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`
}
func LoadConfig(path string) (*Config, error) {
viper.AddConfigPath(path)
viper.SetConfigName("app")
viper.SetConfigType("env")
// 自动识别加载环境变量
viper.AutomaticEnv()
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
var config Config
err = viper.Unmarshal(&config)
return &config, err
}

View File

@@ -0,0 +1,24 @@
package convert
import (
"os/exec"
"strings"
)
func ConvertHLS(savePath, filePath string) error {
binary, err := exec.LookPath("ffmpeg")
if err != nil {
return err
}
// ffmpeg -i web/statics/git.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -hls_segment_filename %d.ts -f hls web/statics/git.m3u8
command := "-i " + filePath + " -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls " + savePath + "index.m3u8"
args := strings.Split(command, " ")
cmd := exec.Command(binary, args...)
_, err = cmd.Output()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,14 @@
package fileutil
import "os"
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

View File

@@ -0,0 +1,33 @@
package logger
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.SugaredLogger
func NewLogger() {
core := zapcore.NewCore(getEncoder(), getLogWriter(), 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 {
lumberJackLogger := &lumberjack.Logger{
Filename: "./log/run.log", // 日志文件的位置
MaxSize: 10, // 在进行切割之前日志文件的最大大小以MB为单位
MaxBackups: 100, // 保留旧文件的最大个数
MaxAge: 365, // 保留旧文件的最大天数
Compress: false, // 是否压缩/归档旧文件
}
return zapcore.AddSync(lumberJackLogger)
}

View File

@@ -0,0 +1,79 @@
package password
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)
// ******************** scrypt ********************
// ScryptHashPassword scrypt 加密
// password 原始密码
func ScryptHashPassword(password string) (string, error) {
// example for making salt - https://play.golang.org/p/_Aw6WeWC42I
salt := make([]byte, 32)
_, err := rand.Read(salt)
if err != nil {
return "", err
}
// using recommended cost parameters from - https://godoc.org/golang.org/x/crypto/scrypt
shash, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return "", err
}
// return hex-encoded string with salt appended to password
hashedPW := fmt.Sprintf("%s.%s", hex.EncodeToString(shash), hex.EncodeToString(salt))
return hashedPW, nil
}
// ScryptComparePassword 判断密码是否正确
// storedPassword 加密密码
// suppliedPassword 原始密码
func ScryptComparePassword(storedPassword string, suppliedPassword string) error {
pwsalt := strings.Split(storedPassword, ".")
// check supplied password salted with hash
salt, err := hex.DecodeString(pwsalt[1])
if err != nil {
return fmt.Errorf("unable to verify user password")
}
shash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32)
if err != nil {
return err
}
if hex.EncodeToString(shash) == pwsalt[0] {
return nil
}
return fmt.Errorf("password error")
}
// ******************** bcrypt ********************
// BcryptHashPassword bcrypt 加密
// password 原始密码
func BcryptHashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
// BcryptComparePassword 判断密码是否正确
// hashedPassword 加密密码
// password 原始密码
func BcryptComparePassword(hashedPassword string, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

View File

@@ -0,0 +1,55 @@
package token
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const minSecretKeySize = 32
// JWTMaker JSON Web Token
type JWTMaker struct {
secretKey string
}
// NewJWTMaker 创建一个新的JWTMaker
func NewJWTMaker(secretKey string) (Maker, error) {
if len(secretKey) < minSecretKeySize {
return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize)
}
return &JWTMaker{secretKey}, nil
}
// CreateToken 根据用户名和时间创建一个新的token
func (maker *JWTMaker) CreateToken(id string, username string, duration time.Duration) (string, *Payload, error) {
payload := NewPayload(id, username, duration)
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
token, err := jwtToken.SignedString([]byte(maker.secretKey))
return token, payload, err
}
// VerifyToken checks if the token is valid or not
func (maker *JWTMaker) VerifyToken(t string) (*Payload, error) {
keyFunc := func(tk *jwt.Token) (interface{}, error) {
_, ok := tk.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, ErrInvalidToken
}
return []byte(maker.secretKey), nil
}
jwtToken, err := jwt.ParseWithClaims(t, &Payload{}, keyFunc)
if err != nil {
return nil, ErrInvalidToken
}
payload, ok := jwtToken.Claims.(*Payload)
if !ok {
return nil, ErrInvalidToken
}
return payload, nil
}

View File

@@ -0,0 +1,14 @@
package token
import (
"time"
)
// Maker 管理token的接口定义
type Maker interface {
// CreateToken 根据用户名和时间创建一个新的token
CreateToken(id string, username string, duration time.Duration) (string, *Payload, error)
// VerifyToken 校验token是否正确
VerifyToken(token string) (*Payload, error)
}

View File

@@ -0,0 +1,53 @@
package token
import (
"fmt"
"time"
"github.com/aead/chacha20poly1305"
"github.com/o1egl/paseto"
)
// PasetoMaker is a PASETO token maker
type PasetoMaker struct {
paseto *paseto.V2
symmetricKey []byte
}
// NewPasetoMaker creates a new PasetoMaker
func NewPasetoMaker(symmetricKey string) (Maker, error) {
if len(symmetricKey) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("invalid key size: must be exactly %d characters", chacha20poly1305.KeySize)
}
maker := &PasetoMaker{
paseto: paseto.NewV2(),
symmetricKey: []byte(symmetricKey),
}
return maker, nil
}
// CreateToken creates a new token for a specific username and duration
func (maker *PasetoMaker) CreateToken(id string, username string, duration time.Duration) (string, *Payload, error) {
payload := NewPayload(id, username, duration)
token, err := maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
return token, payload, err
}
// VerifyToken checks if the token is valid or not
func (maker *PasetoMaker) VerifyToken(t string) (*Payload, error) {
payload := &Payload{}
err := maker.paseto.Decrypt(t, maker.symmetricKey, payload, nil)
if err != nil {
return nil, ErrInvalidToken
}
err = payload.Valid()
if err != nil {
return nil, err
}
return payload, nil
}

View File

@@ -0,0 +1,43 @@
package token
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Different types of error returned by the VerifyToken function
var (
ErrInvalidToken = errors.New("token is invalid")
ErrExpiredToken = errors.New("token has expired")
)
// Payload contains the payload data of the token
type Payload struct {
ID string `json:"id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// NewPayload creates a new token payload with a specific username and duration
func NewPayload(id string, username string, duration time.Duration) *Payload {
payload := &Payload{
ID: id,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)), // 过期时间
IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间
},
}
return payload
}
// Valid checks if the token payload is valid or not
func (payload *Payload) Valid() error {
if time.Now().After(payload.ExpiresAt.Time) {
return ErrExpiredToken
}
return nil
}

View File

@@ -0,0 +1,92 @@
package services
import (
"errors"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"time"
nanoId "github.com/matoous/go-nanoid"
)
var (
// 文件最大数量
maxFileSize = 10 << 20
ErrUnsupportedFileFormat = errors.New("文件格式不支持")
ErrFileStorePath = errors.New("文件存储路径异常")
ErrFileGenerateName = errors.New("文件名称生成异常")
ErrFileSaveFailed = errors.New("文件保存失败")
)
func UploadFile(r io.Reader) (string, error) {
img, format, err := image.Decode(r)
if err != nil {
return "", fmt.Errorf("读取文件错误")
}
if errors.Is(err, image.ErrFormat) {
return "", ErrUnsupportedFileFormat
}
if format != "png" && format != "jpeg" && format != "gif" {
return "", ErrUnsupportedFileFormat
}
// 存放目录
dir := path.Join("upload", time.Now().Format(time.DateOnly))
exist, _ := pathExists(dir)
if !exist {
err := mkDir(dir)
if err != nil {
return "", ErrFileStorePath
}
}
filename, err := nanoId.Nanoid()
if err != nil {
return "", ErrFileGenerateName
}
filePath := path.Join("", dir, filename+"."+format)
f, err := os.Create(filePath)
if err != nil {
return "", ErrFileStorePath
}
defer func(f *os.File) {
_ = f.Close()
}(f)
var e error
switch format {
case "png":
e = png.Encode(f, img)
case "jpeg":
e = jpeg.Encode(f, img, nil)
case "gif":
e = gif.Encode(f, img, nil)
}
if e != nil {
return "", ErrFileSaveFailed
}
return "/" + filePath, nil
}
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func mkDir(path string) error {
return os.MkdirAll(path, os.ModePerm)
}