first commit
This commit is contained in:
31
internal/db/db.go
Normal file
31
internal/db/db.go
Normal 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
17
internal/db/models.go
Normal 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
20
internal/db/querier.go
Normal 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)
|
||||
32
internal/db/query/user.sql
Normal file
32
internal/db/query/user.sql
Normal 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;
|
||||
1
internal/db/schema/000001_init_schema.down.sql
Normal file
1
internal/db/schema/000001_init_schema.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE "users";
|
||||
7
internal/db/schema/000001_init_schema.up.sql
Normal file
7
internal/db/schema/000001_init_schema.up.sql
Normal 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
61
internal/db/store.go
Normal 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
152
internal/db/user.sql.go
Normal 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
20
internal/handlers/home.go
Normal 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
|
||||
}
|
||||
}
|
||||
50
internal/handlers/response.go
Normal file
50
internal/handlers/response.go
Normal 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)
|
||||
}
|
||||
}
|
||||
97
internal/handlers/server.go
Normal file
97
internal/handlers/server.go
Normal 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
102
internal/handlers/upload.go
Normal 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
146
internal/handlers/user.go
Normal 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)
|
||||
}
|
||||
20
internal/handlers/video.go
Normal file
20
internal/handlers/video.go
Normal 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)
|
||||
*/
|
||||
33
internal/pkg/config/config.go
Normal file
33
internal/pkg/config/config.go
Normal 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
|
||||
}
|
||||
24
internal/pkg/convert/convert.go
Normal file
24
internal/pkg/convert/convert.go
Normal 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
|
||||
}
|
||||
14
internal/pkg/fileutil/fileutil.go
Normal file
14
internal/pkg/fileutil/fileutil.go
Normal 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
|
||||
}
|
||||
33
internal/pkg/logger/logger.go
Normal file
33
internal/pkg/logger/logger.go
Normal 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)
|
||||
}
|
||||
79
internal/pkg/password/password.go
Normal file
79
internal/pkg/password/password.go
Normal 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))
|
||||
}
|
||||
55
internal/pkg/token/jwt_maker.go
Normal file
55
internal/pkg/token/jwt_maker.go
Normal 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
|
||||
}
|
||||
14
internal/pkg/token/maker.go
Normal file
14
internal/pkg/token/maker.go
Normal 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)
|
||||
}
|
||||
53
internal/pkg/token/paseto_maker.go
Normal file
53
internal/pkg/token/paseto_maker.go
Normal 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
|
||||
}
|
||||
43
internal/pkg/token/payload.go
Normal file
43
internal/pkg/token/payload.go
Normal 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
|
||||
}
|
||||
92
internal/services/upload.go
Normal file
92
internal/services/upload.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user