From afe3138fb1da60c20c4359de28f5585fbe7dd9ac Mon Sep 17 00:00:00 2001 From: LinXin Date: Thu, 20 Nov 2025 19:38:50 +0800 Subject: [PATCH 1/3] fix: Revise the duplicate codes from #04e140 --- backend/models/User.go | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 backend/models/User.go diff --git a/backend/models/User.go b/backend/models/User.go deleted file mode 100644 index 9a525d8..0000000 --- a/backend/models/User.go +++ /dev/null @@ -1,30 +0,0 @@ -package models - -import ( - "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" -) - -type User struct { - gorm.Model - Username string `gorm:"uniqueIndex;size:255;not null"` - Email string `gorm:"uniqueIndex;size:255;not null"` - Password string `gorm:"size:255;not null"` -} - -// SetPassword 加密密码 -func (user *User) SetPassword(password string) error { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return err - } - user.Password = string(hashedPassword) - return nil -} - -// checkPassword 检查密码 -func (user *User) CheckPassword(password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) - //如果err = nil 说明密码匹配 - return err == nil -} From aa931571b18b37e03d8c7a684a812ea1761d75dd Mon Sep 17 00:00:00 2001 From: LinXin Date: Thu, 20 Nov 2025 23:25:48 +0800 Subject: [PATCH 2/3] fix: Revise the duplicate codes from #04e140 --- backend/config.env | 2 +- backend/database/database.go | 3 +- backend/database/modernc_sqlite.go | 5 - backend/dto/LoginInput.go | 6 - backend/dto/RegisterInput.go | 7 - backend/go.mod | 21 ++- backend/go.sum | 43 +++-- backend/handlers/user/profile.go | 1 + backend/main.go | 47 ++--- backend/repository/UserRepository.go | 44 ----- backend/router/Router.go | 35 ---- backend/router/UserHandler.go | 54 ------ backend/services/UserService.go | 80 -------- frontend/.env.development | 1 + frontend/src/App.vue | 9 - frontend/src/components/ui/Button.vue | 15 +- frontend/src/components/ui/Input.vue | 33 ++++ frontend/src/lib/api.ts | 14 +- frontend/src/router/index.ts | 6 + frontend/src/stores/user.ts | 58 +++--- frontend/src/views/user/LoginView.vue | 169 ++++++++--------- frontend/src/views/user/RegisterView.vue | 231 ++++++++++++++--------- 22 files changed, 386 insertions(+), 498 deletions(-) delete mode 100644 backend/database/modernc_sqlite.go delete mode 100644 backend/dto/LoginInput.go delete mode 100644 backend/dto/RegisterInput.go delete mode 100644 backend/repository/UserRepository.go delete mode 100644 backend/router/Router.go delete mode 100644 backend/router/UserHandler.go delete mode 100644 backend/services/UserService.go create mode 100644 frontend/.env.development create mode 100644 frontend/src/components/ui/Input.vue diff --git a/backend/config.env b/backend/config.env index e5ac5f2..3230629 100644 --- a/backend/config.env +++ b/backend/config.env @@ -1,5 +1,5 @@ # 数据库配置,测试时不想装mysql的话可选sqlite -DB_TYPE=mysql +DB_TYPE=sqlite DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=progress_wall diff --git a/backend/database/database.go b/backend/database/database.go index a7bf2db..9738022 100644 --- a/backend/database/database.go +++ b/backend/database/database.go @@ -3,10 +3,11 @@ package database import ( "fmt" "log" + "progress-wall-backend/config" + sqlite "github.com/glebarez/sqlite" "gorm.io/driver/mysql" - "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) diff --git a/backend/database/modernc_sqlite.go b/backend/database/modernc_sqlite.go deleted file mode 100644 index f9f8d96..0000000 --- a/backend/database/modernc_sqlite.go +++ /dev/null @@ -1,5 +0,0 @@ -package database - -// Import the pure-Go modernc sqlite driver to avoid cgo dependency (replaces mattn/go-sqlite3). -// The driver registers itself with database/sql on import. -import _ "modernc.org/sqlite" diff --git a/backend/dto/LoginInput.go b/backend/dto/LoginInput.go deleted file mode 100644 index 6caa23b..0000000 --- a/backend/dto/LoginInput.go +++ /dev/null @@ -1,6 +0,0 @@ -package dto - -type LoginInput struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` -} diff --git a/backend/dto/RegisterInput.go b/backend/dto/RegisterInput.go deleted file mode 100644 index 298e01a..0000000 --- a/backend/dto/RegisterInput.go +++ /dev/null @@ -1,7 +0,0 @@ -package dto - -type RegisterInput struct { - Username string `json:"username" binding:"required,min=3,max=20"` - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6,max=50"` -} diff --git a/backend/go.mod b/backend/go.mod index b4a7880..b4c2232 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,44 +5,47 @@ go 1.24.0 toolchain go1.24.9 require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/glebarez/sqlite v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/joho/godotenv v1.4.0 + golang.org/x/crypto v0.43.0 gorm.io/driver/mysql v1.5.2 - gorm.io/driver/sqlite v1.5.4 - gorm.io/gorm v1.25.5 + gorm.io/gorm v1.25.7 ) require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.43.0 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.45.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -50,4 +53,8 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index c2b5d08..d535bfe 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -5,9 +5,10 @@ github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -16,6 +17,12 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -24,15 +31,19 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -47,9 +58,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -57,11 +65,15 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -69,6 +81,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -96,11 +110,18 @@ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7I google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= -gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index 9650f7f..35c5bb8 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -2,6 +2,7 @@ package user import ( "net/http" + "progress-wall-backend/services" "github.com/gin-gonic/gin" diff --git a/backend/main.go b/backend/main.go index cda7712..99d0f2b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,49 +1,26 @@ package main import ( + "fmt" "log" + "progress-wall-backend/config" "progress-wall-backend/database" "progress-wall-backend/routes" ) func main() { - + // 加载配置 cfg := config.Load() - // 打印数据库配置,检查用户名/密码是否正确 - fmt.Printf("DB_USER=%s, DB_PASSWORD=%s, DB_HOST=%s\n", cfg.DB.User, cfg.DB.Password, cfg.DB.Host) - - // 下面是你原来的 InitDB 调用 - if err := database.InitDB(cfg); err != nil { - log.Fatalf("Failed to initialize database: %v", err) - } - - // 初始化数据库 - if err := database.InitDB(cfg); err != nil { - log.Fatalf("Failed to initialize database: %v", err) - } - - // 获取数据库实例 - db := database.GetDB() - - // 自动迁移数据库表 - if err := db.AutoMigrate(&models.User{}); err != nil { - log.Fatalf("Failed to migrate database: %v", err) - } - - // 设置Gin模式 - gin.SetMode(cfg.Server.Mode) - - // 初始化依赖注入层 - userRepo := repository.NewUserRepository(db) - userService := services.NewUserService(userRepo, cfg) - - // 初始化路由 - deps := router.HandlerDependencies{ - UserService: userService, - } - r := router.NewRouter(deps) + // 打印配置信息 + fmt.Printf("服务器配置:\n") + fmt.Printf("- 端口: %s\n", cfg.Server.Port) + fmt.Printf("- 模式: %s\n", cfg.Server.Mode) + fmt.Printf("- 数据库类型: %s\n", cfg.DB.Type) + fmt.Printf("- 数据库名称: %s\n", cfg.DB.Name) + fmt.Printf("- JWT密钥长度: %d\n", len(cfg.JWT.Secret)) + fmt.Printf("- CORS允许来源: %s\n", cfg.CORS.AllowOrigins) // 初始化数据库 db, err := database.InitDB(cfg) @@ -61,7 +38,7 @@ func main() { log.Fatal("初始化基础数据失败:", err) } - log.Println("数据库初始化完成") + log.Println("数据 初始化完成") // 设置路由 r := routes.SetupRoutes(db, cfg) diff --git a/backend/repository/UserRepository.go b/backend/repository/UserRepository.go deleted file mode 100644 index 1ce835a..0000000 --- a/backend/repository/UserRepository.go +++ /dev/null @@ -1,44 +0,0 @@ -package repository - -import ( - "progress-wall-backend/models" - - "gorm.io/gorm" -) - -type UserRepository interface { - Save(user *models.User) error - FindByUsername(username string) (*models.User, error) - FindByEmail(email string) (*models.User, error) -} - -type userRepository struct { - db *gorm.DB -} - -func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{db: db} -} - -// 保存用户 -func (r *userRepository) Save(user *models.User) (error) { - return r.db.Create(user).Error -} - -// 根据用户名查找用户 -func (r *userRepository) FindByUsername(username string) (*models.User, error) { - var user models.User - if err := r.db.Where("username = ?", username).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -// 根据邮箱查找用户 -func (r *userRepository) FindByEmail(email string) (*models.User, error) { - var user models.User - if err := r.db.Where("email = ?", email).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} diff --git a/backend/router/Router.go b/backend/router/Router.go deleted file mode 100644 index c070461..0000000 --- a/backend/router/Router.go +++ /dev/null @@ -1,35 +0,0 @@ -package router - -import ( - "progress-wall-backend/services" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -type HandlerDependencies struct { - UserService services.UserService -} - -func NewRouter(deps HandlerDependencies) *gin.Engine { - router := gin.Default() - - // CORS 配置 - router.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - ExposeHeaders: []string{"Content-Length"}, - AllowCredentials: true, - })) - - // 用户相关路由组 - userHandler := NewUserHandler(deps.UserService) - userGroup := router.Group("/api/auth") - { - userGroup.POST("/register", userHandler.Register) // 注册 - userGroup.POST("/login", userHandler.Login) // 登录 - } - - return router -} diff --git a/backend/router/UserHandler.go b/backend/router/UserHandler.go deleted file mode 100644 index 62816dc..0000000 --- a/backend/router/UserHandler.go +++ /dev/null @@ -1,54 +0,0 @@ -package router - -import ( - "net/http" - "progress-wall-backend/dto" - "progress-wall-backend/services" - - "github.com/gin-gonic/gin" -) - -type UserHandler struct { - userService services.UserService -} - -func NewUserHandler(userService services.UserService) *UserHandler { - return &UserHandler{userService: userService} -} - -// Register 用户注册 -func (h *UserHandler) Register(c *gin.Context) { - var input dto.RegisterInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - user, err := h.userService.Register(input.Username, input.Email, input.Password) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"user": user}) -} - -// Login 用户登录 -func (h *UserHandler) Login(c *gin.Context) { - var input dto.LoginInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - token, user, err := h.userService.Login(input.Email, input.Password) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "token": token, - "user": user, - }) - -} diff --git a/backend/services/UserService.go b/backend/services/UserService.go deleted file mode 100644 index c7e35bc..0000000 --- a/backend/services/UserService.go +++ /dev/null @@ -1,80 +0,0 @@ -package services - -import ( - "errors" - "progress-wall-backend/config" - "progress-wall-backend/models" - "progress-wall-backend/repository" - "progress-wall-backend/utils" -) - -type UserService interface { - Register(username, email, password string) (*models.User, error) - Login(email, password string) (string, *models.User, error) -} - -type userService struct { - userRepo repository.UserRepository - cfg *config.Config -} - -func NewUserService(userRepo repository.UserRepository, cfg *config.Config) UserService { - return &userService{ - userRepo: userRepo, - cfg: cfg, - } -} - -// Register 注册新用户 -func (s *userService) Register(username, email, password string) (*models.User, error) { - // 检查用户名或邮箱是否已存在 - if user, err := s.userRepo.FindByUsername(username); err == nil { - return user, errors.New("username already exists") - } - if user, err := s.userRepo.FindByEmail(email); err == nil { - return user, errors.New("email already exists") - } - - // 创建用户实例 - user := &models.User{ - Username: username, - Email: email, - } - - // 加密密码 - if err := user.SetPassword(password); err != nil { - return nil, err - } - - // 保存用户 - if err := s.userRepo.Save(user); err != nil { - return nil, err - } - - return user, nil -} - -// Login 用户登录 -func (s *userService) Login(email, password string) (string, *models.User, error) { - // 查找用户 - user, err := s.userRepo.FindByEmail(email) - if err != nil { - return "", nil, errors.New("user not found") - } - - // 检查密码 - if !user.CheckPassword(password) { - return "", nil, errors.New("invalid password") - } - - // 生成 JWT token - token, err := utils.GenerateToken(user.ID, user.Username, s.cfg) - if err != nil { - return "", nil, errors.New("failed to generate token") - } - - // 不返回密码 - user.Password = "" - - return token, user, nil -} diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..dcb77f9 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8080/api \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 12b6694..2385f78 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,14 +8,5 @@ diff --git a/frontend/src/components/ui/Button.vue b/frontend/src/components/ui/Button.vue index 09cfa8d..b0defae 100644 --- a/frontend/src/components/ui/Button.vue +++ b/frontend/src/components/ui/Button.vue @@ -4,13 +4,15 @@ buttonVariants({ variant, size }), typeof $attrs.class === 'string' ? $attrs.class : undefined )" - v-bind="$attrs" + :type="type" + v-bind="buttonAttrs" > diff --git a/frontend/src/components/ui/Input.vue b/frontend/src/components/ui/Input.vue new file mode 100644 index 0000000..62b9aa5 --- /dev/null +++ b/frontend/src/components/ui/Input.vue @@ -0,0 +1,33 @@ + + + + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index aab90a7..b4290b1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,9 +1,16 @@ import axios from 'axios' import { useUserStore } from '@/stores/user' +import router from '@/router' +// 后端 API 基础地址配置 +// 优先使用环境变量 VITE_API_BASE_URL +// 默认使用 http://localhost:8080/api(本地开发) const api = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || '/api' + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api', + headers: { + 'Content-Type': 'application/json' + } }) api.interceptors.request.use( @@ -24,7 +31,10 @@ api.interceptors.response.use( if (error.response?.status === 401) { const userStore = useUserStore() userStore.logout() - window.location.href = '/login' + // 使用 Vue Router 导航,避免页面刷新 + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } } return Promise.reject(error) } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 68cb942..707b662 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -19,6 +19,12 @@ const routes: RouteRecordRaw[] = [ name: 'Register', component: () => import('@/views/user/RegisterView.vue') }, + { + path: '/boards', + name: 'Boards', + component: () => import('@/views/dashboard/BoardListView.vue'), + meta: { requiresAuth: true } + }, { path: '/kanban', name: 'Kanban', diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index 25e3d04..e6aa4e1 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -26,19 +26,21 @@ export const useUserStore = defineStore('user', () => { function getToken() { return token.value } - async function login(email: string, password: string) { - try { - const res = await api.post('/auth/login', { email, password }) - if (res.data?.token) { // 改成 token - setToken(res.data.token) - currentUser.value = res.data.user - return true + async function login(username: string, password: string) { + try { + const res = await api.post('/auth/login', { username, password }) + if (res.data?.accessToken) { + setToken(res.data.accessToken) + currentUser.value = res.data.user + isLoggedIn.value = true + return true + } + return false + } catch (err: any) { + const errorMsg = err?.response?.data?.error || err?.message || '登录失败' + throw errorMsg } - return false - } catch (err: any) { - throw err?.response?.data?.error || '登录失败' } -} async function register(username: string, email: string, password: string, nickname: string) { try { @@ -54,20 +56,28 @@ export const useUserStore = defineStore('user', () => { // 页面刷新时自动恢复登录状态 - if (token.value && !currentUser.value) { - api.get('/user/profile').then(res => { - currentUser.value = res.data - isLoggedIn.value = true - }).catch(() => { - logout() - }) -} + async function restoreUser() { + if (token.value && !currentUser.value) { + try { + const res = await api.get('/user/profile') + if (res.data?.user) { + currentUser.value = res.data.user + isLoggedIn.value = true + } + } catch { + logout() + } + } + } -function logout() { - setToken(null) - currentUser.value = null -} + function logout() { + setToken(null) + currentUser.value = null + isLoggedIn.value = false + } + // 初始化时恢复用户状态 + restoreUser() - return { token, currentUser, isLoggedIn, setToken, getToken, login, register, logout } + return { token, currentUser, isLoggedIn, setToken, getToken, login, register, logout, restoreUser } }) diff --git a/frontend/src/views/user/LoginView.vue b/frontend/src/views/user/LoginView.vue index c1db907..2c87e16 100644 --- a/frontend/src/views/user/LoginView.vue +++ b/frontend/src/views/user/LoginView.vue @@ -1,100 +1,101 @@ - - diff --git a/frontend/src/views/user/RegisterView.vue b/frontend/src/views/user/RegisterView.vue index 06dc98e..37cc7e3 100644 --- a/frontend/src/views/user/RegisterView.vue +++ b/frontend/src/views/user/RegisterView.vue @@ -1,111 +1,160 @@ - - From 0e78c4a22cc26b55b96ab2297734d7e01215edef Mon Sep 17 00:00:00 2001 From: LinXin Date: Thu, 20 Nov 2025 23:42:21 +0800 Subject: [PATCH 3/3] fix: set some security suggestions from ds --- backend/main.go | 27 +++-- backend/services/auth_service.go | 17 +++- backend/utils/password.go | 58 ++++++++++- frontend/src/utils/validation.ts | 120 +++++++++++++++++++++++ frontend/src/views/user/LoginView.vue | 22 ++++- frontend/src/views/user/RegisterView.vue | 111 ++++++++++++++++++--- 6 files changed, 320 insertions(+), 35 deletions(-) create mode 100644 frontend/src/utils/validation.ts diff --git a/backend/main.go b/backend/main.go index 99d0f2b..e70549d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -13,40 +13,35 @@ func main() { // 加载配置 cfg := config.Load() - // 打印配置信息 - fmt.Printf("服务器配置:\n") - fmt.Printf("- 端口: %s\n", cfg.Server.Port) - fmt.Printf("- 模式: %s\n", cfg.Server.Mode) - fmt.Printf("- 数据库类型: %s\n", cfg.DB.Type) - fmt.Printf("- 数据库名称: %s\n", cfg.DB.Name) - fmt.Printf("- JWT密钥长度: %d\n", len(cfg.JWT.Secret)) - fmt.Printf("- CORS允许来源: %s\n", cfg.CORS.AllowOrigins) - - // 初始化数据库 + // 打印配置信息(不打印敏感信息) + log.Printf("服务器配置: 端口=%s, 模式=%s, 数据库类型=%s", + cfg.Server.Port, cfg.Server.Mode, cfg.DB.Type) + + // 初始化数据库连接 db, err := database.InitDB(cfg) if err != nil { - log.Fatal("数据库初始化失败:", err) + log.Fatalf("数据库初始化失败: %v", err) } // 执行数据库迁移 if err := database.Migrate(db); err != nil { - log.Fatal("数据库迁移失败:", err) + log.Fatalf("数据库迁移失败: %v", err) } // 初始化基础数据 if err := database.Seed(db); err != nil { - log.Fatal("初始化基础数据失败:", err) + log.Fatalf("初始化基础数据失败: %v", err) } - log.Println("数据 初始化完成") + log.Println("数据库初始化完成") // 设置路由 r := routes.SetupRoutes(db, cfg) // 启动HTTP服务器 addr := fmt.Sprintf(":%s", cfg.Server.Port) - log.Printf("服务器启动在端口 %s\n", cfg.Server.Port) + log.Printf("服务器启动在端口 %s", cfg.Server.Port) if err := r.Run(addr); err != nil { - log.Fatal("服务器启动失败:", err) + log.Fatalf("服务器启动失败: %v", err) } } diff --git a/backend/services/auth_service.go b/backend/services/auth_service.go index 52c666f..a2e9bd6 100644 --- a/backend/services/auth_service.go +++ b/backend/services/auth_service.go @@ -2,10 +2,11 @@ package services import ( "errors" + "time" + "progress-wall-backend/config" "progress-wall-backend/models" "progress-wall-backend/utils" - "time" "gorm.io/gorm" ) @@ -116,11 +117,21 @@ func (s *AuthService) Register(req RegisterRequest) (*RegisterResult, error) { return nil, ErrUserExists } - // 验证密码长度(至少6位) - if len(req.Password) < 6 { + // 验证密码强度 + if err := utils.ValidatePasswordStrength(req.Password); err != nil { return nil, ErrInvalidPassword } + // 验证邮箱格式 + if !utils.ValidateEmail(req.Email) { + return nil, errors.New("邮箱格式不正确") + } + + // 验证用户名格式(3-20个字符,只能包含字母、数字、下划线) + if len(req.Username) < 3 || len(req.Username) > 20 { + return nil, errors.New("用户名长度必须在3-20个字符之间") + } + // 加密密码 hashedPassword, err := utils.HashPassword(req.Password) if err != nil { diff --git a/backend/utils/password.go b/backend/utils/password.go index 8fb7cd1..3567da8 100644 --- a/backend/utils/password.go +++ b/backend/utils/password.go @@ -1,10 +1,64 @@ package utils -import "golang.org/x/crypto/bcrypt" +import ( + "errors" + "regexp" + "unicode" + + "golang.org/x/crypto/bcrypt" +) + +const ( + // BcryptCost bcrypt加密成本,提高安全性(默认10,提高到12) + BcryptCost = 12 + // MinPasswordLength 最小密码长度 + MinPasswordLength = 6 + // MaxPasswordLength 最大密码长度 + MaxPasswordLength = 128 +) + +// ValidatePasswordStrength 验证密码强度 +// 要求:至少6位,包含字母和数字,最大128位 +func ValidatePasswordStrength(password string) error { + if len(password) < MinPasswordLength { + return errors.New("密码长度至少6位") + } + if len(password) > MaxPasswordLength { + return errors.New("密码长度不能超过128位") + } + + hasLetter := false + hasDigit := false + + for _, char := range password { + if unicode.IsLetter(char) { + hasLetter = true + } + if unicode.IsDigit(char) { + hasDigit = true + } + } + + if !hasLetter { + return errors.New("密码必须包含至少一个字母") + } + if !hasDigit { + return errors.New("密码必须包含至少一个数字") + } + + return nil +} + +// ValidateEmail 验证邮箱格式 +func ValidateEmail(email string) bool { + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) + return emailRegex.MatchString(email) +} // HashPassword 对密码进行bcrypt哈希加密 +// 使用更高的成本值提高安全性 func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) if err != nil { return "", err } diff --git a/frontend/src/utils/validation.ts b/frontend/src/utils/validation.ts new file mode 100644 index 0000000..8f10ee3 --- /dev/null +++ b/frontend/src/utils/validation.ts @@ -0,0 +1,120 @@ +/** + * 表单验证工具函数 + * 提供常用的输入验证规则,防止XSS攻击和无效输入 + */ + +/** + * 转义HTML特殊字符,防止XSS攻击 + */ +export function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + return text.replace(/[&<>"']/g, (m) => map[m]) +} + +/** + * 验证用户名格式 + * 要求:3-20个字符,只能包含字母、数字、下划线 + */ +export function validateUsername(username: string): { valid: boolean; message?: string } { + if (!username || username.trim().length === 0) { + return { valid: false, message: '用户名不能为空' } + } + + const trimmed = username.trim() + if (trimmed.length < 3) { + return { valid: false, message: '用户名长度至少3个字符' } + } + if (trimmed.length > 20) { + return { valid: false, message: '用户名长度不能超过20个字符' } + } + + const usernameRegex = /^[a-zA-Z0-9_]+$/ + if (!usernameRegex.test(trimmed)) { + return { valid: false, message: '用户名只能包含字母、数字和下划线' } + } + + return { valid: true } +} + +/** + * 验证邮箱格式 + */ +export function validateEmail(email: string): { valid: boolean; message?: string } { + if (!email || email.trim().length === 0) { + return { valid: false, message: '邮箱不能为空' } + } + + const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/ + if (!emailRegex.test(email.trim())) { + return { valid: false, message: '请输入有效的邮箱地址' } + } + + return { valid: true } +} + +/** + * 验证密码强度 + * 要求:至少6位,包含字母和数字,最大128位 + */ +export function validatePassword(password: string): { valid: boolean; message?: string } { + if (!password || password.length === 0) { + return { valid: false, message: '密码不能为空' } + } + + if (password.length < 6) { + return { valid: false, message: '密码长度至少6位' } + } + + if (password.length > 128) { + return { valid: false, message: '密码长度不能超过128位' } + } + + const hasLetter = /[a-zA-Z]/.test(password) + const hasDigit = /[0-9]/.test(password) + + if (!hasLetter) { + return { valid: false, message: '密码必须包含至少一个字母' } + } + + if (!hasDigit) { + return { valid: false, message: '密码必须包含至少一个数字' } + } + + // 检查常见弱密码 + const weakPasswords = ['123456', 'password', '12345678', 'qwerty', 'abc123'] + if (weakPasswords.includes(password.toLowerCase())) { + return { valid: false, message: '密码过于简单,请使用更复杂的密码' } + } + + return { valid: true } +} + +/** + * 验证昵称格式 + * 要求:可选,如果填写则不超过50个字符 + */ +export function validateNickname(nickname: string): { valid: boolean; message?: string } { + if (!nickname || nickname.trim().length === 0) { + return { valid: true } // 昵称是可选的 + } + + if (nickname.length > 50) { + return { valid: false, message: '昵称长度不能超过50个字符' } + } + + return { valid: true } +} + +/** + * 清理输入:去除首尾空格,转义HTML + */ +export function sanitizeInput(input: string): string { + return escapeHtml(input.trim()) +} + diff --git a/frontend/src/views/user/LoginView.vue b/frontend/src/views/user/LoginView.vue index 2c87e16..79f4d6f 100644 --- a/frontend/src/views/user/LoginView.vue +++ b/frontend/src/views/user/LoginView.vue @@ -70,6 +70,7 @@ import Card from '@/components/ui/Card.vue' import CardHeader from '@/components/ui/CardHeader.vue' import CardContent from '@/components/ui/CardContent.vue' import Input from '@/components/ui/Input.vue' +import { sanitizeInput } from '@/utils/validation' const username = ref('') const password = ref('') @@ -82,18 +83,35 @@ const userStore = useUserStore() const handleLogin = async () => { if (loading.value) return + // 基本验证 + if (!username.value || !username.value.trim()) { + errorMessage.value = '请输入用户名或邮箱' + return + } + + if (!password.value) { + errorMessage.value = '请输入密码' + return + } + errorMessage.value = '' loading.value = true try { - const success = await userStore.login(username.value, password.value) + // 清理输入数据,防止XSS + const sanitizedUsername = sanitizeInput(username.value) + + const success = await userStore.login(sanitizedUsername, password.value) if (success) { router.push('/') } else { errorMessage.value = '登录失败,请检查用户名和密码' } } catch (err: any) { - errorMessage.value = typeof err === 'string' ? err : '登录过程中出现错误' + const errorMsg = typeof err === 'string' + ? err + : err?.response?.data?.error || '登录过程中出现错误' + errorMessage.value = errorMsg } finally { loading.value = false } diff --git a/frontend/src/views/user/RegisterView.vue b/frontend/src/views/user/RegisterView.vue index 37cc7e3..a72723b 100644 --- a/frontend/src/views/user/RegisterView.vue +++ b/frontend/src/views/user/RegisterView.vue @@ -19,10 +19,12 @@ type="text" required autocomplete="username" - placeholder="请输入用户名" + placeholder="请输入用户名(3-20个字符)" minlength="3" maxlength="20" + @blur="validateUsernameField" /> +

