feat: implement single binary build and env-based auth
- Unify login page styling with share page - Add AGENTS.md with build commands and code style guidelines - Add dev.sh and make.sh for development and production builds - Implement single binary build with embedded frontend using embed.FS - Change auth configuration from CLI flag to env variables (USHARE_USERNAME, USHARE_PASSWORD) - Set default credentials: admin / ushare@123 - Fix static file serving for SPA routes
This commit is contained in:
@@ -2,19 +2,20 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/nf/nft/tool"
|
||||
"github.com/loveuer/ushare/internal/handler"
|
||||
"github.com/loveuer/ushare/internal/opt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) <-chan struct{} {
|
||||
app := nf.New(nf.Config{BodyLimit: 10 * 1024 * 1024 * 1024})
|
||||
|
||||
app.Get("/api/available", func(c *nf.Ctx) error {
|
||||
app.Get("/api/healthz", func(c *nf.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
@@ -33,6 +34,9 @@ func Start(ctx context.Context) <-chan struct{} {
|
||||
api.Get("/ws", handler.LocalWS())
|
||||
}
|
||||
|
||||
// 静态文件服务 - 作为中间件处理
|
||||
app.Use(handler.ServeFrontendMiddleware())
|
||||
|
||||
ready := make(chan struct{})
|
||||
ln, err := net.Listen("tcp", opt.Cfg.Address)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,11 +21,11 @@ func (um *userManager) Login(username string, password string) (*model.User, err
|
||||
now = time.Now()
|
||||
)
|
||||
|
||||
if username != "admin" {
|
||||
if username != opt.Cfg.Username {
|
||||
return nil, errors.New("账号或密码错误")
|
||||
}
|
||||
|
||||
if !tool.ComparePassword(password, opt.Cfg.Auth) {
|
||||
if !tool.ComparePassword(password, opt.Cfg.Password) {
|
||||
return nil, errors.New("账号或密码错误")
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func AuthVerify() nf.HandlerFunc {
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
if opt.Cfg.Auth == "" {
|
||||
if opt.Cfg.Username == "" || opt.Cfg.Password == "" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
|
||||
114
internal/handler/static.go
Normal file
114
internal/handler/static.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/ushare/internal/static"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ServeFrontend() nf.HandlerFunc {
|
||||
assets := static.Frontend()
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
path := strings.TrimPrefix(c.Path(), "/")
|
||||
if path == "" || path == "/" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
file, err := assets.Open(path)
|
||||
if err != nil {
|
||||
if err.Error() == "file does not exist" {
|
||||
return serveIndex(assets, c)
|
||||
}
|
||||
return c.SendStatus(http.StatusNotFound)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return serveIndex(assets, c)
|
||||
}
|
||||
|
||||
io.Copy(c.Writer, file)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ServeFrontendMiddleware() nf.HandlerFunc {
|
||||
assets := static.Frontend()
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
path := c.Path()
|
||||
|
||||
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/ushare") {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
filePath := strings.TrimPrefix(path, "/")
|
||||
if filePath == "" || filePath == "/" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
|
||||
file, err := assets.Open(filePath)
|
||||
if err != nil {
|
||||
return serveIndex(assets, c)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return serveIndex(assets, c)
|
||||
}
|
||||
|
||||
c.SetHeader("Content-Type", getContentType(filePath))
|
||||
io.Copy(c.Writer, file)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func serveIndex(assets fs.FS, c *nf.Ctx) error {
|
||||
index, err := assets.Open("index.html")
|
||||
if err != nil {
|
||||
log.Error("failed to open index.html: %v", err)
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
defer index.Close()
|
||||
|
||||
c.SetHeader("Content-Type", "text/html; charset=utf-8")
|
||||
io.Copy(c.Writer, index)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getContentType(path string) string {
|
||||
if strings.HasSuffix(path, ".html") {
|
||||
return "text/html; charset=utf-8"
|
||||
}
|
||||
if strings.HasSuffix(path, ".css") {
|
||||
return "text/css; charset=utf-8"
|
||||
}
|
||||
if strings.HasSuffix(path, ".js") {
|
||||
return "application/javascript; charset=utf-8"
|
||||
}
|
||||
if strings.HasSuffix(path, ".png") {
|
||||
return "image/png"
|
||||
}
|
||||
if strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg") {
|
||||
return "image/jpeg"
|
||||
}
|
||||
if strings.HasSuffix(path, ".svg") {
|
||||
return "image/svg+xml"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"context"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/ushare/internal/pkg/tool"
|
||||
"os"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Debug bool
|
||||
Address string
|
||||
DataPath string
|
||||
Auth string
|
||||
Username string
|
||||
Password string
|
||||
CleanInterval int
|
||||
}
|
||||
|
||||
@@ -19,8 +21,22 @@ var (
|
||||
)
|
||||
|
||||
func Init(_ context.Context) {
|
||||
if Cfg.Auth != "" {
|
||||
Cfg.Auth = tool.NewPassword(Cfg.Auth)
|
||||
log.Debug("opt.Init: encrypted password = %s", Cfg.Auth)
|
||||
if Cfg.Username == "" {
|
||||
Cfg.Username = "admin"
|
||||
}
|
||||
if Cfg.Password == "" {
|
||||
Cfg.Password = "ushare@123"
|
||||
}
|
||||
|
||||
Cfg.Password = tool.NewPassword(Cfg.Password)
|
||||
log.Debug("opt.Init: username = %s, encrypted password = %s", Cfg.Username, Cfg.Password)
|
||||
}
|
||||
|
||||
func LoadFromEnv() {
|
||||
if username := os.Getenv("USHARE_USERNAME"); username != "" {
|
||||
Cfg.Username = username
|
||||
}
|
||||
if password := os.Getenv("USHARE_PASSWORD"); password != "" {
|
||||
Cfg.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
14
internal/static/static.go
Normal file
14
internal/static/static.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package static
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
var FrontendFS embed.FS
|
||||
|
||||
func Frontend() fs.FS {
|
||||
sub, _ := fs.Sub(FrontendFS, "frontend/dist")
|
||||
return sub
|
||||
}
|
||||
Reference in New Issue
Block a user