mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
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.
This commit is contained in:
68
go.mod
68
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
|
||||
)
|
||||
|
73
go.sum
73
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=
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
@ -40,7 +40,7 @@ export async function userRegister(
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function ListOidcConfigs(): Promise<BaseResponse<OidcConfig[]>> {
|
||||
export async function listOidcConfigs(): Promise<BaseResponse<OidcConfig[]>> {
|
||||
const res = await axiosClient.get<BaseResponse<OidcConfig[]>>(
|
||||
'/user/oidc/list',
|
||||
)
|
||||
@ -89,3 +89,23 @@ export async function updateUser(data: Partial<User>): Promise<BaseResponse<User
|
||||
const res = await axiosClient.put<BaseResponse<User>>(`/user/u/${data.id}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function requestEmailVerifyCode(email: string): Promise<BaseResponse<{ coolDown: number }>> {
|
||||
const res = await axiosClient.post<BaseResponse<{ coolDown: number }>>('/user/email/verify', { email })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updatePassword({ oldPassword, newPassword }: { oldPassword: string, newPassword: string }): Promise<BaseResponse<null>> {
|
||||
const res = await axiosClient.put<BaseResponse<null>>('/user/password/edit', { oldPassword, newPassword })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function resetPassword({ email, newPassword, verifyCode }: { email: string, newPassword: string, verifyCode: string }): Promise<BaseResponse<null>> {
|
||||
const res = await axiosClient.put<BaseResponse<null>>('/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<BaseResponse<null>> {
|
||||
const res = await axiosClient.put<BaseResponse<null>>('/user/email/edit', null, { headers: { 'X-Email': newEmail, 'X-VerifyCode': verifyCode } })
|
||||
return res.data
|
||||
}
|
5
web/src/app/console/global/page.tsx
Normal file
5
web/src/app/console/global/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import GlobalPage from "@/components/console/global";
|
||||
|
||||
export default function Page() {
|
||||
return <GlobalPage />;
|
||||
}
|
@ -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) {
|
||||
|
@ -1,5 +0,0 @@
|
||||
import SettingPage from "@/components/console/setting";
|
||||
|
||||
export default function Page() {
|
||||
return <SettingPage />;
|
||||
}
|
25
web/src/app/reset-password/page.tsx
Normal file
25
web/src/app/reset-password/page.tsx
Normal file
@ -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 (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<a href="#" className="flex items-center gap-3 self-center font-bold text-2xl">
|
||||
<div className="flex size-10 items-center justify-center rounded-full overflow-hidden border-2 border-gray-300 dark:border-gray-600">
|
||||
<Image
|
||||
src={config.metadata.icon}
|
||||
alt="Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-bold text-2xl">{config.metadata.name}</span>
|
||||
</a>
|
||||
<ResetPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
27
web/src/components/common/input-otp.tsx
Normal file
27
web/src/components/common/input-otp.tsx
Normal file
@ -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 (
|
||||
<div className="space-y-2">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={value}
|
||||
onChange={(value) => setValue(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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 <div>
|
||||
<h2 className="text-2xl font-bold">
|
||||
全局设置
|
@ -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({
|
||||
<Link href={item.url}>
|
||||
<SidebarMenuButton tooltip={item.title} isActive={pathname === item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<span>{t(item.title)}</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
@ -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({
|
||||
<Link href={item.url}>
|
||||
<SidebarMenuButton tooltip={item.title} isActive={pathname === item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<span>{t(item.title)}</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
|
@ -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<File | null>(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 (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">
|
||||
Public Profile
|
||||
{t("public_profile")}
|
||||
</h1>
|
||||
<Separator className="my-2" />
|
||||
<div className="grid w-full max-w-sm items-center gap-3">
|
||||
<Label htmlFor="picture">Picture</Label>
|
||||
<Label htmlFor="picture">{t("picture")}</Label>
|
||||
<Avatar className="h-40 w-40 rounded-xl border-2">
|
||||
{avatarFileUrl ?
|
||||
<AvatarImage src={avatarFileUrl} alt={nickname || username} /> :
|
||||
@ -157,13 +155,13 @@ export function UserProfilePage() {
|
||||
/>
|
||||
<ImageCropper image={avatarFile} onCropped={handleCropped} />
|
||||
</div>
|
||||
<Label htmlFor="nickname">Nickname</Label>
|
||||
<Label htmlFor="nickname">{t("nickname")}</Label>
|
||||
<Input type="nickname" id="nickname" value={nickname} onChange={(e) => setNickname(e.target.value)} />
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
<Input type="username" id="username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
<Label htmlFor="gender">Gender</Label>
|
||||
<Label htmlFor="gender">{t("gender")}</Label>
|
||||
<Input type="gender" id="gender" value={gender} onChange={(e) => setGender(e.target.value)}/>
|
||||
<Button className="max-w-1/3" onClick={handleSubmit} disabled={submitting}>Submit{submitting && '...'}</Button>
|
||||
<Button className="max-w-1/3" onClick={handleSubmit} disabled={submitting}>{t("update_profile")}{submitting && '...'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div className="grid w-full max-w-sm items-center gap-3">
|
||||
<h1 className="text-2xl font-bold">
|
||||
密码设置
|
||||
{t("password_setting")}
|
||||
</h1>
|
||||
<Label htmlFor="password">Old Password</Label>
|
||||
<Label htmlFor="password">{t("old_password")}</Label>
|
||||
<Input id="password" type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} />
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<Label htmlFor="password">{t("new_password")}</Label>
|
||||
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||
<Button className="max-w-1/3 border-2" onClick={handleSubmitPassword}>Submit</Button>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button disabled={!oldPassword || !newPassword} className="max-w-1/3 border-2" onClick={handleSubmitPassword}>{t("update_password")}</Button>
|
||||
<Button onClick={toResetPassword} variant="ghost">{t("forgot_password_or_no_password")}</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="grid w-full max-w-sm items-center gap-3 py-4">
|
||||
<h1 className="text-2xl font-bold">
|
||||
邮箱设置
|
||||
{t("email_setting")}
|
||||
</h1>
|
||||
<Label htmlFor="email">email</Label>
|
||||
<Label htmlFor="email">{t("email")}</Label>
|
||||
<div className="flex gap-3">
|
||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<Button variant="outline" className="border-2" onClick={handleSendVerifyCode}>发送验证码</Button>
|
||||
<Button disabled={!email || email == user.email} variant="outline" className="border-2" onClick={handleSendVerifyCode}>{t("send_verify_code")}</Button>
|
||||
</div>
|
||||
<Label htmlFor="verify-code">verify code</Label>
|
||||
<Label htmlFor="verify-code">{t("verify_code")}</Label>
|
||||
<div className="flex gap-3">
|
||||
<InputOTPControlled onChange={(value) => setVerifyCode(value)} />
|
||||
<Button className="border-2" onClick={handleSubmitEmail}>Submit</Button>
|
||||
<Button disabled={verifyCode.length < 6} className="border-2" onClick={handleSubmitEmail}>{t("update_email")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPControlled({ onChange }: { onChange: (value: string) => void }) {
|
||||
const [value, setValue] = useState("")
|
||||
useEffect(() => {
|
||||
onChange(value)
|
||||
}, [value, onChange])
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={value}
|
||||
onChange={(value) => setValue(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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<OidcConfig[]>([])
|
||||
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({
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">{t("password")}</Label>
|
||||
<a
|
||||
href="#"
|
||||
<Link
|
||||
href={resetPasswordPath}
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("forgot_password")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
@ -179,7 +181,7 @@ export function LoginForm({
|
||||
</div>
|
||||
}
|
||||
<Button
|
||||
type="submit"
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={handleLogin}
|
||||
disabled={!captchaToken || isLogging}
|
||||
@ -191,9 +193,9 @@ export function LoginForm({
|
||||
{/* 注册链接 */}
|
||||
<div className="text-center text-sm">
|
||||
{t("no_account")}{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
<Link href="#" className="underline underline-offset-4">
|
||||
{t("register")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
103
web/src/components/reset-password/reset-password-form.tsx
Normal file
103
web/src/components/reset-password/reset-password-form.tsx
Normal file
@ -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 (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">{t("title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="password">{t("new_password")}</Label>
|
||||
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">{t("email")}</Label>
|
||||
<div className="flex gap-3">
|
||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<Button
|
||||
disabled={!email}
|
||||
variant="outline"
|
||||
className="border-2"
|
||||
type="button"
|
||||
onClick={handleSendVerifyCode}>{t("send_verify_code")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="verify_code">{t("verify_code")}</Label>
|
||||
<InputOTPControlled onChange={value => setVerifyCode(value)} />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
disabled={!email || !newPassword || !verifyCode}
|
||||
onClick={handleResetPassword}
|
||||
>
|
||||
{t("title")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* TODO 回归登录和注册链接 */}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 服务条款 */}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -5,11 +5,22 @@ import { useRouter, usePathname } from "next/navigation"
|
||||
* 用于跳转到登录页并自动带上 redirect_back 参数
|
||||
* 用法:const toLogin = useToLogin(); <Button onClick={toLogin}>去登录</Button>
|
||||
*/
|
||||
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)}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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": "验证码"
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
export interface BaseResponse<T> {
|
||||
data: T;
|
||||
message: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface BaseErrorResponse<T = unknown, E = Record<string, unknown>> extends AxiosError<T> {
|
||||
response: AxiosResponse & {
|
||||
data: E & BaseResponse<null>;
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user