Add hot reload for backend
All checks were successful
Build Backend (NixCN CMS) TeamCity build finished
Build Frontend (NixCN CMS) TeamCity build finished

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2026-01-01 20:22:55 +08:00
parent 83fe326962
commit 3d685b5a86
10 changed files with 46 additions and 58 deletions

View File

@@ -1,32 +1 @@
TZ=Asia/Shanghai
SERVER_APPLICATION=nixcn-cms
SERVER_ADDRESS=:8000
SERVER_EXTERNAL_URL=http://test.sne.moe:8080
SERVER_DEBUG_MODE=true
SERVER_FILE_LOGGER=false
DATABASE_TYPE=postgres
DATABASE_HOST=localhost:5432
DATABASE_NAME=postgres
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
CACHE_HOSTS=localhost:6379
CACHE_MASTER=
CACHE_USERNAME=
CACHE_PASSWORD=
CACHE_DB=0
SEARCH_HOST=localhost
SEARCH_API_KEY=
EMAIL_RESEND_API_KEY=re_BMJaPVVB_kgdf1Go7n3dWVywp6hp4WmSA
EMAIL_FROM=NixCN CMS Email Verify <nixcn@violet.sne.moe>
SECRETS_JWT_SECRET=6Wd5xkDkF4XX5q2Ckq6TY6WX
SECRETS_TURNSTILE_SECRET=0x4AAAAAACI5pgVONOZ0rzyAYsdUcoOBF8w
TTL_MAGIC_LINK_TTL=10m
TTL_ACCESS_TTL=15s
TTL_REFRESH_TTL=168h
TTL_CHECKIN_COSE_TTL=10m

View File

@@ -28,5 +28,5 @@ secrets:
ttl: ttl:
magic_link_ttl: 10m magic_link_ttl: 10m
jwt_ttl: 15s jwt_ttl: 15s
refresh_ttl: 7d refresh_ttl: 168h
checkin_code_ttl: 5m checkin_code_ttl: 10m

View File

