From b0b32c93d1bf68e15ab12cbce13666dc6c1c26b5 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Tue, 23 Sep 2025 00:33:34 +0800 Subject: [PATCH] feat: add email verification and password reset functionality - Introduced environment variables for database and email configurations. - Implemented email verification code generation and validation. - Added password reset feature with email verification. - Updated user registration and profile management APIs. - Refactored user security settings to include email and password updates. - Enhanced console layout with internationalization support. - Removed deprecated settings page and integrated global settings. - Added new reset password page and form components. - Updated localization files for new features and translations. --- go.mod | 68 +++--- go.sum | 73 ++++++ internal/controller/v1/file.go | 212 +++++++++--------- internal/controller/v1/user.go | 60 ++++- internal/ctxutils/user.go | 37 ++- internal/dto/user.go | 96 ++++---- internal/middleware/email_verify.go | 30 +++ internal/repo/init.go | 17 +- internal/router/apiv1/user.go | 12 +- internal/service/user.go | 83 +++++-- pkg/constant/constant.go | 20 ++ pkg/utils/email.go | 21 +- pkg/utils/email_verify.go | 26 +++ pkg/utils/password.go | 1 + web/src/api/user.ts | 22 +- web/src/app/console/global/page.tsx | 5 + web/src/app/console/layout.tsx | 8 +- web/src/app/console/setting/page.tsx | 5 - web/src/app/reset-password/page.tsx | 25 +++ web/src/components/common/input-otp.tsx | 27 +++ web/src/components/console/data.ts | 20 +- .../console/{setting => global}/colors.ts | 0 .../console/{setting => global}/index.tsx | 2 +- web/src/components/console/nav-main.tsx | 4 +- web/src/components/console/nav-ucenter.tsx | 5 +- .../components/console/user-profile/index.tsx | 34 ++- .../console/user-security/index.tsx | 101 +++++---- web/src/components/login/login-form.tsx | 18 +- .../reset-password/reset-password-form.tsx | 103 +++++++++ web/src/hooks/use-route.ts | 13 +- web/src/locales/zh-CN.json | 77 ++++++- web/src/models/resp.ts | 8 + 32 files changed, 888 insertions(+), 345 deletions(-) create mode 100644 web/src/app/console/global/page.tsx delete mode 100644 web/src/app/console/setting/page.tsx create mode 100644 web/src/app/reset-password/page.tsx create mode 100644 web/src/components/common/input-otp.tsx rename web/src/components/console/{setting => global}/colors.ts (100%) rename web/src/components/console/{setting => global}/index.tsx (90%) create mode 100644 web/src/components/reset-password/reset-password-form.tsx diff --git a/go.mod b/go.mod index 3bd8572..51fbefd 100644 --- a/go.mod +++ b/go.mod @@ -1,56 +1,60 @@ module github.com/snowykami/neo-blog -go 1.23.3 +go 1.24.0 + +toolchain go1.24.1 require ( - github.com/cloudwego/hertz v0.10.1 + github.com/cloudwego/hertz v0.10.2 github.com/glebarez/sqlite v1.11.0 - github.com/golang-jwt/jwt/v5 v5.2.3 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/joho/godotenv v1.5.1 github.com/sirupsen/logrus v1.9.3 github.com/studio-b12/gowebdav v0.11.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.42.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/postgres v1.6.0 - gorm.io/gorm v1.30.0 + gorm.io/gorm v1.31.0 resty.dev/v3 v3.0.0-beta.3 ) require ( - github.com/bytedance/gopkg v0.1.1 // indirect - github.com/bytedance/sonic v1.13.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/gopkg v0.1.4 // indirect - github.com/cloudwego/netpoll v0.7.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/gopkg v0.1.6 // indirect + github.com/cloudwego/netpoll v0.7.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/golang/protobuf v1.5.0 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nyaruka/phonenumbers v1.6.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/tidwall/gjson v1.14.4 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/arch v0.21.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // 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 + modernc.org/libc v1.66.9 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.0 // indirect ) diff --git a/go.sum b/go.sum index 355b0f5..ea5bc96 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,33 @@ github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= +github.com/cloudwego/gopkg v0.1.6 h1:EMlOHg975CxKX1/BtIVYKGW8hxNptTkjjJ7bvfXu4L4= +github.com/cloudwego/gopkg v0.1.6/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= github.com/cloudwego/hertz v0.10.1 h1:gTM2JIGO7vmRoaDz71GctyoUE19pXGuznFX55HjGs1g= github.com/cloudwego/hertz v0.10.1/go.mod h1:0sofikwk5YcHCerClgCzcaoamY61JiRwR5G0mAUo+Y0= +github.com/cloudwego/hertz v0.10.2 h1:scaVn4E/AQ/vuMAC8FXzUzsEXS/TF1ix1I+4slPhh7c= +github.com/cloudwego/hertz v0.10.2/go.mod h1:W5dUFXZPZkyfjMMo3EQrMQbofuvTsctM9IxmhbkuT18= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= +github.com/cloudwego/netpoll v0.7.1 h1://3rtQV/auOCsqHn9XrXwYJhSgAS+5zSBPpYPm5vydY= +github.com/cloudwego/netpoll v0.7.1/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -22,27 +36,39 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -54,14 +80,22 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +github.com/nyaruka/phonenumbers v1.6.5 h1:aBCaUhfpRA7hU6fsXk+p7KF1aNx4nQlq9hGeo2qdFg8= +github.com/nyaruka/phonenumbers v1.6.5/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= 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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -86,21 +120,35 @@ github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Z github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= +golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -112,12 +160,17 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -128,11 +181,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -147,6 +204,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -155,9 +215,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -171,14 +234,24 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/libc v1.66.9 h1:YkHp7E1EWrN2iyNav7JE/nHasmshPvlGkon1VxGqOw0= +modernc.org/libc v1.66.9/go.mod h1:aVdcY7udcawRqauu0HukYYxtBSizV+R80n/6aQe9D5k= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= +modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E= resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4= diff --git a/internal/controller/v1/file.go b/internal/controller/v1/file.go index e0743d4..a83dcd0 100644 --- a/internal/controller/v1/file.go +++ b/internal/controller/v1/file.go @@ -1,129 +1,129 @@ package v1 import ( - "context" - "io" - "path/filepath" - "strconv" + "context" + "io" + "path/filepath" + "strconv" - "github.com/cloudwego/hertz/pkg/app" - "github.com/sirupsen/logrus" - "github.com/snowykami/neo-blog/internal/ctxutils" - "github.com/snowykami/neo-blog/internal/model" - "github.com/snowykami/neo-blog/internal/repo" - "github.com/snowykami/neo-blog/pkg/filedriver" - "github.com/snowykami/neo-blog/pkg/resps" - "github.com/snowykami/neo-blog/pkg/utils" + "github.com/cloudwego/hertz/pkg/app" + "github.com/sirupsen/logrus" + "github.com/snowykami/neo-blog/internal/ctxutils" + "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/filedriver" + "github.com/snowykami/neo-blog/pkg/resps" + "github.com/snowykami/neo-blog/pkg/utils" ) type FileController struct{} func NewFileController() *FileController { - return &FileController{} + return &FileController{} } func (f *FileController) UploadFileStream(ctx context.Context, c *app.RequestContext) { - // 获取文件信息 - file, err := c.FormFile("file") - if err != nil { - logrus.Error("无法读取文件: ", err) - resps.BadRequest(c, err.Error()) - return - } + // 获取文件信息 + file, err := c.FormFile("file") + if err != nil { + logrus.Error("无法读取文件: ", err) + resps.BadRequest(c, err.Error()) + return + } - group := string(c.FormValue("group")) - name := string(c.FormValue("name")) + group := string(c.FormValue("group")) + name := string(c.FormValue("name")) - // 初始化文件驱动 - driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig()) - if err != nil { - logrus.Error("获取文件驱动失败: ", err) - resps.InternalServerError(c, "获取文件驱动失败") - return - } + // 初始化文件驱动 + driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig()) + if err != nil { + logrus.Error("获取文件驱动失败: ", err) + resps.InternalServerError(c, "获取文件驱动失败") + return + } - // 校验文件哈希 - if hashForm := string(c.FormValue("hash")); hashForm != "" { - dir, fileName := utils.FilePath(hashForm) - storagePath := filepath.Join(dir, fileName) - if _, err := driver.Stat(c, storagePath); err == nil { - resps.Ok(c, "文件已存在", map[string]any{"hash": hashForm}) - return - } - } + // 校验文件哈希 + if hashForm := string(c.FormValue("hash")); hashForm != "" { + dir, fileName := utils.FilePath(hashForm) + storagePath := filepath.Join(dir, fileName) + if _, err := driver.Stat(c, storagePath); err == nil { + resps.Ok(c, "文件已存在", map[string]any{"hash": hashForm}) + return + } + } - // 打开文件 - src, err := file.Open() - if err != nil { - logrus.Error("无法打开文件: ", err) - resps.BadRequest(c, err.Error()) - return - } - defer src.Close() + // 打开文件 + src, err := file.Open() + if err != nil { + logrus.Error("无法打开文件: ", err) + resps.BadRequest(c, err.Error()) + return + } + defer src.Close() - // 计算文件哈希值 - hash, err := utils.FileHashFromStream(src) - if err != nil { - logrus.Error("计算文件哈希失败: ", err) - resps.BadRequest(c, err.Error()) - return - } + // 计算文件哈希值 + hash, err := utils.FileHashFromStream(src) + if err != nil { + logrus.Error("计算文件哈希失败: ", err) + resps.BadRequest(c, err.Error()) + return + } - // 根据哈希值生成存储路径 - dir, fileName := utils.FilePath(hash) - storagePath := filepath.Join(dir, fileName) - // 保存文件 - if _, err := src.Seek(0, io.SeekStart); err != nil { - logrus.Error("无法重置文件流位置: ", err) - resps.BadRequest(c, err.Error()) - return - } - if err := driver.Save(c, storagePath, src); err != nil { - logrus.Error("保存文件失败: ", err) - resps.InternalServerError(c, err.Error()) - return - } - // 数据库索引建立 - currentUser, ok := ctxutils.GetCurrentUser(ctx) - if !ok { - resps.InternalServerError(c, "获取当前用户失败") - return - } - fileModel := &model.File{ - Hash: hash, - UserID: currentUser.ID, - Group: group, - Name: name, - } + // 根据哈希值生成存储路径 + dir, fileName := utils.FilePath(hash) + storagePath := filepath.Join(dir, fileName) + // 保存文件 + if _, err := src.Seek(0, io.SeekStart); err != nil { + logrus.Error("无法重置文件流位置: ", err) + resps.BadRequest(c, err.Error()) + return + } + if err := driver.Save(c, storagePath, src); err != nil { + logrus.Error("保存文件失败: ", err) + resps.InternalServerError(c, err.Error()) + return + } + // 数据库索引建立 + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok { + resps.InternalServerError(c, "获取当前用户失败") + return + } + fileModel := &model.File{ + Hash: hash, + UserID: currentUser.ID, + Group: group, + Name: name, + } - if err := repo.File.Create(fileModel); err != nil { - logrus.Error("数据库索引建立失败: ", err) - resps.InternalServerError(c, "数据库索引建立失败") - return - } - resps.Ok(c, "文件上传成功", map[string]any{"hash": hash, "id": fileModel.ID}) + if err := repo.File.Create(fileModel); err != nil { + logrus.Error("数据库索引建立失败: ", err) + resps.InternalServerError(c, "数据库索引建立失败") + return + } + resps.Ok(c, "文件上传成功", map[string]any{"hash": hash, "id": fileModel.ID}) } func (f *FileController) GetFile(ctx context.Context, c *app.RequestContext) { - fileIdString := c.Param("id") - fileId, err := strconv.ParseUint(fileIdString, 10, 64) - if err != nil { - logrus.Error("无效的文件ID: ", err) - resps.BadRequest(c, "无效的文件ID") - return - } - fileModel, err := repo.File.GetByID(uint(fileId)) - if err != nil { - logrus.Error("获取文件信息失败: ", err) - resps.InternalServerError(c, "获取文件信息失败") - return - } - driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig()) - if err != nil { - logrus.Error("获取文件驱动失败: ", err) - resps.InternalServerError(c, "获取文件驱动失败") - return - } - filePath := filepath.Join(utils.FilePath(fileModel.Hash)) - driver.Get(c, filePath) + fileIdString := c.Param("id") + fileId, err := strconv.ParseUint(fileIdString, 10, 64) + if err != nil { + logrus.Error("无效的文件ID: ", err) + resps.BadRequest(c, "无效的文件ID") + return + } + fileModel, err := repo.File.GetByID(uint(fileId)) + if err != nil { + logrus.Error("获取文件信息失败: ", err) + resps.InternalServerError(c, "获取文件信息失败") + return + } + driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig()) + if err != nil { + logrus.Error("获取文件驱动失败: ", err) + resps.InternalServerError(c, "获取文件驱动失败") + return + } + filePath := filepath.Join(utils.FilePath(fileModel.Hash)) + driver.Get(c, filePath) } diff --git a/internal/controller/v1/user.go b/internal/controller/v1/user.go index 621bfa2..6c0e622 100644 --- a/internal/controller/v1/user.go +++ b/internal/controller/v1/user.go @@ -3,6 +3,7 @@ package v1 import ( "context" "strconv" + "strings" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/utils" @@ -184,6 +185,9 @@ func (u *UserController) VerifyEmail(ctx context.Context, c *app.RequestContext) resps.BadRequest(c, resps.ErrParamInvalid) return } + if verifyEmailReq.Email == "" { + resps.BadRequest(c, resps.ErrParamInvalid) + } resp, err := u.service.RequestVerifyEmail(&verifyEmailReq) if err != nil { serviceErr := errs.AsServiceError(err) @@ -194,11 +198,63 @@ func (u *UserController) VerifyEmail(ctx context.Context, c *app.RequestContext) } func (u *UserController) ChangePassword(ctx context.Context, c *app.RequestContext) { - // TODO: 实现修改密码功能 + var updatePasswordReq dto.UpdatePasswordReq + if err := c.BindAndValidate(&updatePasswordReq); err != nil { + resps.BadRequest(c, resps.ErrParamInvalid) + return + } + ok, err := u.service.UpdatePassword(ctx, &updatePasswordReq) + if err != nil { + resps.InternalServerError(c, err.Error()) + return + } + if !ok { + resps.BadRequest(c, "Failed to change password") + return + } + resps.Ok(c, resps.Success, nil) +} + +func (u *UserController) ResetPassword(ctx context.Context, c *app.RequestContext) { + var resetPasswordReq dto.ResetPasswordReq + if err := c.BindAndValidate(&resetPasswordReq); err != nil { + resps.BadRequest(c, resps.ErrParamInvalid) + return + } + email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail))) + if email == "" { + resps.BadRequest(c, "Email header is required") + return + } + resetPasswordReq.Email = email + ok, err := u.service.ResetPassword(&resetPasswordReq) + if err != nil { + resps.InternalServerError(c, err.Error()) + return + } + if !ok { + resps.BadRequest(c, "Failed to reset password") + return + } + resps.Ok(c, resps.Success, nil) } func (u *UserController) ChangeEmail(ctx context.Context, c *app.RequestContext) { - // TODO: 实现修改邮箱功能 + email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail))) + if email == "" { + resps.BadRequest(c, "Email header is required") + return + } + ok, err := u.service.UpdateEmail(ctx, email) + if err != nil { + resps.InternalServerError(c, err.Error()) + return + } + if !ok { + resps.BadRequest(c, "Failed to change email") + return + } + resps.Ok(c, resps.Success, nil) } func (u *UserController) GetCaptchaConfig(ctx context.Context, c *app.RequestContext) { diff --git a/internal/ctxutils/user.go b/internal/ctxutils/user.go index ee627ab..fcab263 100644 --- a/internal/ctxutils/user.go +++ b/internal/ctxutils/user.go @@ -1,33 +1,32 @@ package ctxutils import ( - "context" + "context" - "github.com/snowykami/neo-blog/internal/model" - "github.com/snowykami/neo-blog/internal/repo" - "github.com/snowykami/neo-blog/pkg/constant" + "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/pkg/constant" ) // GetCurrentUser 从上下文中获取当前用户 func GetCurrentUser(ctx context.Context) (*model.User, bool) { - val := ctx.Value(constant.ContextKeyUserID) - if val == nil { - return nil, false - } - user, err := repo.User.GetUserByID(val.(uint)) - if err != nil { - return nil, false - } + val := ctx.Value(constant.ContextKeyUserID) + if val == nil { + return nil, false + } + user, err := repo.User.GetUserByID(val.(uint)) + if err != nil { + return nil, false + } - return user, true + return user, true } // GetCurrentUserID 从上下文中获取当前用户ID func GetCurrentUserID(ctx context.Context) (uint, bool) { - user, ok := GetCurrentUser(ctx) - if !ok || user == nil { - return 0, false - } - - return user.ID, true + user, ok := GetCurrentUser(ctx) + if !ok || user == nil { + return 0, false + } + return user.ID, true } diff --git a/internal/dto/user.go b/internal/dto/user.go index 0f0dc1b..b8bdfc5 100644 --- a/internal/dto/user.go +++ b/internal/dto/user.go @@ -1,91 +1,101 @@ package dto type UserDto struct { - ID uint `json:"id"` // 用户ID - Username string `json:"username"` // 用户名 - Nickname string `json:"nickname"` - AvatarUrl string `json:"avatar_url"` // 头像URL - Email string `json:"email"` // 邮箱 - Gender string `json:"gender"` - Role string `json:"role"` - Language string `json:"language"` // 语言 + ID uint `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Nickname string `json:"nickname"` + AvatarUrl string `json:"avatar_url"` // 头像URL + Email string `json:"email"` // 邮箱 + Gender string `json:"gender"` + Role string `json:"role"` + Language string `json:"language"` // 语言 } type UserOidcConfigDto struct { - Name string `json:"name"` // OIDC配置名称 - DisplayName string `json:"display_name"` // OIDC配置显示名称 - Icon string `json:"icon"` // OIDC配置图标URL - LoginUrl string `json:"login_url"` // OIDC登录URL + Name string `json:"name"` // OIDC配置名称 + DisplayName string `json:"display_name"` // OIDC配置显示名称 + Icon string `json:"icon"` // OIDC配置图标URL + LoginUrl string `json:"login_url"` // OIDC登录URL } type UserLoginReq struct { - Username string `json:"username"` // username or email - Password string `json:"password"` + Username string `json:"username"` // username or email + Password string `json:"password"` } type UserLoginResp struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token"` - User UserDto `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + User UserDto `json:"user"` } type UserRegisterReq struct { - Username string `json:"username"` // 用户名 - Nickname string `json:"nickname"` // 昵称 - Password string `json:"password"` // 密码 - Email string `json:"email"` // 邮箱 - VerificationCode string `json:"verification_code"` // 邮箱验证码 + Username string `json:"username"` // 用户名 + Nickname string `json:"nickname"` // 昵称 + Password string `json:"password"` // 密码 + Email string `json:"email"` // 邮箱 + VerificationCode string `json:"verification_code"` // 邮箱验证码 } type UserRegisterResp struct { - Token string `json:"token"` // 访问令牌 - RefreshToken string `json:"refresh_token"` // 刷新令牌 - User UserDto `json:"user"` // 用户信息 + Token string `json:"token"` // 访问令牌 + RefreshToken string `json:"refresh_token"` // 刷新令牌 + User UserDto `json:"user"` // 用户信息 } type VerifyEmailReq struct { - Email string `json:"email"` // 邮箱地址 + Email string `json:"email"` // 邮箱地址 } type VerifyEmailResp struct { - Success bool `json:"success"` // 验证码发送成功与否 + Success bool `json:"success"` // 验证码发送成功与否 } type OidcLoginReq struct { - Name string `json:"name"` // OIDC配置名称 - Code string `json:"code"` // OIDC授权码 - State string `json:"state"` + Name string `json:"name"` // OIDC配置名称 + Code string `json:"code"` // OIDC授权码 + State string `json:"state"` } type OidcLoginResp struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token"` - User UserDto `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + User UserDto `json:"user"` } type ListOidcConfigResp struct { - OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表 + OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表 } type GetUserReq struct { - UserID uint `json:"user_id"` + UserID uint `json:"user_id"` } type GetUserByUsernameReq struct { - Username string `json:"username"` + Username string `json:"username"` } type GetUserResp struct { - User UserDto `json:"user"` // 用户信息 + User UserDto `json:"user"` // 用户信息 } type UpdateUserReq struct { - ID uint `json:"id"` - Username string `json:"username"` - Nickname string `json:"nickname"` - AvatarUrl string `json:"avatar_url"` - Gender string `json:"gender"` + ID uint `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + AvatarUrl string `json:"avatar_url"` + Gender string `json:"gender"` } type UpdateUserResp struct { - User *UserDto `json:"user"` // 更新后的用户信息 + User *UserDto `json:"user"` // 更新后的用户信息 +} + +type UpdatePasswordReq struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +type ResetPasswordReq struct { + Email string `json:"-" binding:"-"` + NewPassword string `json:"new_password"` } diff --git a/internal/middleware/email_verify.go b/internal/middleware/email_verify.go index c870d7c..818008a 100644 --- a/internal/middleware/email_verify.go +++ b/internal/middleware/email_verify.go @@ -1 +1,31 @@ package middleware + +import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/snowykami/neo-blog/pkg/constant" + "github.com/snowykami/neo-blog/pkg/resps" + "github.com/snowykami/neo-blog/pkg/utils" +) + +// UseEmailVerify 中间件函数,用于邮箱验证,使用前先调用请求发送邮件验证码函数 +func UseEmailVerify() app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + email := string(c.GetHeader(constant.HeaderKeyEmail)) + verifyCode := string(c.GetHeader(constant.HeaderKeyVerifyCode)) + if !utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) { + c.Next(ctx) + } + if email == "" || verifyCode == "" { + resps.BadRequest(c, "缺失email和verifyCode") + return + } + ok := utils.VerifyEmailCode(email, verifyCode) + if !ok { + resps.Unauthorized(c, "验证码错误") + return + } + c.Next(ctx) + } +} diff --git a/internal/repo/init.go b/internal/repo/init.go index dbcfc1a..3aaf9df 100644 --- a/internal/repo/init.go +++ b/internal/repo/init.go @@ -9,6 +9,7 @@ import ( "github.com/glebarez/sqlite" "github.com/sirupsen/logrus" "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/utils" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -36,14 +37,14 @@ type DBConfig struct { // loadDBConfig 从配置文件加载数据库配置 func loadDBConfig() DBConfig { return DBConfig{ - Driver: utils.Env.Get("DB_DRIVER", "sqlite"), - Path: utils.Env.Get("DB_PATH", "./data/data.db"), - Host: utils.Env.Get("DB_HOST", "postgres"), - Port: utils.Env.GetAsInt("DB_PORT", 5432), - User: utils.Env.Get("DB_USER", "blog"), - Password: utils.Env.Get("DB_PASSWORD", "blog"), - DBName: utils.Env.Get("DB_NAME", "blog"), - SSLMode: utils.Env.Get("DB_SSLMODE", "disable"), + Driver: utils.Env.Get(constant.EnvKeyDBDriver, "sqlite"), + Path: utils.Env.Get(constant.EnvKeyDBPath, "./data/data.db"), + Host: utils.Env.Get(constant.EnvKeyDBHost, "postgres"), + Port: utils.Env.GetAsInt(constant.EnvKeyDBPort, 5432), + User: utils.Env.Get(constant.EnvKeyDBUser, "blog"), + Password: utils.Env.Get(constant.EnvKeyDBPassword, "blog"), + DBName: utils.Env.Get(constant.EnvKeyDBName, "blog"), + SSLMode: utils.Env.Get(constant.EnvKeyDBSSLMode, "disable"), } } diff --git a/internal/router/apiv1/user.go b/internal/router/apiv1/user.go index 08c8072..f852a3c 100644 --- a/internal/router/apiv1/user.go +++ b/internal/router/apiv1/user.go @@ -7,10 +7,11 @@ import ( ) func registerUserRoutes(group *route.RouterGroup) { + const userRoute = "/user" userController := v1.NewUserController() - userGroup := group.Group("/user").Use(middleware.UseAuth(true)) - userGroupWithoutAuth := group.Group("/user").Use(middleware.UseAuth(false)) - userGroupWithoutAuthNeedsCaptcha := group.Group("/user").Use(middleware.UseCaptcha()) + userGroup := group.Group(userRoute).Use(middleware.UseAuth(true)) + userGroupWithoutAuth := group.Group(userRoute).Use(middleware.UseAuth(false)) + userGroupWithoutAuthNeedsCaptcha := group.Group(userRoute).Use(middleware.UseCaptcha()) { userGroupWithoutAuthNeedsCaptcha.POST("/login", userController.Login) userGroupWithoutAuthNeedsCaptcha.POST("/register", userController.Register) @@ -20,10 +21,11 @@ func registerUserRoutes(group *route.RouterGroup) { userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin) userGroupWithoutAuth.GET("/u/:id", userController.GetUser) userGroupWithoutAuth.GET("/username/:username", userController.GetUserByUsername) + userGroup.POST("/logout", userController.Logout) userGroup.GET("/me", userController.GetUser) - userGroupWithoutAuth.POST("/logout", userController.Logout) userGroup.PUT("/u/:id", userController.UpdateUser) userGroup.PUT("/password/edit", userController.ChangePassword) - userGroup.PUT("/email/edit", userController.ChangeEmail) + group.Group(userRoute).Use(middleware.UseEmailVerify()).PUT("/password/reset", userController.ResetPassword) // 不需要登录 + group.Group(userRoute).Use(middleware.UseAuth(true), middleware.UseEmailVerify()).PUT("/email/edit", userController.ChangeEmail) } } diff --git a/internal/service/user.go b/internal/service/user.go index 9940fb9..272b811 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( "time" "github.com/sirupsen/logrus" + "github.com/snowykami/neo-blog/internal/ctxutils" "github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/repo" @@ -36,7 +38,7 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro if user == nil { return nil, errs.ErrNotFound } - if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) { + if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) { token, refreshToken, err := s.generate2Token(user.ID) if err != nil { logrus.Errorln("Failed to generate tokens:", err) @@ -55,15 +57,11 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) { // 验证邮箱验证码 - if !utils.Env.GetAsBool("ENABLE_REGISTER", true) { + if !utils.Env.GetAsBool(constant.EnvKeyEnableRegister, true) { return nil, errs.ErrForbidden } - if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) { - ok, err := s.verifyEmail(req.Email, req.VerificationCode) - if err != nil { - logrus.Errorln("Failed to verify email:", err) - return nil, errs.ErrInternalServer - } + if utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) { + ok := utils.VerifyEmailCode(req.Email, req.VerificationCode) if !ok { return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil) } @@ -81,7 +79,7 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR return nil, errs.New(http.StatusConflict, "Username or email already exists", nil) } // 创建新用户 - hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) + hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) if err != nil { logrus.Errorln("Failed to hash password:", err) return nil, errs.ErrInternalServer @@ -122,19 +120,15 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR } func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) { - generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef") - kv := utils.KV.GetInstance() - kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10) - + verifyCode := utils.RequestEmailVerify(req.Email) template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{}) if err != nil { return nil, errs.ErrInternalServer } if utils.IsDevMode { - logrus.Infof("%s's verification code is %s", req.Email, generatedVerificationCode) + logrus.Infof("%s's verification code is %s", req.Email, verifyCode) } err = utils.Email.SendEmail(utils.Email.GetEmailConfigFromEnv(), req.Email, "验证你的电子邮件 / Verify your email", template, true) - if err != nil { return nil, errs.ErrInternalServer } @@ -373,6 +367,56 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e return &dto.UpdateUserResp{}, nil } +func (s *UserService) UpdatePassword(ctx context.Context, req *dto.UpdatePasswordReq) (bool, error) { + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok || currentUser == nil { + return false, errs.ErrUnauthorized + } + if !utils.Password.VerifyPassword(req.OldPassword, currentUser.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) { + return false, errs.New(http.StatusForbidden, "Old password is incorrect", nil) + } + hashedPassword, err := utils.Password.HashPassword(req.NewPassword, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) + if err != nil { + logrus.Errorln("Failed to update password:", err) + } + currentUser.Password = hashedPassword + err = repo.GetDB().Save(currentUser).Error + if err != nil { + return false, errs.ErrInternalServer + } + return true, nil +} + +func (s *UserService) ResetPassword(req *dto.ResetPasswordReq) (bool, error) { + user, err := repo.User.GetUserByEmail(req.Email) + if err != nil { + return false, errs.ErrInternalServer + } + hashedPassword, err := utils.Password.HashPassword(req.NewPassword, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) + if err != nil { + return false, errs.ErrInternalServer + } + user.Password = hashedPassword + err = repo.User.UpdateUser(user) + if err != nil { + return false, errs.ErrInternalServer + } + return true, nil +} + +func (s *UserService) UpdateEmail(ctx context.Context, email string) (bool, error) { + currentUser, ok := ctxutils.GetCurrentUser(ctx) + if !ok || currentUser == nil { + return false, errs.ErrUnauthorized + } + currentUser.Email = email + err := repo.GetDB().Save(currentUser).Error + if err != nil { + return false, errs.ErrInternalServer + } + return true, nil +} + func (s *UserService) generate2Token(userID uint) (string, string, error) { token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second) tokenString, err := token.ToString() @@ -390,12 +434,3 @@ func (s *UserService) generate2Token(userID uint) (string, string, error) { } return tokenString, refreshTokenString, nil } - -func (s *UserService) verifyEmail(email, code string) (bool, error) { - kv := utils.KV.GetInstance() - verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email) - if !ok || verificationCode != code { - return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil) - } - return true, nil -} diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index af9cdec..ddcdda4 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -19,6 +19,23 @@ const ( EnvKeyCaptchaSecreteKey = "CAPTCHA_SECRET_KEY" // captcha站点密钥 EnvKeyCaptchaUrl = "CAPTCHA_URL" // 某些自托管的captcha的url EnvKeyCaptchaSiteKey = "CAPTCHA_SITE_KEY" // captcha密钥key + EnvKeyDBDriver = "DB_DRIVER" // 环境变量:数据库驱动 + EnvKeyDBPath = "DB_PATH" // 环境变量:数据库文件路径(仅适用于SQLite) + EnvKeyDBHost = "DB_HOST" // 环境变量:数据库主机(仅适用于PostgreSQL) + EnvKeyDBPort = "DB_PORT" // 环境变量:数据库端口(仅适用于PostgreSQL) + EnvKeyDBUser = "DB_USER" // 环境变量:数据库用户(仅适用于PostgreSQL) + EnvKeyDBPassword = "DB_PASSWORD" // 环境变量:数据库密码(仅适用于PostgreSQL) + EnvKeyDBName = "DB_NAME" // 环境变量:数据库名称(仅适用于PostgreSQL) + EnvKeyDBSSLMode = "DB_SSLMODE" // 环境变量:数据库SSL模式(仅适用于PostgreSQL) + EnvKeyEmailAddress = "EMAIL_ADDRESS" + EnvKeyEmailEnable = "EMAIL_ENABLE" + EnvKeyEmailHost = "EMAIL_HOST" + EnvKeyEmailPort = "EMAIL_PORT" + EnvKeyEmailUsername = "EMAIL_USERNAME" + EnvKeyEmailPassword = "EMAIL_PASSWORD" + EnvKeyEmailSsl = "EMAIL_SSL" + EnvKeyEnableRegister = "ENABLE_REGISTER" + EnvKeyEnableEmailVerify = "ENABLE_EMAIL_VERIFY" EnvKeyFileDriverType = "FILE_DRIVER_TYPE" EnvKeyFileBasepath = "FILE_BASEPATH" EnvKeyFileWebdavUrl = "FILE_WEBDAV_URL" @@ -39,6 +56,8 @@ const ( FileDriverTypeLocal = "local" FileDriverTypeWebdav = "webdav" FileDriverTypeS3 = "s3" + HeaderKeyEmail = "X-Email" + HeaderKeyVerifyCode = "X-VerifyCode" KVKeyEmailVerificationCode = "email_verification_code:" // KV存储:邮箱验证码 KVKeyOidcState = "oidc_state:" // KV存储:OIDC状态 ApiSuffix = "/api/v1" // API版本前缀 @@ -46,6 +65,7 @@ const ( OidcProviderTypeMisskey = "misskey" // OIDC提供者类型:Misskey OidcProviderTypeOauth2 = "oauth2" // OIDC提供者类型:GitHub DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl + DefaultPasswordSalt = "default_salt_114514" TargetTypePost = "post" TargetTypeComment = "comment" WebdavPolicyProxy = "proxy" diff --git a/pkg/utils/email.go b/pkg/utils/email.go index 0145cd7..c0e1d93 100644 --- a/pkg/utils/email.go +++ b/pkg/utils/email.go @@ -3,9 +3,12 @@ package utils import ( "bytes" "crypto/tls" + "errors" "fmt" - "gopkg.in/gomail.v2" "html/template" + + "github.com/snowykami/neo-blog/pkg/constant" + "gopkg.in/gomail.v2" ) type emailUtils struct{} @@ -42,7 +45,7 @@ func (e *emailUtils) SendTemplate(emailConfig *EmailConfig, target, subject, htm // SendEmail 使用gomail库发送邮件 func (e *emailUtils) SendEmail(emailConfig *EmailConfig, target, subject, content string, isHTML bool) error { if !emailConfig.Enable { - return nil + return errors.New("邮箱服务未启用") } // 创建新邮件 m := gomail.NewMessage() @@ -73,12 +76,12 @@ func (e *emailUtils) SendEmail(emailConfig *EmailConfig, target, subject, conten func (e *emailUtils) GetEmailConfigFromEnv() *EmailConfig { return &EmailConfig{ - Enable: Env.GetAsBool("EMAIL_ENABLE", false), - Username: Env.Get("EMAIL_USERNAME", ""), - Address: Env.Get("EMAIL_ADDRESS", ""), - Host: Env.Get("EMAIL_HOST", "smtp.example.com"), - Port: Env.GetAsInt("EMAIL_PORT", 587), - Password: Env.Get("EMAIL_PASSWORD", ""), - SSL: Env.GetAsBool("EMAIL_SSL", true), + Enable: Env.GetAsBool(constant.EnvKeyEmailEnable, false), + Username: Env.Get(constant.EnvKeyEmailUsername, ""), + Address: Env.Get(constant.EnvKeyEmailAddress, ""), + Host: Env.Get(constant.EnvKeyEmailHost, "smtp.example.com"), + Port: Env.GetAsInt(constant.EnvKeyEmailPort, 587), + Password: Env.Get(constant.EnvKeyEmailPassword, ""), + SSL: Env.GetAsBool(constant.EnvKeyEmailSsl, true), } } diff --git a/pkg/utils/email_verify.go b/pkg/utils/email_verify.go index d4b585b..d707666 100644 --- a/pkg/utils/email_verify.go +++ b/pkg/utils/email_verify.go @@ -1 +1,27 @@ package utils + +import ( + "time" + + "github.com/snowykami/neo-blog/pkg/constant" +) + +func RequestEmailVerify(email string) string { + generatedVerificationCode := Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef") + kv := KV.GetInstance() + kv.Set(constant.KVKeyEmailVerificationCode+email, generatedVerificationCode, time.Minute*10) + return generatedVerificationCode +} + +func VerifyEmailCode(email, code string) bool { + kv := KV.GetInstance() + storedCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email) + if !ok { + return false + } + if storedCode != code { + return false + } + kv.Delete(constant.KVKeyEmailVerificationCode + email) + return true +} diff --git a/pkg/utils/password.go b/pkg/utils/password.go index 7ae7135..bd65c6d 100644 --- a/pkg/utils/password.go +++ b/pkg/utils/password.go @@ -3,6 +3,7 @@ package utils import ( "crypto/sha256" "encoding/hex" + "golang.org/x/crypto/bcrypt" ) diff --git a/web/src/api/user.ts b/web/src/api/user.ts index bd67a8c..f86aea3 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -40,7 +40,7 @@ export async function userRegister( return res.data } -export async function ListOidcConfigs(): Promise> { +export async function listOidcConfigs(): Promise> { const res = await axiosClient.get>( '/user/oidc/list', ) @@ -88,4 +88,24 @@ export async function getCaptchaConfig(): Promise): Promise> { const res = await axiosClient.put>(`/user/u/${data.id}`, data) return res.data +} + +export async function requestEmailVerifyCode(email: string): Promise> { + const res = await axiosClient.post>('/user/email/verify', { email }) + return res.data +} + +export async function updatePassword({ oldPassword, newPassword }: { oldPassword: string, newPassword: string }): Promise> { + const res = await axiosClient.put>('/user/password/edit', { oldPassword, newPassword }) + return res.data +} + +export async function resetPassword({ email, newPassword, verifyCode }: { email: string, newPassword: string, verifyCode: string }): Promise> { + const res = await axiosClient.put>('/user/password/reset', { newPassword }, { headers: { 'X-Email': email, 'X-VerifyCode': verifyCode } }) + return res.data +} + +export async function updateEmail({ newEmail, verifyCode }: { newEmail: string, verifyCode: string }): Promise> { + const res = await axiosClient.put>('/user/email/edit', null, { headers: { 'X-Email': newEmail, 'X-VerifyCode': verifyCode } }) + return res.data } \ No newline at end of file diff --git a/web/src/app/console/global/page.tsx b/web/src/app/console/global/page.tsx new file mode 100644 index 0000000..3fd3466 --- /dev/null +++ b/web/src/app/console/global/page.tsx @@ -0,0 +1,5 @@ +import GlobalPage from "@/components/console/global"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/web/src/app/console/layout.tsx b/web/src/app/console/layout.tsx index fcbceee..806e95e 100644 --- a/web/src/app/console/layout.tsx +++ b/web/src/app/console/layout.tsx @@ -11,12 +11,14 @@ import { useEffect, useState } from "react" import { useAuth } from "@/contexts/auth-context" import { sidebarData, SidebarItem } from "@/components/console/data" import { usePathname } from "next/navigation" +import { useTranslations } from "next-intl" export default function ConsoleLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const t = useTranslations("Console") const { user } = useAuth(); const [title, setTitle] = useState("Title"); const toLogin = useToLogin(); @@ -27,12 +29,12 @@ export default function ConsoleLayout({ useEffect(() => { const currentItem = sideBarItems.find(item => item.url === pathname); if (currentItem) { - setTitle(currentItem.title); - document.title = `${currentItem.title} - 控制台`; + setTitle(t(currentItem.title)); + document.title = `${t(currentItem.title)} - 控制台`; } else { setTitle("Title"); } - }, [pathname, sideBarItems]); + }, [pathname, sideBarItems, t]); useEffect(() => { if (!user) { diff --git a/web/src/app/console/setting/page.tsx b/web/src/app/console/setting/page.tsx deleted file mode 100644 index 05e6b59..0000000 --- a/web/src/app/console/setting/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import SettingPage from "@/components/console/setting"; - -export default function Page() { - return ; -} \ No newline at end of file diff --git a/web/src/app/reset-password/page.tsx b/web/src/app/reset-password/page.tsx new file mode 100644 index 0000000..5c7b4e3 --- /dev/null +++ b/web/src/app/reset-password/page.tsx @@ -0,0 +1,25 @@ +import { ResetPasswordForm } from "@/components/reset-password/reset-password-form"; +import config from "@/config"; +import Image from "next/image"; + +export default function Page() { + return ( + + ) +} \ No newline at end of file diff --git a/web/src/components/common/input-otp.tsx b/web/src/components/common/input-otp.tsx new file mode 100644 index 0000000..0762b81 --- /dev/null +++ b/web/src/components/common/input-otp.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react" +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp" + +export function InputOTPControlled({ onChange }: { onChange: (value: string) => void }) { + const [value, setValue] = useState("") + useEffect(() => { + onChange(value) + }, [value, onChange]) + return ( +
+ setValue(value)} + > + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/web/src/components/console/data.ts b/web/src/components/console/data.ts index 4a92d21..cc44b26 100644 --- a/web/src/components/console/data.ts +++ b/web/src/components/console/data.ts @@ -14,57 +14,57 @@ export interface SidebarItem { export const sidebarData: { navMain: SidebarItem[]; navUserCenter: SidebarItem[] } = { navMain: [ { - title: "大石坝", + title: "dashboard.title", url: "/console", icon: Gauge, permission: isAdmin }, { - title: "文章管理", + title: "post.title", url: "/console/post", icon: Newspaper, permission: isEditor }, { - title: "评论管理", + title: "comment.title", url: "/console/comment", icon: MessageCircle, permission: isEditor }, { - title: "文件管理", + title: "file.title", url: "/console/file", icon: Folder, permission: () => true }, { - title: "用户管理", + title: "user.title", url: "/console/user", icon: Users, permission: isAdmin }, { - title: "全局设置", - url: "/console/setting", + title: "global.title", + url: "/console/global", icon: Settings, permission: isAdmin }, ], navUserCenter: [ { - title: "个人资料", + title: "user_profile.title", url: "/console/user-profile", icon: UserPen, permission: () => true }, { - title: "安全设置", + title: "user_security.title", url: "/console/user-security", icon: ShieldCheck, permission: () => true }, { - title: "个性化", + title: "user-preference.title", url: "/console/user-preference", icon: Palette, permission: () => true diff --git a/web/src/components/console/setting/colors.ts b/web/src/components/console/global/colors.ts similarity index 100% rename from web/src/components/console/setting/colors.ts rename to web/src/components/console/global/colors.ts diff --git a/web/src/components/console/setting/index.tsx b/web/src/components/console/global/index.tsx similarity index 90% rename from web/src/components/console/setting/index.tsx rename to web/src/components/console/global/index.tsx index 955ab2f..c82e51c 100644 --- a/web/src/components/console/setting/index.tsx +++ b/web/src/components/console/global/index.tsx @@ -1,7 +1,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -export default function SettingPage() { +export default function GlobalPage() { return

全局设置 diff --git a/web/src/components/console/nav-main.tsx b/web/src/components/console/nav-main.tsx index a152f2d..30eb7ff 100644 --- a/web/src/components/console/nav-main.tsx +++ b/web/src/components/console/nav-main.tsx @@ -13,6 +13,7 @@ import { usePathname } from "next/navigation"; import { User } from "@/models/user"; import { useAuth } from "@/contexts/auth-context"; import { IconType } from "@/types/icon"; +import { useTranslations } from "next-intl"; export function NavMain({ items, @@ -24,6 +25,7 @@ export function NavMain({ permission: ({ user }: { user: User }) => boolean }[] }) { + const t = useTranslations("Console") const { user } = useAuth(); const pathname = usePathname() ?? "/" @@ -39,7 +41,7 @@ export function NavMain({ {item.icon && } - {item.title} + {t(item.title)} diff --git a/web/src/components/console/nav-ucenter.tsx b/web/src/components/console/nav-ucenter.tsx index d214569..b170a20 100644 --- a/web/src/components/console/nav-ucenter.tsx +++ b/web/src/components/console/nav-ucenter.tsx @@ -1,6 +1,5 @@ "use client" - import { SidebarGroup, SidebarGroupLabel, @@ -13,6 +12,7 @@ import Link from "next/link" import { usePathname } from "next/navigation" import { useAuth } from "@/contexts/auth-context" import { IconType } from "@/types/icon" +import { useTranslations } from "next-intl" export function NavUserCenter({ items, @@ -24,6 +24,7 @@ export function NavUserCenter({ permission: ({ user }: { user: User }) => boolean }[] }) { + const t = useTranslations("Console") const { user } = useAuth(); const pathname = usePathname() ?? "/" @@ -38,7 +39,7 @@ export function NavUserCenter({ {item.icon && } - {item.title} + {t(item.title)} diff --git a/web/src/components/console/user-profile/index.tsx b/web/src/components/console/user-profile/index.tsx index 031e1a7..7a04516 100644 --- a/web/src/components/console/user-profile/index.tsx +++ b/web/src/components/console/user-profile/index.tsx @@ -11,6 +11,7 @@ import { useAuth } from "@/contexts/auth-context"; import { getFileUri } from "@/utils/client/file"; import { getGravatarFromUser } from "@/utils/common/gravatar"; import { getFallbackAvatarFromUsername } from "@/utils/common/username"; +import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -25,9 +26,8 @@ interface PictureInputChangeEvent { } export function UserProfilePage() { + const t = useTranslations("Console.user_profile") const { user } = useAuth(); - - const [nickname, setNickname] = useState(user?.nickname || '') const [username, setUsername] = useState(user?.username || '') const [avatarFile, setAvatarFile] = useState(null) @@ -62,12 +62,12 @@ export function UserProfilePage() { }; if (!file.type || !file.type.startsWith('image/') || !constraints.allowedTypes.includes(file.type)) { setAvatarFile(null); - toast.error('只允许上传 PNG / JPEG / WEBP / GIF 格式的图片'); + toast.error(t("only_allow_picture")); return; } if (file.size > constraints.maxSize) { setAvatarFile(null); - toast.error('图片大小不能超过 5MB'); + toast.error(t("picture_size_cannot_exceed", {"size": "5MiB"})); return; } setAvatarFile(file); @@ -79,15 +79,15 @@ export function UserProfilePage() { nickname.trim() === '' || username.trim() === '' ) { - toast.error('Nickname and Username cannot be empty') + toast.error(t("nickname_and_username_cannot_be_empty")) return } if ( - (username.length < 3 || username.length > 20) || + (username.length < 1 || username.length > 20) || (nickname.length < 1 || nickname.length > 20) ) { - toast.error('Nickname and Username must be between 3 and 20 characters') + toast.error(t("nickname_and_username_must_be_between", {"min": 1, "max": 20})) return } @@ -97,7 +97,7 @@ export function UserProfilePage() { gender === user.gender && avatarFile === null ) { - toast.warning('No changes made') + toast.warning(t("no_changes_made")) return } @@ -108,19 +108,17 @@ export function UserProfilePage() { try { const resp = await uploadFile({ file: avatarFile }); avatarUrl = getFileUri(resp.data.id); - console.log('Uploaded avatar, got URL:', avatarUrl); } catch (error: unknown) { - toast.error(`Failed to upload avatar ${error}`); + toast.error(`${t("failed_to_upload_avatar")}: ${error}`); return; } } try { await updateUser({ nickname, username, avatarUrl, gender, id: user.id }); - toast.success('Profile updated successfully'); window.location.reload(); } catch (error: unknown) { - toast.error(`Failed to update profile ${error}`); + toast.error(`${t("failed_to_update_profile")}: ${error}`); } finally { setSubmitting(false); } @@ -138,11 +136,11 @@ export function UserProfilePage() { return (

- Public Profile + {t("public_profile")}

- + {avatarFileUrl ? : @@ -157,13 +155,13 @@ export function UserProfilePage() { />
- + setNickname(e.target.value)} /> - + setUsername(e.target.value)} /> - + setGender(e.target.value)}/> - +

) diff --git a/web/src/components/console/user-security/index.tsx b/web/src/components/console/user-security/index.tsx index ddf240d..d937e55 100644 --- a/web/src/components/console/user-security/index.tsx +++ b/web/src/components/console/user-security/index.tsx @@ -3,82 +3,95 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp" -import { useEffect, useState } from "react"; - +import { useState } from "react"; +import { requestEmailVerifyCode, updateEmail, updatePassword } from "@/api/user"; +import { toast } from "sonner"; +import { useTranslations } from "next-intl"; +import { BaseErrorResponse } from "@/models/resp"; +import { useAuth } from "@/contexts/auth-context"; +import { useToResetPassword } from "@/hooks/use-route"; +import { InputOTPControlled } from "@/components/common/input-otp"; // const VERIFY_CODE_COOL_DOWN = 60; // seconds export function UserSecurityPage() { - const [email, setEmail] = useState("") + const t = useTranslations("Console.user_security") + const { user, setUser } = useAuth(); + const toResetPassword = useToResetPassword(); + const [email, setEmail] = useState(user?.email || "") const [verifyCode, setVerifyCode] = useState("") const [oldPassword, setOldPassword] = useState("") const [newPassword, setNewPassword] = useState("") - const handleSubmitPassword = () => { + const handleSubmitPassword = () => { + updatePassword({ oldPassword, newPassword }).then(() => { + toast.success(t("update_password_success")) + setOldPassword("") + setNewPassword("") + }).catch((error: BaseErrorResponse) => { + toast.error(`${t("update_password_failed")}: ${error.response.data.message}`) + }) } + const handleSendVerifyCode = () => { - console.log("send verify code to ", email) + requestEmailVerifyCode(email) + .then(() => { + toast.success(t("send_verify_code_success")) + }) + .catch((error: BaseErrorResponse) => { + console.log("error", error) + toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`) + }) } + const handleSubmitEmail = () => { - console.log("submit email ", email, verifyCode) + updateEmail({ newEmail: email, verifyCode }).then(() => { + toast.success(t("update_email_success")) + if (user) { + setUser({ + ...user, + email, + }) + } + setVerifyCode("") + }).catch((error: BaseErrorResponse) => { + toast.error(`${t("update_email_failed")}: ${error.response.data.message}`) + }) } + if (!user) return null; return (

- 密码设置 + {t("password_setting")}

- + setOldPassword(e.target.value)} /> - + setNewPassword(e.target.value)} /> - +
+ + +
+

- 邮箱设置 + {t("email_setting")}

- +
setEmail(e.target.value)} /> - +
- +
setVerifyCode(value)} /> - +
) } -function InputOTPControlled({ onChange }: { onChange: (value: string) => void }) { - const [value, setValue] = useState("") - useEffect(() => { - onChange(value) - }, [value, onChange]) - return ( -
- setValue(value)} - > - - - - - - - - - -
- ) -} + diff --git a/web/src/components/login/login-form.tsx b/web/src/components/login/login-form.tsx index 935de3f..39b2188 100644 --- a/web/src/components/login/login-form.tsx +++ b/web/src/components/login/login-form.tsx @@ -14,7 +14,7 @@ import { Label } from "@/components/ui/label" import Image from "next/image" import { useEffect, useState } from "react" import type { OidcConfig } from "@/models/oidc-config" -import { getCaptchaConfig, ListOidcConfigs, userLogin } from "@/api/user" +import { getCaptchaConfig, listOidcConfigs, userLogin } from "@/api/user" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { useTranslations } from "next-intl" @@ -22,12 +22,14 @@ import Captcha from "../common/captcha" import { CaptchaProvider } from "@/models/captcha" import { toast } from "sonner" import { useAuth } from "@/contexts/auth-context" +import { resetPasswordPath, useToResetPassword } from "@/hooks/use-route" export function LoginForm({ className, ...props }: React.ComponentProps<"div">) { const t = useTranslations('Login') + const toResetPassword = useToResetPassword(); const {user, setUser} = useAuth(); const [oidcConfigs, setOidcConfigs] = useState([]) const [captchaProps, setCaptchaProps] = useState<{ @@ -50,7 +52,7 @@ export function LoginForm({ }, [user, router, redirectBack]); useEffect(() => { - ListOidcConfigs() + listOidcConfigs() .then((res) => { setOidcConfigs(res.data || []) }) @@ -158,12 +160,12 @@ export function LoginForm({
- {t("forgot_password")} - +
}
diff --git a/web/src/components/reset-password/reset-password-form.tsx b/web/src/components/reset-password/reset-password-form.tsx new file mode 100644 index 0000000..410cf1e --- /dev/null +++ b/web/src/components/reset-password/reset-password-form.tsx @@ -0,0 +1,103 @@ +"use client" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import Image from "next/image" +import { useState } from "react" +import { requestEmailVerifyCode, resetPassword } from "@/api/user" +import Link from "next/link" +import { useTranslations } from "next-intl" +import { toast } from "sonner" +import { InputOTPControlled } from "@/components/common/input-otp" +import { BaseErrorResponse } from "@/models/resp" +import { loginPath, useToLogin } from "@/hooks/use-route" +import router from "next/router" + +export function ResetPasswordForm({ + className, + ...props +}: React.ComponentProps<"div">) { + const t = useTranslations('ResetPassword') + const toLogin = useToLogin(); + const [email, setEmail] = useState("") + const [verifyCode, setVerifyCode] = useState("") + const [newPassword, setNewPassword] = useState("") + + const handleSendVerifyCode = () => { + requestEmailVerifyCode(email) + .then(() => { + toast.success(t("send_verify_code_success")) + }) + .catch((error: BaseErrorResponse) => { + toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`) + }) + } + + const handleResetPassword = () => { + resetPassword({ email, newPassword, verifyCode }).then(() => { + toast.success(t("reset_password_success")) + router.push(loginPath); + }).catch((error: BaseErrorResponse) => { + toast.error(`${t("reset_password_failed")}: ${error.response.data.message}`) + }) + } + + return ( +
+ + + {t("title")} + + +
+
+
+
+ + setNewPassword(e.target.value)} /> +
+
+ +
+ setEmail(e.target.value)} /> + +
+
+
+ + setVerifyCode(value)} /> +
+ +
+ + {/* TODO 回归登录和注册链接 */} +
+
+
+
+ + {/* 服务条款 */} +
+ ) +} \ No newline at end of file diff --git a/web/src/hooks/use-route.ts b/web/src/hooks/use-route.ts index 4b9c2e4..1d5095e 100644 --- a/web/src/hooks/use-route.ts +++ b/web/src/hooks/use-route.ts @@ -5,11 +5,22 @@ import { useRouter, usePathname } from "next/navigation" * 用于跳转到登录页并自动带上 redirect_back 参数 * 用法:const toLogin = useToLogin(); */ +export const loginPath = "/login" +export const resetPasswordPath = "/reset-password" + export function useToLogin() { const router = useRouter() const pathname = usePathname() return () => { - router.push(`/login?redirect_back=${encodeURIComponent(pathname)}`) + router.push(`${loginPath}?redirect_back=${encodeURIComponent(pathname)}`) + } +} + +export function useToResetPassword() { + const router = useRouter() + const pathname = usePathname() + return () => { + router.push(`${resetPasswordPath}?redirect_back=${encodeURIComponent(pathname)}`) } } diff --git a/web/src/locales/zh-CN.json b/web/src/locales/zh-CN.json index 42cd24b..effc8be 100644 --- a/web/src/locales/zh-CN.json +++ b/web/src/locales/zh-CN.json @@ -53,15 +53,74 @@ "unlike_success": "已取消点赞", "update": "更新" }, - "Common":{ + "Common": { "login": "登录", "daysAgo": "天前", "hoursAgo": "小时前", "minutesAgo": "分钟前", - "secondsAgo": "秒前" + "secondsAgo": "秒前", + "submit": "提交", + "update": "更新" }, "Console": { - "login_required": "请先登录再进入后台" + "comment": { + "title": "评论管理" + }, + "dashboard": { + "title": "大石坝" + }, + "file": { + "title": "文件管理" + }, + "global": { + "title": "全局配置" + }, + "login_required": "请先登录再进入后台", + "post": { + "title": "文章管理" + }, + "user": { + "title": "用户管理" + }, + "user_profile": { + "title": "个人资料", + "edit": "编辑", + "failed_to_upload_avatar": "上传头像失败", + "failed_to_update_profile": "更新个人资料失败", + "gender": "性别", + "nickname": "昵称", + "nickname_and_username_cannot_be_empty": "昵称和用户名不能为空", + "nickname_and_username_must_be_between": "昵称和用户名长度必须在{min}~{max}之间", + "no_changes_made": "没有修改任何内容", + "only_allow_picture": "仅允许上传图片格式:PNG / JPEG / WEBP / GIF", + "picture": "头像", + "picture_size_cannot_exceed": "图片大小不能超过{size}", + "public_profile": "公开资料", + "username": "用户名", + "update_profile": "更新资料" + }, + "user_security": { + "title": "安全设置", + "email": "邮箱", + "email_setting": "邮箱设置", + "forgot_password_or_no_password": "忘记密码或没有密码", + "new_password": "新密码", + "old_password": "旧密码", + "password_setting": "密码设置", + "send_verify_code": "发送验证码", + "send_verify_code_failed": "发送验证码失败", + "send_verify_code_success": "验证码已发送", + "update_email": "更新邮箱", + "update_email_failed": "更新邮箱失败", + "update_email_success": "邮箱已更新", + "update_password": "更新密码", + "update_password_failed": "更新密码失败", + "update_password_success": "密码已更新", + "verify_code": "验证码" + }, + "user-preference": { + "title": "个性化" + } }, "Login": { "captcha_error": "验证错误,请重试。", @@ -83,5 +142,17 @@ "terms_of_service": "服务条款", "and": "和", "privacy_policy": "隐私政策" + }, + "ResetPassword": { + "title": "重置密码", + "email": "邮箱", + "new_password": "新密码", + "reset_password": "重置密码", + "reset_password_failed": "重置密码失败", + "reset_password_success": "密码已重置,请使用新密码登录", + "send_verify_code": "发送验证码", + "send_verify_code_failed": "发送验证码失败", + "send_verify_code_success": "验证码已发送", + "verify_code": "验证码" } } \ No newline at end of file diff --git a/web/src/models/resp.ts b/web/src/models/resp.ts index 0067ddd..ee2625a 100644 --- a/web/src/models/resp.ts +++ b/web/src/models/resp.ts @@ -1,5 +1,13 @@ +import { AxiosError, AxiosResponse } from "axios"; + export interface BaseResponse { data: T; message: string; status: number; +} + +export interface BaseErrorResponse> extends AxiosError { + response: AxiosResponse & { + data: E & BaseResponse; + }; } \ No newline at end of file