{{ usernameError }}

@@ -36,7 +38,9 @@ required autocomplete="email" placeholder="请输入邮箱地址" + @blur="validateEmailField" /> +

{{ emailError }}

@@ -48,8 +52,11 @@ v-model="nickname" type="text" autocomplete="nickname" - placeholder="请输入昵称(可选)" + placeholder="请输入昵称(可选,最多50个字符)" + maxlength="50" + @blur="validateNicknameField" /> +

{{ nicknameError }}

@@ -62,9 +69,12 @@ type="password" required autocomplete="new-password" - placeholder="请输入密码(至少6位)" + placeholder="请输入密码(至少6位,包含字母和数字)" minlength="6" + maxlength="128" + @blur="validatePasswordField" /> +

{{ passwordError }}

@@ -79,7 +89,9 @@ autocomplete="new-password" placeholder="请再次输入密码" minlength="6" + @blur="validateConfirmPasswordField" /> +

{{ confirmPasswordError }}

@@ -115,6 +127,13 @@ import Card from '@/components/ui/Card.vue' import CardHeader from '@/components/ui/CardHeader.vue' import CardContent from '@/components/ui/CardContent.vue' import Input from '@/components/ui/Input.vue' +import { + validateUsername, + validateEmail, + validatePassword, + validateNickname, + sanitizeInput +} from '@/utils/validation' const username = ref('') const email = ref('') @@ -124,27 +143,94 @@ const nickname = ref('') const errorMessage = ref('') const loading = ref(false) +// 字段级错误提示 +const usernameError = ref('') +const emailError = ref('') +const passwordError = ref('') +const confirmPasswordError = ref('') +const nicknameError = ref('') + const router = useRouter() const userStore = useUserStore() +// 实时验证用户名 +const validateUsernameField = () => { + const result = validateUsername(username.value) + usernameError.value = result.valid ? '' : (result.message || '') + return result.valid +} + +// 实时验证邮箱 +const validateEmailField = () => { + const result = validateEmail(email.value) + emailError.value = result.valid ? '' : (result.message || '') + return result.valid +} + +// 实时验证密码 +const validatePasswordField = () => { + const result = validatePassword(password.value) + passwordError.value = result.valid ? '' : (result.message || '') + return result.valid +} + +// 实时验证确认密码 +const validateConfirmPasswordField = () => { + if (!confirmPassword.value) { + confirmPasswordError.value = '' + return true + } + if (password.value !== confirmPassword.value) { + confirmPasswordError.value = '两次输入的密码不一致' + return false + } + confirmPasswordError.value = '' + return true +} + +// 实时验证昵称 +const validateNicknameField = () => { + const result = validateNickname(nickname.value) + nicknameError.value = result.valid ? '' : (result.message || '') + return result.valid +} + const handleRegister = async () => { if (loading.value) return - // 验证密码匹配 - if (password.value !== confirmPassword.value) { - errorMessage.value = '两次输入的密码不一致' + // 清除所有错误 + errorMessage.value = '' + usernameError.value = '' + emailError.value = '' + passwordError.value = '' + confirmPasswordError.value = '' + nicknameError.value = '' + + // 验证所有字段 + const isUsernameValid = validateUsernameField() + const isEmailValid = validateEmailField() + const isPasswordValid = validatePasswordField() + const isConfirmPasswordValid = validateConfirmPasswordField() + const isNicknameValid = validateNicknameField() + + if (!isUsernameValid || !isEmailValid || !isPasswordValid || !isConfirmPasswordValid || !isNicknameValid) { + errorMessage.value = '请检查并修正表单中的错误' return } - errorMessage.value = '' loading.value = true try { + // 清理输入数据,防止XSS + const sanitizedUsername = sanitizeInput(username.value) + const sanitizedEmail = sanitizeInput(email.value) + const sanitizedNickname = nickname.value ? sanitizeInput(nickname.value) : '' + const user = await userStore.register( - username.value, - email.value, - password.value, - nickname.value || '' + sanitizedUsername, + sanitizedEmail, + password.value, // 密码不需要HTML转义,但需要确保不包含特殊字符 + sanitizedNickname ) if (user) { router.push('/login') @@ -152,7 +238,8 @@ const handleRegister = async () => { errorMessage.value = '注册失败,请稍后重试' } } catch (err: any) { - errorMessage.value = typeof err === 'string' ? err : '注册过程中出现错误' + const errorMsg = typeof err === 'string' ? err : err?.response?.data?.error || '注册过程中出现错误' + errorMessage.value = errorMsg } finally { loading.value = false }