开发...
This commit is contained in:
@@ -12,6 +12,7 @@ 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)
|
||||
GetUserByEmail(ctx context.Context, email 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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
username, hashed_password, email
|
||||
id, username, hashed_password, email
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
@@ -25,6 +25,10 @@ WHERE id = $1 LIMIT 1;
|
||||
SELECT * FROM users
|
||||
WHERE username = $1 LIMIT 1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users
|
||||
WHERE email = $1 LIMIT 1;
|
||||
|
||||
-- name: ListUsers :many
|
||||
SELECT * FROM users
|
||||
ORDER BY id
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
CREATE TABLE "users" (
|
||||
"id" varchar NOT NULL PRIMARY KEY,
|
||||
"username" varchar NOT NULL,
|
||||
"username" varchar NOT NULL UNIQUE,
|
||||
"hashed_password" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"email" varchar NOT NULL UNIQUE,
|
||||
"created_at" timestamptz NOT NULL DEFAULT (now())
|
||||
);
|
||||
@@ -11,21 +11,27 @@ import (
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
username, hashed_password, email
|
||||
id, username, hashed_password, email
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
RETURNING id, username, hashed_password, email, created_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
ID string `json:"id"`
|
||||
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)
|
||||
row := q.db.QueryRowContext(ctx, createUser,
|
||||
arg.ID,
|
||||
arg.Username,
|
||||
arg.HashedPassword,
|
||||
arg.Email,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
@@ -65,6 +71,24 @@ func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, username, hashed_password, email, created_at FROM users
|
||||
WHERE email = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
|
||||
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
|
||||
|
||||
37
internal/handlers/base.go
Normal file
37
internal/handlers/base.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/zhang2092/mediahls/internal/pkg/cookie"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthorizeCookie = "authorize"
|
||||
ContextUser CtxTypeUser = "context_user"
|
||||
)
|
||||
|
||||
type CtxTypeUser string
|
||||
|
||||
type authorize struct {
|
||||
AuthID string `json:"auth_id"`
|
||||
AuthName string `json:"auth_name"`
|
||||
}
|
||||
|
||||
func genId() string {
|
||||
id := xid.New()
|
||||
return id.String()
|
||||
}
|
||||
|
||||
func (server *Server) isRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := server.withCookie(r)
|
||||
if err != nil {
|
||||
// 1. 删除cookie
|
||||
cookie.DeleteCookie(w, cookie.AuthorizeName)
|
||||
return
|
||||
}
|
||||
|
||||
// cookie 校验成功
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
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
|
||||
}
|
||||
type pageData struct {
|
||||
AuthID string
|
||||
AuthName string
|
||||
}
|
||||
|
||||
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
|
||||
pd := pageData{}
|
||||
auth, err := server.withCookie(r)
|
||||
if err == nil {
|
||||
pd.AuthID = auth.AuthID
|
||||
pd.AuthName = auth.AuthName
|
||||
}
|
||||
renderHome(w, pd)
|
||||
}
|
||||
|
||||
func renderHome(w http.ResponseWriter, data any) {
|
||||
render(w, data, "web/templates/home.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
}
|
||||
|
||||
65
internal/handlers/middleware.go
Normal file
65
internal/handlers/middleware.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/zhang2092/mediahls/internal/pkg/convert"
|
||||
)
|
||||
|
||||
func (server *Server) authorizeMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
u, err := server.withCookie(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Printf("json marshal authorize user: %v", err)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, ContextUser, b)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (server *Server) withCookie(r *http.Request) (*authorize, error) {
|
||||
cookie, err := r.Cookie(AuthorizeCookie)
|
||||
if err != nil {
|
||||
log.Printf("get cookie: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &authorize{}
|
||||
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
|
||||
if err != nil {
|
||||
log.Printf("secure decode cookie: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func withUser(ctx context.Context) *authorize {
|
||||
var result authorize
|
||||
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
|
||||
log.Printf("ctx: %s", ctxValue)
|
||||
if err != nil {
|
||||
log.Printf("1: %v", err)
|
||||
return nil
|
||||
}
|
||||
err = json.Unmarshal(ctxValue, &result)
|
||||
if err != nil {
|
||||
log.Printf("2: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
23
internal/handlers/render.go
Normal file
23
internal/handlers/render.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func render(w http.ResponseWriter, data any, tmpls ...string) {
|
||||
t, err := template.ParseFiles(tmpls...)
|
||||
if err != nil {
|
||||
log.Printf("template parse: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.Execute(w, data)
|
||||
if err != nil {
|
||||
log.Printf("template execute: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/zhang2092/mediahls/internal/db"
|
||||
"github.com/zhang2092/mediahls/internal/pkg/config"
|
||||
"github.com/zhang2092/mediahls/internal/pkg/logger"
|
||||
@@ -20,8 +21,9 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
conf *config.Config
|
||||
router *mux.Router
|
||||
conf *config.Config
|
||||
router *mux.Router
|
||||
secureCookie *securecookie.SecureCookie
|
||||
|
||||
store db.Store
|
||||
tokenMaker token.Maker
|
||||
@@ -33,10 +35,16 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
|
||||
return nil, fmt.Errorf("cannot create token maker: %w", err)
|
||||
}
|
||||
|
||||
hashKey := securecookie.GenerateRandomKey(32)
|
||||
blockKey := securecookie.GenerateRandomKey(32)
|
||||
secureCookie := securecookie.New(hashKey, blockKey)
|
||||
secureCookie.MaxAge(7200)
|
||||
|
||||
server := &Server{
|
||||
conf: conf,
|
||||
store: store,
|
||||
tokenMaker: tokenMaker,
|
||||
conf: conf,
|
||||
secureCookie: secureCookie,
|
||||
store: store,
|
||||
tokenMaker: tokenMaker,
|
||||
}
|
||||
|
||||
server.setupRouter()
|
||||
@@ -45,18 +53,23 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
|
||||
|
||||
func (server *Server) setupRouter() {
|
||||
router := mux.NewRouter()
|
||||
router.Use(mux.CORSMethodMiddleware(router))
|
||||
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("/logout", server.logout).Methods(http.MethodGet)
|
||||
router.HandleFunc("/", server.home).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/play/{xid}", server.play).Methods(http.MethodGet)
|
||||
router.HandleFunc("/media/{xid}/stream/", server.stream).Methods(http.MethodGet)
|
||||
router.HandleFunc("/media/{xid}/stream/{segName:index[0-9]+.ts}", server.stream).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/upload", server.upload).Methods(http.MethodGet, http.MethodPost)
|
||||
subRouter := router.PathPrefix("/").Subrouter()
|
||||
subRouter.Use(server.authorizeMiddleware)
|
||||
subRouter.HandleFunc("/upload", server.uploadView).Methods(http.MethodGet)
|
||||
subRouter.HandleFunc("/upload", server.upload).Methods(http.MethodPost)
|
||||
|
||||
server.router = router
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -14,89 +13,83 @@ import (
|
||||
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
|
||||
)
|
||||
|
||||
func (server *Server) uploadView(w http.ResponseWriter, r *http.Request) {
|
||||
user := withUser(r.Context())
|
||||
log.Printf("%v", user)
|
||||
renderUpload(w, nil)
|
||||
}
|
||||
|
||||
func renderUpload(w http.ResponseWriter, data any) {
|
||||
render(w, data, "web/templates/me/upload.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
}
|
||||
|
||||
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
defer r.Body.Close()
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
t, err := template.ParseFiles("web/templates/upload.html.tmpl")
|
||||
// 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 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.Execute(w, nil)
|
||||
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 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,45 +2,72 @@ package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zhang2092/mediahls/internal/db"
|
||||
"github.com/zhang2092/mediahls/internal/pkg/cookie"
|
||||
pwd "github.com/zhang2092/mediahls/internal/pkg/password"
|
||||
)
|
||||
|
||||
func (server *Server) registerView(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := template.ParseFiles("web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.Execute(w, nil)
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 是否已经登录
|
||||
server.isRedirect(w, r)
|
||||
renderRegister(w, nil)
|
||||
}
|
||||
|
||||
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 renderRegister(w http.ResponseWriter, data any) {
|
||||
render(w, data, "web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
}
|
||||
|
||||
func newUserResponse(user db.User) userResponse {
|
||||
return userResponse{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
CreatedAt: user.CreatedAt,
|
||||
// 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 viladatorRegister(email, username, password string) (*respErrs, bool) {
|
||||
ok := true
|
||||
errs := &respErrs{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if !ValidateRxEmail(email) {
|
||||
errs.EmailErr = "请填写正确的邮箱地址"
|
||||
ok = false
|
||||
}
|
||||
if !ValidateRxUsername(username) {
|
||||
errs.UsernameErr = "名称(6-20,字母,数字)"
|
||||
ok = false
|
||||
}
|
||||
if !ValidatePassword(password) {
|
||||
errs.PasswordErr = "密码(8-20位)"
|
||||
ok = false
|
||||
}
|
||||
|
||||
return errs, ok
|
||||
}
|
||||
|
||||
type respErrs struct {
|
||||
Summary string
|
||||
Email string
|
||||
Username string
|
||||
Password string
|
||||
EmailErr string
|
||||
UsernameErr string
|
||||
PasswordErr string
|
||||
}
|
||||
|
||||
func (server *Server) register(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -51,9 +78,14 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
username := r.PostFormValue("username")
|
||||
email := r.PostFormValue("email")
|
||||
username := r.PostFormValue("username")
|
||||
password := r.PostFormValue("password")
|
||||
errs, ok := viladatorRegister(email, username, password)
|
||||
if !ok {
|
||||
renderRegister(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := pwd.BcryptHashPassword(password)
|
||||
if err != nil {
|
||||
@@ -62,88 +94,116 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
arg := db.CreateUserParams{
|
||||
ID: genId(),
|
||||
Username: username,
|
||||
HashedPassword: hashedPassword,
|
||||
Email: email,
|
||||
}
|
||||
|
||||
user, err := server.store.CreateUser(r.Context(), arg)
|
||||
_, err = server.store.CreateUser(r.Context(), arg)
|
||||
if err != nil {
|
||||
if server.store.IsUniqueViolation(err) {
|
||||
http.Error(w, "数据已经存在", http.StatusInternalServerError)
|
||||
errs.Summary = "邮箱或名称已经存在"
|
||||
renderRegister(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
errs.Summary = "请求网络错误,请刷新重试"
|
||||
renderRegister(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
rsp := newUserResponse(user)
|
||||
Respond(w, "ok", rsp, http.StatusOK)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
|
||||
// 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/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = t.Execute(w, nil)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// 是否已经登录
|
||||
server.isRedirect(w, r)
|
||||
renderLogin(w, nil)
|
||||
}
|
||||
|
||||
type loginUserResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
|
||||
User userResponse `json:"user"`
|
||||
func renderLogin(w http.ResponseWriter, data any) {
|
||||
render(w, data, "web/templates/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
}
|
||||
|
||||
// type loginUserResponse struct {
|
||||
// AccessToken string `json:"access_token"`
|
||||
// AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
|
||||
// User userResponse `json:"user"`
|
||||
// }
|
||||
|
||||
func viladatorLogin(email, password string) (*respErrs, bool) {
|
||||
ok := true
|
||||
errs := &respErrs{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if !ValidateRxEmail(email) {
|
||||
errs.EmailErr = "请填写正确的邮箱地址"
|
||||
ok = false
|
||||
}
|
||||
if len(password) == 0 {
|
||||
errs.PasswordErr = "请填写正确的密码"
|
||||
ok = false
|
||||
}
|
||||
|
||||
return errs, ok
|
||||
}
|
||||
|
||||
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)
|
||||
renderLogin(w, respErrs{Summary: "请求网络错误,请刷新重试"})
|
||||
return
|
||||
}
|
||||
|
||||
username := r.PostFormValue("username")
|
||||
email := r.PostFormValue("email")
|
||||
password := r.PostFormValue("password")
|
||||
ctx := r.Context()
|
||||
errs, ok := viladatorLogin(email, password)
|
||||
if !ok {
|
||||
renderLogin(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := server.store.GetUserByName(ctx, username)
|
||||
ctx := r.Context()
|
||||
user, err := server.store.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if server.store.IsNoRows(sql.ErrNoRows) {
|
||||
http.Error(w, "用户不存在", http.StatusInternalServerError)
|
||||
errs.Summary = "邮箱或密码错误"
|
||||
renderLogin(w, errs)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
errs.Summary = "请求网络错误,请刷新重试"
|
||||
renderLogin(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
err = pwd.BcryptComparePassword(password, user.HashedPassword)
|
||||
err = pwd.BcryptComparePassword(user.HashedPassword, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
errs.Summary = "邮箱或密码错误"
|
||||
renderLogin(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
|
||||
user.ID,
|
||||
user.Username,
|
||||
server.conf.AccessTokenDuration,
|
||||
)
|
||||
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &authorize{AuthID: user.ID, AuthName: user.Username})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
errs.Summary = "请求网络错误,请刷新重试(cookie)"
|
||||
renderLogin(w, errs)
|
||||
return
|
||||
}
|
||||
|
||||
rsp := loginUserResponse{
|
||||
AccessToken: accessToken,
|
||||
AccessTokenExpiresAt: accessPayload.ExpiresAt.Time,
|
||||
User: newUserResponse(user),
|
||||
}
|
||||
Respond(w, "ok", rsp, http.StatusOK)
|
||||
c := cookie.NewCookie(cookie.AuthorizeName, encoded, time.Now().Add(time.Duration(7200)*time.Second))
|
||||
http.SetCookie(w, c)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (server *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
cookie.DeleteCookie(w, cookie.AuthorizeName)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
38
internal/handlers/validator.go
Normal file
38
internal/handlers/validator.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
rxPhone = regexp.MustCompile(`^(13|14|15|16|17|18|19)\d{9}$`)
|
||||
rxEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
|
||||
rxUsername = regexp.MustCompile(`^[a-z0-9A-Z]{6,20}$`) // 6到20位(字母,数字)
|
||||
//rxPassword = regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]{8,18}$`) // 最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
|
||||
)
|
||||
|
||||
func ValidateRxPhone(phone string) bool {
|
||||
phone = strings.TrimSpace(phone)
|
||||
return rxPhone.MatchString(phone)
|
||||
}
|
||||
|
||||
func ValidateRxEmail(email string) bool {
|
||||
email = strings.TrimSpace(email)
|
||||
return rxEmail.MatchString(email)
|
||||
}
|
||||
|
||||
func ValidateRxUsername(username string) bool {
|
||||
username = strings.TrimSpace(username)
|
||||
return rxUsername.MatchString(username)
|
||||
}
|
||||
|
||||
// func ValidateRxPassword(password string) bool {
|
||||
// password = strings.TrimSpace(password)
|
||||
// return rxPassword.MatchString(password)
|
||||
// }
|
||||
|
||||
func ValidatePassword(password string) bool {
|
||||
password = strings.TrimSpace(password)
|
||||
return len(password) >= 8 && len(password) <= 20
|
||||
}
|
||||
@@ -1,9 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type playData struct {
|
||||
AuthID string
|
||||
AuthName string
|
||||
Url string
|
||||
}
|
||||
|
||||
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
xid := vars["xid"]
|
||||
data := playData{
|
||||
Url: "/media/" + xid + "/stream/",
|
||||
}
|
||||
auth, err := server.withCookie(r)
|
||||
if err == nil {
|
||||
data.AuthID = auth.AuthID
|
||||
data.AuthName = auth.AuthName
|
||||
}
|
||||
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -18,3 +39,45 @@ defer video.Close()
|
||||
|
||||
http.ServeContent(w, r, "git", time.Now(), video)
|
||||
*/
|
||||
|
||||
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
mId := vars["xid"]
|
||||
fmt.Println(mId)
|
||||
|
||||
segName, ok := vars["segName"]
|
||||
|
||||
if !ok {
|
||||
mediaBase := getMediaBase(mId)
|
||||
m3u8Name := "index.m3u8"
|
||||
serveHlsM3u8(response, request, mediaBase, m3u8Name)
|
||||
} else {
|
||||
mediaBase := getMediaBase(mId)
|
||||
serveHlsTs(response, request, mediaBase, segName)
|
||||
}
|
||||
|
||||
}
|
||||
func getMediaBase(mId string) string {
|
||||
mediaRoot := "media"
|
||||
return fmt.Sprintf("%s/%s", mediaRoot, mId)
|
||||
}
|
||||
|
||||
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
|
||||
fmt.Println("serveHlsM3u8...")
|
||||
|
||||
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
|
||||
fmt.Println(mediaFile)
|
||||
http.ServeFile(w, r, mediaFile)
|
||||
w.Header().Set("Content-Type", "application/x-mpegURL")
|
||||
|
||||
}
|
||||
|
||||
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
|
||||
fmt.Println("serveHlsTs...")
|
||||
|
||||
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
|
||||
fmt.Println(mediaFile)
|
||||
|
||||
http.ServeFile(w, r, mediaFile)
|
||||
w.Header().Set("Content-Type", "video/MP2T")
|
||||
}
|
||||
|
||||
99
internal/pkg/convert/any.go
Normal file
99
internal/pkg/convert/any.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
)
|
||||
|
||||
// Copied from html/template/content.go.
|
||||
// indirectToStringerOrError returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
|
||||
// or error,
|
||||
func indirectToStringerOrError(a any) any {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
v := reflect.ValueOf(a)
|
||||
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Pointer && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
// ToStringE converts any type to a string type.
|
||||
func ToStringE(i any) (string, error) {
|
||||
i = indirectToStringerOrError(i)
|
||||
|
||||
switch s := i.(type) {
|
||||
case string:
|
||||
return s, nil
|
||||
case bool:
|
||||
return strconv.FormatBool(s), nil
|
||||
case float64:
|
||||
return strconv.FormatFloat(s, 'f', -1, 64), nil
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(s), 'f', -1, 32), nil
|
||||
case int:
|
||||
return strconv.Itoa(s), nil
|
||||
case int64:
|
||||
return strconv.FormatInt(s, 10), nil
|
||||
case int32:
|
||||
return strconv.Itoa(int(s)), nil
|
||||
case int16:
|
||||
return strconv.FormatInt(int64(s), 10), nil
|
||||
case int8:
|
||||
return strconv.FormatInt(int64(s), 10), nil
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(s), 10), nil
|
||||
case uint64:
|
||||
return strconv.FormatUint(uint64(s), 10), nil
|
||||
case uint32:
|
||||
return strconv.FormatUint(uint64(s), 10), nil
|
||||
case uint16:
|
||||
return strconv.FormatUint(uint64(s), 10), nil
|
||||
case uint8:
|
||||
return strconv.FormatUint(uint64(s), 10), nil
|
||||
case json.Number:
|
||||
return s.String(), nil
|
||||
case []byte:
|
||||
return string(s), nil
|
||||
case template.HTML:
|
||||
return string(s), nil
|
||||
case template.URL:
|
||||
return string(s), nil
|
||||
case template.JS:
|
||||
return string(s), nil
|
||||
case template.CSS:
|
||||
return string(s), nil
|
||||
case template.HTMLAttr:
|
||||
return string(s), nil
|
||||
case nil:
|
||||
return "", nil
|
||||
case fmt.Stringer:
|
||||
return s.String(), nil
|
||||
case error:
|
||||
return s.Error(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringE converts any type to a string type.
|
||||
func ToByteE(i any) ([]byte, error) {
|
||||
i = indirectToStringerOrError(i)
|
||||
|
||||
switch s := i.(type) {
|
||||
case []byte:
|
||||
return s, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to cast %#v of type %T to string", i, i)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,41 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
|
||||
)
|
||||
|
||||
func ConvertHLS(savePath, filePath string) error {
|
||||
exist, _ := fileutil.PathExists(savePath)
|
||||
if !exist {
|
||||
err := os.MkdirAll(savePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
binary, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
log.Println("1: ", err)
|
||||
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"
|
||||
log.Println(command)
|
||||
args := strings.Split(command, " ")
|
||||
cmd := exec.Command(binary, args...)
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
log.Println("2: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// ffmpeg -i upload/20231129/o6e6qKaMdk0VC1Ys2SHnr.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -f hls -hls_time 10 -segment_list web/statics/js/index.m3u8 web/statics/mmm/index.m3u8
|
||||
// ffmpeg -i upload/20231129/o6e6qKaMdk0VC1Ys2SHnr.mp4 -c copy -map 0 -f segment -segment_list web/statics/js/index.m3u8 -segment_time 20 web/statics/js/index_%3d.ts
|
||||
}
|
||||
41
internal/pkg/cookie/cookie.go
Normal file
41
internal/pkg/cookie/cookie.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package cookie
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthorizeName = "authorize"
|
||||
)
|
||||
|
||||
func NewCookie(name, value string, expired time.Time) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
Secure: false, // true->只能https站点操作
|
||||
HttpOnly: true, // true->js不能捕获
|
||||
Expires: expired,
|
||||
}
|
||||
}
|
||||
|
||||
func SetCookie(w http.ResponseWriter, name, value string, expired time.Time) {
|
||||
cookie := NewCookie(name, value, expired)
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func ReadCookie(r *http.Request, name string) (string, error) {
|
||||
cookie, err := r.Cookie(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
||||
func DeleteCookie(w http.ResponseWriter, name string) {
|
||||
cookie := NewCookie(name, "", time.Now().Add(time.Duration(-10)*time.Second))
|
||||
cookie.MaxAge = -1
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
Reference in New Issue
Block a user