@@ -13,9 +13,9 @@ type config struct {
type server struct { type server struct {
Application string `yaml:"application"` Application string `yaml:"application"`
Address string `yaml:"address"` Address string `yaml:"address"`
ExternalUrl string `yaml:"external_url"`
DebugMode string `yaml:"debug_mode"` DebugMode string `yaml:"debug_mode"`
FileLogger string `yaml:"file_logger"` FileLogger string `yaml:"file_logger"`
JwtSecret string `yaml:"jwt_secret"`
} }
type database struct { type database struct {

View File

@@ -8,6 +8,7 @@
packages = [ packages = [
pkgs.git pkgs.git
pkgs.just pkgs.just
pkgs.watchexec
]; ];
dotenv = { dotenv = {
@@ -32,11 +33,7 @@
exec = "bun run dev"; exec = "bun run dev";
cwd = "./client"; cwd = "./client";
}; };
backend.exec = "just backend"; backend.exec = "just dev-back";
};
tasks = {
"backend:build".exec = "just clean && just build";
}; };
services = { services = {

View File

@@ -34,5 +34,8 @@ run-back:
test-back: test-back:
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./... cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./...
dev-back: clean
watchexec -r -e go,yaml,tpl -i '.devenv/**' -i '.direnv/**' -i 'client/**' -i 'vendor/**' 'go build -o {{ join(output_dir, "nixcn-cms") }} . && cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ exec_path }}'
dev: dev:
devenv up --verbose devenv up --verbose

View File

@@ -1,4 +1,4 @@
package magiclink package authcode
import ( import (
"crypto/rand" "crypto/rand"
@@ -19,7 +19,7 @@ var (
) )
// Generate magic token // Generate magic token
func NewMagicToken(email string) (string, error) { func NewAuthCode(email string) (string, error) {
b := make([]byte, 32) b := make([]byte, 32)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
return "", err return "", err
@@ -36,7 +36,7 @@ func NewMagicToken(email string) (string, error) {
} }
// Verify magic token // Verify magic token
func VerifyMagicToken(token string) (string, bool) { func VerifyAuthCode(token string) (string, bool) {
val, ok := store.Load(token) val, ok := store.Load(token)
if !ok { if !ok {
return "", false return "", false

View File

@@ -3,6 +3,8 @@ package auth
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
func Handler(r *gin.RouterGroup) { func Handler(r *gin.RouterGroup) {
r.POST("/magic", RequestMagicLink) r.GET("/redirect", Redirect)
r.POST("/magic", Magic)
r.POST("/refresh", Refresh) r.POST("/refresh", Refresh)
r.POST("/token", Token)
} }

View File

@@ -3,8 +3,8 @@ package auth
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"nixcn-cms/internal/cryptography" "nixcn-cms/internal/cryptography"
"nixcn-cms/pkgs/authcode"
"nixcn-cms/pkgs/email" "nixcn-cms/pkgs/email"
"nixcn-cms/pkgs/magiclink"
"nixcn-cms/pkgs/turnstile" "nixcn-cms/pkgs/turnstile"
"github.com/google/uuid" "github.com/google/uuid"
@@ -15,14 +15,17 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type MagicLinkRequest struct { type MagicRequest struct {
Email string `json:"email" binding:"required,email"` ClientId string `json:"client_id"`
TurnstileToken string `json:"turnstile_token" binding:"required"` RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Email string `json:"email"`
TurnstileToken string `json:"turnstile_token"`
} }
func RequestMagicLink(c *gin.Context) { func Magic(c *gin.Context) {
// Parse request // Parse request
var req MagicLinkRequest var req MagicRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"}) c.JSON(400, gin.H{"error": "invalid request"})
return return
@@ -35,18 +38,20 @@ func RequestMagicLink(c *gin.Context) {
return return
} }
// Generate magic token code, err := authcode.NewAuthCode(req.Email)
token, err := magiclink.NewMagicToken(req.Email)
if err != nil { if err != nil {
c.JSON(500, gin.H{"error": "internal error"}) c.JSON(500, gin.H{"status": "code gen failed"})
return
} }
link := viper.GetString("server.external_url") + "/login?ticket=" + token uri := viper.GetString("server.external_url") +
"/api/v1/auth/redirect?" +
"code=" + code +
"&redirect_uri" + req.RedirectUri +
"&state" + req.State
debugMode := viper.GetString("server.debug_mode") debugMode := viper.GetString("server.debug_mode")
if debugMode == "true" { if debugMode == "true" {
log.Info("Magic link for " + req.Email + " : " + link) log.Info("Magic link for " + req.Email + " : " + uri)
} else { } else {
// Send email using resend // Send email using resend
resend, err := email.NewResendClient() resend, err := email.NewResendClient()
@@ -58,7 +63,7 @@ func RequestMagicLink(c *gin.Context) {
resend.Send( resend.Send(
req.Email, req.Email,
"NixCN CMS Email Verify", "NixCN CMS Email Verify",
"<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+link+">"+link+"</a>", "<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+uri+">"+uri+"</a>",
) )
} }
@@ -74,7 +79,7 @@ func VerifyMagicLink(c *gin.Context) {
} }
// Verify email token // Verify email token
email, ok := magiclink.VerifyMagicToken(magicToken) email, ok := authcode.VerifyAuthCode(magicToken)
if !ok { if !ok {
c.JSON(401, gin.H{"error": "invalid or expired token"}) c.JSON(401, gin.H{"error": "invalid or expired token"})
return return

View File

@@ -1 +1,7 @@
package auth package auth
import "github.com/gin-gonic/gin"
func Redirect(c *gin.Context) {
}

View File

@@ -1 +1,7 @@
package auth package auth
import "github.com/gin-gonic/gin"
func Token(c *gin.Context) {
}