chore: implement 2fa auth (#2968)
Some checks failed
Build and Release 3X-UI / build (386) (push) Has been cancelled
Build and Release 3X-UI / build (amd64) (push) Has been cancelled
Build and Release 3X-UI / build (arm64) (push) Has been cancelled
Build and Release 3X-UI / build (armv5) (push) Has been cancelled
Build and Release 3X-UI / build (armv6) (push) Has been cancelled
Build and Release 3X-UI / build (armv7) (push) Has been cancelled
Build and Release 3X-UI / build (s390x) (push) Has been cancelled

* chore: implement 2fa auth

from #2786

* chore: format code

* chore: replace two factor token input with qr-code

* chore: requesting confirmation of setting/removing two-factor authentication

otpauth library was taken from cdnjs

* chore: revert changes in `ClipboardManager`

don't need it.

* chore: removing twoFactor prop in settings page

* chore: remove `twoFactorQr` object in `mounted` function
This commit is contained in:
Shishkevich D. 2025-05-08 21:20:58 +07:00 committed by GitHub
parent d39ccf4b8f
commit fe3b1c9b52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 452 additions and 302 deletions

View file

@ -24,7 +24,6 @@ var db *gorm.DB
const ( const (
defaultUsername = "admin" defaultUsername = "admin"
defaultPassword = "admin" defaultPassword = "admin"
defaultSecret = ""
) )
func initModels() error { func initModels() error {
@ -63,7 +62,6 @@ func initUser() error {
user := &model.User{ user := &model.User{
Username: defaultUsername, Username: defaultUsername,
Password: hashedPassword, Password: hashedPassword,
LoginSecret: defaultSecret,
} }
return db.Create(user).Error return db.Create(user).Error
} }

View file

@ -24,7 +24,6 @@ type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
LoginSecret string `json:"loginSecret"`
} }
type Inbound struct { type Inbound struct {

3
go.mod
View file

@ -15,8 +15,10 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.3 github.com/shirou/gopsutil/v4 v4.25.3
github.com/valyala/fasthttp v1.61.0 github.com/valyala/fasthttp v1.61.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882 github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.37.0
golang.org/x/text v0.24.0 golang.org/x/text v0.24.0
google.golang.org/grpc v1.72.0 google.golang.org/grpc v1.72.0
gorm.io/driver/sqlite v1.5.7 gorm.io/driver/sqlite v1.5.7
@ -84,7 +86,6 @@ require (
go.uber.org/mock v0.5.2 // indirect go.uber.org/mock v0.5.2 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.16.0 // indirect golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/sync v0.13.0 // indirect

2
go.sum
View file

@ -187,6 +187,8 @@ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM= github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg= github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882 h1:O/aN4TCrJ+fmaDOBoQhtTRev2hVHIENy2EJ70jQcyEY= github.com/xtls/xray-core v1.250306.1-0.20250430044058-87ab8e512882 h1:O/aN4TCrJ+fmaDOBoQhtTRev2hVHIENy2EJ70jQcyEY=

35
main.go
View file

@ -346,36 +346,6 @@ func migrateDb() {
fmt.Println("Migration done!") fmt.Println("Migration done!")
} }
func removeSecret() {
userService := service.UserService{}
secretExists, err := userService.CheckSecretExistence()
if err != nil {
fmt.Println("Error checking secret existence:", err)
return
}
if !secretExists {
fmt.Println("No secret exists to remove.")
return
}
err = userService.RemoveUserSecret()
if err != nil {
fmt.Println("Error removing secret:", err)
return
}
settingService := service.SettingService{}
err = settingService.SetSecretStatus(false)
if err != nil {
fmt.Println("Error updating secret status:", err)
return
}
fmt.Println("Secret removed successfully.")
}
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
runWebServer() runWebServer()
@ -403,10 +373,8 @@ func main() {
var reset bool var reset bool
var show bool var show bool
var getCert bool var getCert bool
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
settingCmd.BoolVar(&show, "show", false, "Display current settings") settingCmd.BoolVar(&show, "show", false, "Display current settings")
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "Remove secret key")
settingCmd.IntVar(&port, "port", 0, "Set panel port number") settingCmd.IntVar(&port, "port", 0, "Set panel port number")
settingCmd.StringVar(&username, "username", "", "Set login username") settingCmd.StringVar(&username, "username", "", "Set login username")
settingCmd.StringVar(&password, "password", "", "Set login password") settingCmd.StringVar(&password, "password", "", "Set login password")
@ -470,9 +438,6 @@ func main() {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
} }
if remove_secret {
removeSecret()
}
if enabletgbot { if enabletgbot {
updateTgbotEnableSts(enabletgbot) updateTgbotEnableSts(enabletgbot)
} }

View file

@ -23,8 +23,9 @@ class AllSetting {
this.tgBotLoginNotify = true; this.tgBotLoginNotify = true;
this.tgCpu = 80; this.tgCpu = 80;
this.tgLang = "en-US"; this.tgLang = "en-US";
this.twoFactorEnable = false;
this.twoFactorToken = "";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false; this.subEnable = false;
this.subTitle = ""; this.subTitle = "";
this.subListen = ""; this.subListen = "";

View file

@ -145,6 +145,33 @@ class RandomUtil {
return Base64.alternativeEncode(String.fromCharCode(...array)); return Base64.alternativeEncode(String.fromCharCode(...array));
} }
static randomBase32String(length = 16) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let result = '';
let bits = 0;
let buffer = 0;
for (let i = 0; i < array.length; i++) {
buffer = (buffer << 8) | array[i];
bits += 8;
while (bits >= 5) {
bits -= 5;
result += base32Chars[(buffer >>> bits) & 0x1F];
}
}
if (bits > 0) {
result += base32Chars[(buffer << (5 - bits)) & 0x1F];
}
return result;
}
} }
class ObjectUtil { class ObjectUtil {

19
web/assets/otpauth/otpauth.umd.min.js vendored Normal file
View file

@ -0,0 +1,19 @@
//! otpauth 9.4.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
//! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
/// <reference types="./otpauth.d.ts" />
// @ts-nocheck
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,(function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function o(t,e){return t<<32-e|t>>>e}function h(t,e){return t<<e|t>>>32-e>>>0}const a=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])();function l(t){for(let s=0;s<t.length;s++)t[s]=(e=t[s])<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255;var e}function c(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("utf8ToBytes expected string, got "+typeof t);return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class u{clone(){return this._cloneInto()}}function d(t){const e=e=>t().update(c(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class f extends u{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}));const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this
;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");e(t.outputLen),e(t.blockLen)}(t);const i=c(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,n=new Uint8Array(r);n.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<n.length;t++)n[t]^=54;this.iHash.update(n),this.oHash=t.create();for(let t=0;t<n.length;t++)n[t]^=106;this.oHash.update(n),n.fill(0)}}const b=(t,e,s)=>new f(t,e).update(s).digest();function g(t,e,s){return t&e^~t&s}function p(t,e,s){return t&e^t&s^e&s}b.create=(t,e)=>new f(t,e);class w extends u{update(t){i(this);const{view:e,buffer:s,blockLen:r}=this,o=(t=c(t)).length;for(let i=0;i<o;){const h=Math.min(r-this.pos,o-i);if(h!==r)s.set(t.subarray(i,i+h),this.pos),this.pos+=h,i+=h,this.pos===r&&(this.process(e,0),this.pos=0);else{const e=n(t);for(;r<=o-i;i+=r)this.process(e,i)}}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:o,isLE:h}=this;let{pos:a}=this;e[a++]=128,this.buffer.subarray(a).fill(0),this.padOffset>o-a&&(this.process(s,0),a=0);for(let t=a;t<o;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,l=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+l,h,i)}(s,o-8,BigInt(8*this.length),h),this.process(s,0);const l=n(t),c=this.outputLen;if(c%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const u=c/4,d=this.get()
;if(u>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<u;t++)l.setUint32(4*t,d[t],h)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.length=i,t.pos=o,t.finished=r,t.destroyed=n,i%e&&t.buffer.set(s),t}constructor(t,e,s,i){super(),this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=n(this.buffer)}}const y=new Uint32Array([1732584193,4023233417,2562383102,271733878,3285377520]),x=new Uint32Array(80);class A extends w{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)x[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)x[t]=h(x[t-3]^x[t-8]^x[t-14]^x[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,a;t<20?(e=g(i,r,n),a=1518500249):t<40?(e=i^r^n,a=1859775393):t<60?(e=p(i,r,n),a=2400959708):(e=i^r^n,a=3395469782);const l=h(s,5)+e+o+a+x[t]|0;o=n,n=r,r=h(i,30),i=s,s=l}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,this.set(s,i,r,n,o)}roundClean(){x.fill(0)}destroy(){this.set(0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,20,8,!1),this.A=0|y[0],this.B=0|y[1],this.C=0|y[2],this.D=0|y[3],this.E=0|y[4]}}
const m=d((()=>new A)),H=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),L=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),I=new Uint32Array(64);class S extends w{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)I[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=I[t-15],s=I[t-2],i=o(e,7)^o(e,18)^e>>>3,r=o(s,17)^o(s,19)^s>>>10;I[t]=r+I[t-7]+i+I[t-16]|0}let{A:s,B:i,C:r,D:n,E:h,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(o(h,6)^o(h,11)^o(h,25))+g(h,a,l)+H[t]+I[t]|0,u=(o(s,2)^o(s,13)^o(s,22))+p(s,i,r)|0;c=l,l=a,a=h,h=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,h=h+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(s,i,r,n,h,a,l,c)}roundClean(){I.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,32,8,!1),this.A=0|L[0],this.B=0|L[1],this.C=0|L[2],this.D=0|L[3],this.E=0|L[4],this.F=0|L[5],this.G=0|L[6],this.H=0|L[7]}}class B extends S{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}
const E=d((()=>new S)),U=d((()=>new B)),C=BigInt(2**32-1),O=BigInt(32);function v(t,e=!1){return e?{h:Number(t&C),l:Number(t>>O&C)}:{h:0|Number(t>>O&C),l:0|Number(t&C)}}function k(t,e=!1){let s=new Uint32Array(t.length),i=new Uint32Array(t.length);for(let r=0;r<t.length;r++){const{h:n,l:o}=v(t[r],e);[s[r],i[r]]=[n,o]}return[s,i]}const T=(t,e,s)=>t<<s|e>>>32-s,$=(t,e,s)=>e<<s|t>>>32-s,D=(t,e,s)=>e<<s-32|t>>>64-s,_=(t,e,s)=>t<<s-32|e>>>64-s,F={fromBig:v,split:k,toBig:(t,e)=>BigInt(t>>>0)<<O|BigInt(e>>>0),shrSH:(t,e,s)=>t>>>s,shrSL:(t,e,s)=>t<<32-s|e>>>s,rotrSH:(t,e,s)=>t>>>s|e<<32-s,rotrSL:(t,e,s)=>t<<32-s|e>>>s,rotrBH:(t,e,s)=>t<<64-s|e>>>s-32,rotrBL:(t,e,s)=>t>>>s-32|e<<64-s,rotr32H:(t,e)=>e,rotr32L:(t,e)=>t,rotlSH:T,rotlSL:$,rotlBH:D,rotlBL:_,add:function(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}},add3L:(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),add3H:(t,e,s,i)=>e+s+i+(t/2**32|0)|0,add4L:(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),add4H:(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,add5H:(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,add5L:(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0)
},[G,P]=(()=>F.split(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map((t=>BigInt(t)))))(),j=new Uint32Array(80),M=new Uint32Array(80);class R extends w{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:l,Fh:c,Fl:u,Gh:d,Gl:f,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g]}set(t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,this.Cl=0|n,this.Dh=0|o,
this.Dl=0|h,this.Eh=0|a,this.El=0|l,this.Fh=0|c,this.Fl=0|u,this.Gh=0|d,this.Gl=0|f,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)j[s]=t.getUint32(e),M[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|j[t-15],s=0|M[t-15],i=F.rotrSH(e,s,1)^F.rotrSH(e,s,8)^F.shrSH(e,s,7),r=F.rotrSL(e,s,1)^F.rotrSL(e,s,8)^F.shrSL(e,s,7),n=0|j[t-2],o=0|M[t-2],h=F.rotrSH(n,o,19)^F.rotrBH(n,o,61)^F.shrSH(n,o,6),a=F.rotrSL(n,o,19)^F.rotrBL(n,o,61)^F.shrSL(n,o,6),l=F.add4L(r,a,M[t-7],M[t-16]),c=F.add4H(l,i,h,j[t-7],j[t-16]);j[t]=0|c,M[t]=0|l}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:l,Eh:c,El:u,Fh:d,Fl:f,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=F.rotrSH(c,u,14)^F.rotrSH(c,u,18)^F.rotrBH(c,u,41),y=F.rotrSL(c,u,14)^F.rotrSL(c,u,18)^F.rotrBL(c,u,41),x=c&d^~c&b,A=u&f^~u&g,m=F.add5L(w,y,A,P[t],M[t]),H=F.add5H(m,p,e,x,G[t],j[t]),L=0|m,I=F.rotrSH(s,i,28)^F.rotrBH(s,i,34)^F.rotrBH(s,i,39),S=F.rotrSL(s,i,28)^F.rotrBL(s,i,34)^F.rotrBL(s,i,39),B=s&r^s&o^r&o,E=i&n^i&h^n&h;p=0|b,w=0|g,b=0|d,g=0|f,d=0|c,f=0|u,({h:c,l:u}=F.add(0|a,0|l,0|H,0|L)),a=0|o,l=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const U=F.add3L(L,S,E);s=F.add3H(U,H,I,B),i=0|U}({h:s,l:i}=F.add(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F.add(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F.add(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l}=F.add(0|this.Dh,0|this.Dl,0|a,0|l)),({h:c,l:u}=F.add(0|this.Eh,0|this.El,0|c,0|u)),({h:d,l:f}=F.add(0|this.Fh,0|this.Fl,0|d,0|f)),({h:b,l:g}=F.add(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F.add(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,l,c,u,d,f,b,g,p,w)}roundClean(){j.fill(0),M.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}}class N extends R{constructor(){super(),
this.Ah=-876896931,this.Al=-1056596264,this.Bh=1654270250,this.Bl=914150663,this.Ch=-1856437926,this.Cl=812702999,this.Dh=355462360,this.Dl=-150054599,this.Eh=1731405415,this.El=-4191439,this.Fh=-1900787065,this.Fl=1750603025,this.Gh=-619958771,this.Gl=1694076839,this.Hh=1203062813,this.Hl=-1090891868,this.outputLen=48}}const X=d((()=>new R)),V=d((()=>new N)),Z=[],z=[],J=[],K=BigInt(0),Q=BigInt(1),W=BigInt(2),Y=BigInt(7),q=BigInt(256),tt=BigInt(113);for(let t=0,e=Q,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],Z.push(2*(5*i+s)),z.push((t+1)*(t+2)/2%64);let r=K;for(let t=0;t<7;t++)e=(e<<Q^(e>>Y)*tt)%q,e&W&&(r^=Q<<(Q<<BigInt(t))-Q);J.push(r)}const[et,st]=k(J,!0),it=(t,e,s)=>s>32?D(t,e,s):T(t,e,s),rt=(t,e,s)=>s>32?_(t,e,s):$(t,e,s);class nt extends u{keccak(){a||l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=it(n,o,1)^s[i],a=rt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=z[s],n=it(e,r,i),o=rt(e,r,i),h=Z[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=et[i],t[1]^=st[i]}s.fill(0)}(this.state32,this.rounds),a||l(this.state32),this.posOut=0,this.pos=0}update(t){i(this);const{blockLen:e,state:s}=this,r=(t=c(t)).length;for(let i=0;i<r;){const n=Math.min(e-this.pos,r-i);for(let e=0;e<n;e++)s[this.pos++]^=t[i++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){
if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new nt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,e(i),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const ot=(t,e,s)=>d((()=>new nt(e,t,s))),ht=ot(6,144,28),at=ot(6,136,32),lt=ot(6,104,48),ct=ot(6,72,64),ut=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),dt={SHA1:m,SHA224:U,SHA256:E,SHA384:V,SHA512:X,"SHA3-224":ht,"SHA3-256":at,"SHA3-384":lt,"SHA3-512":ct},ft=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):
return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}},bt="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",gt=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=bt.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},pt=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=bt[s>>>e-5&31],e-=5;return e>0&&(i+=bt[s<<5-e&31]),i},wt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},yt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},xt=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},At=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},mt=ut.TextEncoder?new ut.TextEncoder:null,Ht=ut.TextDecoder?new ut.TextDecoder:null,Lt=t=>{if(!mt)throw new Error("Encoding API not available");return mt.encode(t)},It=t=>{if(!Ht)throw new Error("Encoding API not available");return Ht.decode(t)};class St{static fromLatin1(t){return new St({buffer:xt(t).buffer})}static fromUTF8(t){return new St({buffer:Lt(t).buffer})}static fromBase32(t){return new St({buffer:gt(t).buffer})}static fromHex(t){return new St({buffer:wt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:At(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:It(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:pt(this.bytes)}),
this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,value:yt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(ut.crypto?.getRandomValues)return ut.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class Bt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=Bt.defaults.algorithm,digits:s=Bt.defaults.digits,counter:i=Bt.defaults.counter}){const r=((t,e,s)=>{if(b){const i=dt[t]??dt[ft(t)];return b(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return Bt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=Bt.defaults.digits,counter:r=Bt.defaults.counter,window:n=Bt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=Bt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return Bt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=Bt.defaults.issuer,label:e=Bt.defaults.label,issuerInLabel:s=Bt.defaults.issuerInLabel,secret:i=new St,algorithm:r=Bt.defaults.algorithm,digits:n=Bt.defaults.digits,counter:o=Bt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.counter=o}}class Et{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Et.counter({period:this.period,timestamp:t})}static remaining({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Et.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Et.defaults.period,timestamp:r=Date.now()}){return Bt.generate({secret:t,algorithm:e,digits:s,counter:Et.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Et.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Et.defaults.period,timestamp:n=Date.now(),window:o}){return Bt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Et.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Et.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Et.defaults.issuer,label:e=Et.defaults.label,issuerInLabel:s=Et.defaults.issuerInLabel,secret:i=new St,algorithm:r=Et.defaults.algorithm,digits:n=Et.defaults.digits,period:o=Et.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.period=o}}const Ut=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Ct=/^[2-7A-Z]+=*$/i,Ot=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,vt=/^[+-]?\d+$/,kt=/^\+?[1-9]\d*$/;t.HOTP=Bt,t.Secret=St,t.TOTP=Et,t.URI=class{static parse(t){let e;try{e=t.match(Ut)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce(((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n}),{});let n;const o={};if("hotp"===s){if(n=Bt,void 0===r.counter||!vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Et,void 0!==r.period){if(!kt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Ct.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
if(!Ot.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!kt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof Bt||t instanceof Et)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.0"}));
//# sourceMappingURL=otpauth.umd.min.js.map

View file

@ -16,7 +16,7 @@ import (
type LoginForm struct { type LoginForm struct {
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
LoginSecret string `json:"loginSecret" form:"loginSecret"` TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
} }
type IndexController struct { type IndexController struct {
@ -37,7 +37,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.POST("/login", a.login) g.POST("/login", a.login)
g.GET("/logout", a.logout) g.GET("/logout", a.logout)
g.POST("/getSecretStatus", a.getSecretStatus) g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
} }
func (a *IndexController) index(c *gin.Context) { func (a *IndexController) index(c *gin.Context) {
@ -64,14 +64,13 @@ func (a *IndexController) login(c *gin.Context) {
return return
} }
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret) user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
safeUser := template.HTMLEscapeString(form.Username) safeUser := template.HTMLEscapeString(form.Username)
safePass := template.HTMLEscapeString(form.Password) safePass := template.HTMLEscapeString(form.Password)
safeSecret := template.HTMLEscapeString(form.LoginSecret)
if user == nil { if user == nil {
logger.Warningf("wrong username: \"%s\", password: \"%s\", secret: \"%s\", IP: \"%s\"", safeUser, safePass, safeSecret, getRemoteIp(c)) logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0) a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
@ -108,8 +107,8 @@ func (a *IndexController) logout(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
} }
func (a *IndexController) getSecretStatus(c *gin.Context) { func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
status, err := a.settingService.GetSecretStatus() status, err := a.settingService.GetTwoFactorEnable()
if err == nil { if err == nil {
jsonObj(c, status, nil) jsonObj(c, status, nil)
} }

View file

@ -19,10 +19,6 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"` NewPassword string `json:"newPassword" form:"newPassword"`
} }
type updateSecretForm struct {
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type SettingController struct { type SettingController struct {
settingService service.SettingService settingService service.SettingService
userService service.UserService userService service.UserService
@ -44,8 +40,6 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/updateUser", a.updateUser) g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel) g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
} }
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
@ -107,29 +101,6 @@ func (a *SettingController) restartPanel(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err) jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
} }
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
}
}
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil { if err != nil {

View file

@ -38,7 +38,8 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"` TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"` TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"` TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
SubEnable bool `json:"subEnable" form:"subEnable"` SubEnable bool `json:"subEnable" form:"subEnable"`
SubTitle string `json:"subTitle" form:"subTitle"` SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"` SubListen string `json:"subListen" form:"subListen"`

View file

@ -512,11 +512,11 @@
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon> <a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
</a-input-password> </a-input-password>
</a-form-item> </a-form-item>
<a-form-item v-if="secretEnable"> <a-form-item v-if="twoFactorEnable">
<a-input-password autocomplete="secret" name="secret" v-model.trim="user.loginSecret" <a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login"> placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon> <a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
</a-input-password> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-row justify="center" class="centered">
@ -549,14 +549,14 @@
user: { user: {
username: "", username: "",
password: "", password: "",
loginSecret: "" twoFactorCode: ""
}, },
secretEnable: false, twoFactorEnable: false,
lang: "" lang: ""
}, },
async mounted() { async mounted() {
this.lang = LanguageManager.getLanguage(); this.lang = LanguageManager.getLanguage();
this.secretEnable = await this.getSecretStatus(); this.twoFactorEnable = await this.getTwoFactorEnable();
}, },
methods: { methods: {
async login() { async login() {
@ -567,12 +567,12 @@
location.href = basePath + 'panel/'; location.href = basePath + 'panel/';
} }
}, },
async getSecretStatus() { async getTwoFactorEnable() {
this.loading = true; this.loading = true;
const msg = await HttpUtil.post('/getSecretStatus'); const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false; this.loading = false;
if (msg.success) { if (msg.success) {
this.secretEnable = msg.obj; this.twoFactorEnable = msg.obj;
return msg.obj; return msg.obj;
} }
}, },

View file

@ -0,0 +1,118 @@
{{define "modals/twoFactorModal"}}
<a-modal id="two-factor-modal" v-model="twoFactorModal.visible" :title="twoFactorModal.title" :closable="true"
:class="themeSwitcher.currentTheme">
<template v-if="twoFactorModal.type === 'set'">
<p>{{ i18n "pages.settings.security.twoFactorModalSteps" }}</p>
<a-divider></a-divider>
<p>{{ i18n "pages.settings.security.twoFactorModalFirstStep" }}</p>
<div :style="{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: '12px' }">
<div
:style="{ border: '1px solid', borderRadius: '1rem', borderColor: themeSwitcher.isDarkTheme ? 'var(--dark-color-surface-300)' : '#d9d9d9', padding: 0 }">
<img :src="twoFactorModal.qrImage"
:style="{ filter: themeSwitcher.isDarkTheme ? 'invert(1)' : 'none'}"
:alt="twoFactorModal.token">
</div>
<span :style="{ fontSize: '12px', fontFamily: 'monospace' }">[[ twoFactorModal.token ]]</span>
</div>
<a-divider></a-divider>
<p>{{ i18n "pages.settings.security.twoFactorModalSecondStep" }}</p>
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
</template>
<template v-if="twoFactorModal.type === 'remove'">
<p>{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}</p>
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
</template>
<template slot="footer">
<a-button @click="twoFactorModal.cancel">
<span>{{ i18n "cancel" }}</span>
</a-button>
<a-button type="primary" :disabled="twoFactorModal.enteredCode.length < 6" @click="twoFactorModal.ok">
<span>{{ i18n "confirm" }}</span>
</a-button>
</template>
</a-modal>
<script>
const twoFactorModal = {
title: '',
fileName: '',
token: '',
enteredCode: '',
visible: false,
type: 'set',
confirm: null,
totpObject: null,
qrImage: "",
ok() {
if (twoFactorModal.totpObject.generate() === twoFactorModal.enteredCode) {
ObjectUtil.execute(twoFactorModal.confirm, true)
twoFactorModal.close()
switch (twoFactorModal.type) {
case 'set':
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
break;
case 'remove':
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
break;
default:
break;
}
} else {
Vue.prototype.$message['error']('{{ i18n "pages.settings.security.twoFactorModalError" }}')
}
},
cancel() {
ObjectUtil.execute(twoFactorModal.confirm, false)
twoFactorModal.close()
},
show: function ({
title = '',
token = '',
type = 'set',
confirm = (success) => { }
}) {
this.title = title;
this.token = token;
this.visible = true;
this.confirm = confirm;
this.type = type;
this.totpObject = new OTPAuth.TOTP({
issuer: "3x-ui",
label: "Administrator",
algorithm: "SHA1",
digits: 6,
period: 30,
secret: twoFactorModal.token,
});
if (type === 'set') {
this.qrImage = new QRious({
size: 150,
value: twoFactorModal.totpObject.toString(),
background: 'white',
backgroundAlpha: 0,
foreground: 'black',
padding: 12,
level: 'L'
}).toDataURL()
}
},
close: function () {
twoFactorModal.enteredCode = "";
twoFactorModal.visible = false;
},
};
const twoFactorModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#two-factor-modal',
data: {
twoFactorModal: twoFactorModal,
},
});
</script>
{{end}}

View file

@ -122,10 +122,13 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}} {{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
{{template "component/aSettingListItem" .}} {{template "component/aSettingListItem" .}}
{{template "modals/twoFactorModal"}}
<script> <script>
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
@ -133,7 +136,6 @@
data: { data: {
themeSwitcher, themeSwitcher,
spinning: false, spinning: false,
changeSecret: false,
oldAllSetting: new AllSetting(), oldAllSetting: new AllSetting(),
allSetting: new AllSetting(), allSetting: new AllSetting(),
saveBtnDisable: true, saveBtnDisable: true,
@ -258,7 +260,6 @@
app.changeRemarkSample(); app.changeRemarkSample();
this.saveBtnDisable = true; this.saveBtnDisable = true;
} }
await this.fetchUserSecret();
}, },
async updateAllSetting() { async updateAllSetting() {
this.loading(true); this.loading(true);
@ -302,38 +303,34 @@
window.location.replace(url); window.location.replace(url);
} }
}, },
async fetchUserSecret() { toggleTwoFactor(newValue) {
this.loading(true); if (newValue) {
const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user); const newTwoFactorToken = RandomUtil.randomBase32String()
if (userMessage.success) {
this.user = userMessage.obj; twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalSetTitle" }}',
token: newTwoFactorToken,
type: 'set',
confirm: (success) => {
if (success) {
this.allSetting.twoFactorToken = newTwoFactorToken
} }
this.loading(false);
}, this.allSetting.twoFactorEnable = success
async updateSecret() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
if (msg && msg.obj) {
this.user = msg.obj;
} }
this.loading(false); })
await this.updateAllSetting();
},
async getNewSecret() {
if (!this.changeSecret) {
this.changeSecret = true;
this.user.loginSecret = '';
const newSecret = RandomUtil.randomSeq(64);
await PromiseUtil.sleep(1000);
this.user.loginSecret = newSecret;
this.changeSecret = false;
}
},
async toggleToken(value) {
if (value) {
await this.getNewSecret();
} else { } else {
this.user.loginSecret = ""; twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalDeleteTitle" }}',
token: this.allSetting.twoFactorToken,
type: 'remove',
confirm: (success) => {
if (success) {
this.allSetting.twoFactorEnable = false
this.allSetting.twoFactorToken = ""
}
}
})
} }
}, },
addNoise() { addNoise() {
@ -526,6 +523,7 @@
}, },
async mounted() { async mounted() {
await this.getAllSetting(); await this.getAllSetting();
while (true) { while (true) {
await PromiseUtil.sleep(1000); await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);

View file

@ -31,30 +31,14 @@
</a-space> </a-space>
</a-list-item> </a-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel key="2" header='{{ i18n "pages.settings.security.secret"}}'> <a-collapse-panel key="2" header='{{ i18n "pages.settings.security.twoFactor" }}'>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.security.loginSecurity" }}</template> <template #title>{{ i18n "pages.settings.security.twoFactorEnable" }}</template>
<template #description>{{ i18n "pages.settings.security.loginSecurityDesc" }}</template> <template #description>{{ i18n "pages.settings.security.twoFactorEnableDesc" }}</template>
<template #control> <template #control>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch> <a-switch @click="toggleTwoFactor" :checked="allSetting.twoFactorEnable"></a-switch>
<a-icon :style="{ marginLeft: '1rem' }" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync"
@click="getNewSecret"></a-icon>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.security.secretToken" }}</template>
<template #description>{{ i18n "pages.settings.security.secretTokenDesc" }}</template>
<template #control>
<a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-setting-list-item>
<a-list-item>
<a-space direction="horizontal" :style="{ padding: '0 20px' }">
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">
<span>{{ i18n "confirm"}}</span>
</a-button>
</a-space>
</a-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
{{end}} {{end}}

View file

@ -48,7 +48,8 @@ var defaultValueMap = map[string]string{
"tgBotLoginNotify": "true", "tgBotLoginNotify": "true",
"tgCpu": "80", "tgCpu": "80",
"tgLang": "en-US", "tgLang": "en-US",
"secretEnable": "false", "twoFactorEnable": "false",
"twoFactorToken": "",
"subEnable": "false", "subEnable": "false",
"subTitle": "", "subTitle": "",
"subListen": "", "subListen": "",
@ -166,8 +167,7 @@ func (s *SettingService) ResetSettings() error {
return err return err
} }
return db.Model(model.User{}). return db.Model(model.User{}).
Where("1 = 1"). Where("1 = 1").Error
Update("login_secret", "").Error
} }
func (s *SettingService) getSetting(key string) (*model.Setting, error) { func (s *SettingService) getSetting(key string) (*model.Setting, error) {
@ -318,6 +318,14 @@ func (s *SettingService) GetTgLang() (string, error) {
return s.getString("tgLang") return s.getString("tgLang")
} }
func (s *SettingService) GetTwoFactorEnable() (bool, error) {
return s.getBool("twoFactorEnable")
}
func (s *SettingService) GetTwoFactorToken() (string, error) {
return s.getString("twoFactorToken")
}
func (s *SettingService) GetPort() (int, error) { func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort") return s.getInt("webPort")
} }
@ -358,14 +366,6 @@ func (s *SettingService) GetRemarkModel() (string, error) {
return s.getString("remarkModel") return s.getString("remarkModel")
} }
func (s *SettingService) GetSecretStatus() (bool, error) {
return s.getBool("secretEnable")
}
func (s *SettingService) SetSecretStatus(value bool) error {
return s.setBool("secretEnable", value)
}
func (s *SettingService) GetSecret() ([]byte, error) { func (s *SettingService) GetSecret() ([]byte, error) {
secret, err := s.getString("secret") secret, err := s.getString("secret")
if secret == defaultValueMap["secret"] { if secret == defaultValueMap["secret"] {

View file

@ -8,10 +8,13 @@ import (
"x-ui/logger" "x-ui/logger"
"x-ui/util/crypto" "x-ui/util/crypto"
"github.com/xlzd/gotp"
"gorm.io/gorm" "gorm.io/gorm"
) )
type UserService struct{} type UserService struct {
settingService SettingService
}
func (s *UserService) GetFirstUser() (*model.User, error) { func (s *UserService) GetFirstUser() (*model.User, error) {
db := database.GetDB() db := database.GetDB()
@ -26,13 +29,13 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
return user, nil return user, nil
} }
func (s *UserService) CheckUser(username string, password string, secret string) *model.User { func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User {
db := database.GetDB() db := database.GetDB()
user := &model.User{} user := &model.User{}
err := db.Model(model.User{}). err := db.Model(model.User{}).
Where("username = ? and login_secret = ?", username, secret). Where("username = ?", username).
First(user). First(user).
Error Error
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
@ -42,11 +45,30 @@ func (s *UserService) CheckUser(username string, password string, secret string)
return nil return nil
} }
if crypto.CheckPasswordHash(user.Password, password) { if !crypto.CheckPasswordHash(user.Password, password) {
return user return nil
} }
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil {
logger.Warning("check two factor err:", err)
return nil return nil
}
if twoFactorEnable {
twoFactorToken, err := s.settingService.GetTwoFactorToken()
if err != nil {
logger.Warning("check two factor token err:", err)
return nil
}
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
return nil
}
}
return user
} }
func (s *UserService) UpdateUser(id int, username string, password string) error { func (s *UserService) UpdateUser(id int, username string, password string) error {
@ -63,50 +85,6 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
Error Error
} }
func (s *UserService) UpdateUserSecret(id int, secret string) error {
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Update("login_secret", secret).
Error
}
func (s *UserService) RemoveUserSecret() error {
db := database.GetDB()
return db.Model(model.User{}).
Where("1 = 1").
Update("login_secret", "").
Error
}
func (s *UserService) GetUserSecret(id int) *model.User {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
Where("id = ?", id).
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil
}
return user
}
func (s *UserService) CheckSecretExistence() (bool, error) {
db := database.GetDB()
var count int64
err := db.Model(model.User{}).
Where("login_secret IS NOT NULL").
Count(&count).
Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *UserService) UpdateFirstUser(username string, password string) error { func (s *UserService) UpdateFirstUser(username string, password string) error {
if username == "" { if username == "" {
return errors.New("username can not be empty") return errors.New("username can not be empty")

View file

@ -51,7 +51,7 @@
"install" = "تثبيت" "install" = "تثبيت"
"clients" = "عملاء" "clients" = "عملاء"
"usage" = "استخدام" "usage" = "استخدام"
"secretToken" = "توكن سري" "twoFactorCode" = "الكود"
"remained" = "المتبقي" "remained" = "المتبقي"
"security" = "أمان" "security" = "أمان"
"secAlertTitle" = "تنبيه أمني" "secAlertTitle" = "تنبيه أمني"
@ -87,7 +87,7 @@
"invalidFormData" = "تنسيق البيانات المدخلة مش صحيح." "invalidFormData" = "تنسيق البيانات المدخلة مش صحيح."
"emptyUsername" = "اسم المستخدم مطلوب" "emptyUsername" = "اسم المستخدم مطلوب"
"emptyPassword" = "الباسورد مطلوب" "emptyPassword" = "الباسورد مطلوب"
"wrongUsernameOrPassword" = "اسم المستخدم أو الباسورد أو السر مش صحيح." "wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."
"successLogin" = "تسجيل دخول ناجح" "successLogin" = "تسجيل دخول ناجح"
[pages.index] [pages.index]
@ -501,11 +501,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "بيانات الأدمن" "admin" = "بيانات الأدمن"
"secret" = "توكن سري" "twoFactor" = "المصادقة الثنائية"
"loginSecurity" = "أمان تسجيل الدخول" "twoFactorEnable" = "تفعيل المصادقة الثنائية"
"loginSecurityDesc" = "بيضيف طبقة مصادقة إضافية لزيادة الأمان." "twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."
"secretToken" = "توكن سري" "twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية"
"secretTokenDesc" = "احتفظ بالتوكن ده في مكان آمن. التوكن ده مطلوب لتسجيل الدخول ومش ممكن تسترجعه لو ضاع." "twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية"
"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:"
"twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق"
"twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق"
"twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية."
"twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح"
"twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح"
"twoFactorModalError" = "رمز خاطئ"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "تعديل الإعدادات" "modifySettings" = "تعديل الإعدادات"

View file

@ -51,7 +51,7 @@
"install" = "Install" "install" = "Install"
"clients" = "Clients" "clients" = "Clients"
"usage" = "Usage" "usage" = "Usage"
"secretToken" = "Secret Token" "twoFactorCode" = "Code"
"remained" = "Remained" "remained" = "Remained"
"security" = "Security" "security" = "Security"
"secAlertTitle" = "Security Alert" "secAlertTitle" = "Security Alert"
@ -87,7 +87,7 @@
"invalidFormData" = "The Input data format is invalid." "invalidFormData" = "The Input data format is invalid."
"emptyUsername" = "Username is required" "emptyUsername" = "Username is required"
"emptyPassword" = "Password is required" "emptyPassword" = "Password is required"
"wrongUsernameOrPassword" = "Invalid username or password or secret." "wrongUsernameOrPassword" = "Invalid username or password or two-factor code."
"successLogin" = "Login" "successLogin" = "Login"
[pages.index] [pages.index]
@ -501,11 +501,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Admin credentials" "admin" = "Admin credentials"
"secret" = "Secret Token" "twoFactor" = "Two-factor authentication"
"loginSecurity" = "Secure Login" "twoFactorEnable" = "Enable 2FA"
"loginSecurityDesc" = "Adds an additional layer of authentication to provide more security." "twoFactorEnableDesc" = "Adds an additional layer of authentication to provide more security."
"secretToken" = "Secret Token" "twoFactorModalSetTitle" = "Enable two-factor authentication"
"secretTokenDesc" = "Please securely store this token in a safe place. This token is required for login and cannot be recovered." "twoFactorModalDeleteTitle" = "Disable two-factor authentication"
"twoFactorModalSteps" = "To set up two-factor authentication, perform a few steps:"
"twoFactorModalFirstStep" = "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app"
"twoFactorModalSecondStep" = "2. Enter the code from the app"
"twoFactorModalRemoveStep" = "Enter the code from the application to remove two-factor authentication."
"twoFactorModalSetSuccess" = "Two-factor authentication has been successfully established"
"twoFactorModalDeleteSuccess" = "Two-factor authentication has been successfully deleted"
"twoFactorModalError" = "Wrong code"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Modify Settings" "modifySettings" = "Modify Settings"

View file

@ -51,7 +51,7 @@
"install" = "Instalar" "install" = "Instalar"
"clients" = "Clientes" "clients" = "Clientes"
"usage" = "Uso" "usage" = "Uso"
"secretToken" = "Token Secreto" "twoFactorCode" = "Código"
"remained" = "Restante" "remained" = "Restante"
"security" = "Seguridad" "security" = "Seguridad"
"secAlertTitle" = "Alerta de Seguridad" "secAlertTitle" = "Alerta de Seguridad"
@ -87,7 +87,7 @@
"invalidFormData" = "El formato de los datos de entrada es inválido." "invalidFormData" = "El formato de los datos de entrada es inválido."
"emptyUsername" = "Por favor ingresa el nombre de usuario." "emptyUsername" = "Por favor ingresa el nombre de usuario."
"emptyPassword" = "Por favor ingresa la contraseña." "emptyPassword" = "Por favor ingresa la contraseña."
"wrongUsernameOrPassword" = "Nombre de usuario o contraseña inválidos." "wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"successLogin" = "Inicio de Sesión Exitoso" "successLogin" = "Inicio de Sesión Exitoso"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Credenciales de administrador" "admin" = "Credenciales de administrador"
"secret" = "Token Secreto" "twoFactor" = "Autenticación de dos factores"
"loginSecurity" = "Seguridad de Inicio de Sesión" "twoFactorEnable" = "Habilitar 2FA"
"loginSecurityDesc" = "Habilitar un paso adicional de seguridad para el inicio de sesión de usuarios." "twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"secretToken" = "Token Secreto" "twoFactorModalSetTitle" = "Activar autenticación de dos factores"
"secretTokenDesc" = "Por favor, copia y guarda este token de forma segura en un lugar seguro. Este token es necesario para iniciar sesión y no se puede recuperar con la herramienta de comando x-ui." "twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
"twoFactorModalFirstStep" = "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación"
"twoFactorModalSecondStep" = "2. Ingresa el código de la aplicación"
"twoFactorModalRemoveStep" = "Ingresa el código de la aplicación para eliminar la autenticación de dos factores."
"twoFactorModalSetSuccess" = "La autenticación de dos factores se ha establecido con éxito"
"twoFactorModalDeleteSuccess" = "La autenticación de dos factores se ha eliminado con éxito"
"twoFactorModalError" = "Código incorrecto"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Modificar Configuraciones " "modifySettings" = "Modificar Configuraciones "

View file

@ -51,7 +51,7 @@
"install" = "نصب" "install" = "نصب"
"clients" = "کاربران" "clients" = "کاربران"
"usage" = "استفاده" "usage" = "استفاده"
"secretToken" = "توکن امنیتی" "twoFactorCode" = "کد"
"remained" = "باقی‌مانده" "remained" = "باقی‌مانده"
"security" = "امنیت" "security" = "امنیت"
"secAlertTitle" = "هشدار‌امنیتی" "secAlertTitle" = "هشدار‌امنیتی"
@ -87,7 +87,7 @@
"invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است" "invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است"
"emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌" "emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌"
"emptyPassword" = "لطفا یک رمزعبور وارد کنید" "emptyPassword" = "لطفا یک رمزعبور وارد کنید"
"wrongUsernameOrPassword" = "نام‌کاربری یا رمزعبور‌اشتباه‌است" "wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است."
"successLogin" = "ورود" "successLogin" = "ورود"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "اعتبارنامه‌های ادمین" "admin" = "اعتبارنامه‌های ادمین"
"secret" = "توکن مخفی" "twoFactor" = "احراز هویت دو مرحله‌ای"
"loginSecurity" = "ورود ایمن" "twoFactorEnable" = "فعال‌سازی 2FA"
"loginSecurityDesc" = "یک لایه اضافی از احراز هویت برای ایجاد امنیت بیشتر اضافه می کند" "twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند."
"secretToken" = "توکن مخفی" "twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای"
"secretTokenDesc" = "لطفاً این توکن را در مکانی امن ذخیره کنید. این توکن برای ورود به سیستم مورد نیاز است و قابل بازیابی نیست" "twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای"
"twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:"
"twoFactorModalFirstStep" = "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید"
"twoFactorModalSecondStep" = "2. کد را از برنامه وارد کنید"
"twoFactorModalRemoveStep" = "برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید."
"twoFactorModalSetSuccess" = "احراز هویت دو مرحله‌ای با موفقیت برقرار شد"
"twoFactorModalDeleteSuccess" = "احراز هویت دو مرحله‌ای با موفقیت حذف شد"
"twoFactorModalError" = "کد نادرست"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات" "modifySettings" = "ویرایش تنظیمات"

View file

@ -51,7 +51,7 @@
"install" = "Instal" "install" = "Instal"
"clients" = "Klien" "clients" = "Klien"
"usage" = "Penggunaan" "usage" = "Penggunaan"
"secretToken" = "Token Rahasia" "twoFactorCode" = "Kode"
"remained" = "Tersisa" "remained" = "Tersisa"
"security" = "Keamanan" "security" = "Keamanan"
"secAlertTitle" = "Peringatan keamanan" "secAlertTitle" = "Peringatan keamanan"
@ -87,7 +87,7 @@
"invalidFormData" = "Format data input tidak valid." "invalidFormData" = "Format data input tidak valid."
"emptyUsername" = "Nama Pengguna diperlukan" "emptyUsername" = "Nama Pengguna diperlukan"
"emptyPassword" = "Kata Sandi diperlukan" "emptyPassword" = "Kata Sandi diperlukan"
"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid." "wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."
"successLogin" = "Login berhasil" "successLogin" = "Login berhasil"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Kredensial admin" "admin" = "Kredensial admin"
"secret" = "Token Rahasia" "twoFactor" = "Autentikasi dua faktor"
"loginSecurity" = "Login Aman" "twoFactorEnable" = "Aktifkan 2FA"
"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih." "twoFactorEnableDesc" = "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih."
"secretToken" = "Token Rahasia" "twoFactorModalSetTitle" = "Aktifkan autentikasi dua faktor"
"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan." "twoFactorModalDeleteTitle" = "Nonaktifkan autentikasi dua faktor"
"twoFactorModalSteps" = "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:"
"twoFactorModalFirstStep" = "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi"
"twoFactorModalSecondStep" = "2. Masukkan kode dari aplikasi"
"twoFactorModalRemoveStep" = "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor."
"twoFactorModalSetSuccess" = "Autentikasi dua faktor telah berhasil dibuat"
"twoFactorModalDeleteSuccess" = "Autentikasi dua faktor telah berhasil dihapus"
"twoFactorModalError" = "Kode salah"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Ubah Pengaturan" "modifySettings" = "Ubah Pengaturan"

View file

@ -51,7 +51,7 @@
"install" = "インストール" "install" = "インストール"
"clients" = "クライアント" "clients" = "クライアント"
"usage" = "利用状況" "usage" = "利用状況"
"secretToken" = "シークレットトークン" "twoFactorCode" = "コード"
"remained" = "残り" "remained" = "残り"
"security" = "セキュリティ" "security" = "セキュリティ"
"secAlertTitle" = "セキュリティアラート" "secAlertTitle" = "セキュリティアラート"
@ -87,7 +87,7 @@
"invalidFormData" = "データ形式エラー" "invalidFormData" = "データ形式エラー"
"emptyUsername" = "ユーザー名を入力してください" "emptyUsername" = "ユーザー名を入力してください"
"emptyPassword" = "パスワードを入力してください" "emptyPassword" = "パスワードを入力してください"
"wrongUsernameOrPassword" = "ユーザー名またはパスワードが間違っています" "wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"
"successLogin" = "ログイン成功" "successLogin" = "ログイン成功"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理者の資格情報" "admin" = "管理者の資格情報"
"secret" = "セキュリティトークン" "twoFactor" = "二段階認証"
"loginSecurity" = "ログインセキュリティ" "twoFactorEnable" = "2FAを有効化"
"loginSecurityDesc" = "追加の認証を追加してセキュリティを向上させる" "twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"
"secretToken" = "セキュリティトークン" "twoFactorModalSetTitle" = "二段階認証を有効にする"
"secretTokenDesc" = "このトークンを安全な場所に保管してください。このトークンはログインに使用され、紛失すると回復できません。" "twoFactorModalDeleteTitle" = "二段階認証を無効にする"
"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:"
"twoFactorModalFirstStep" = "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます"
"twoFactorModalSecondStep" = "2. アプリからコードを入力してください"
"twoFactorModalRemoveStep" = "二段階認証を削除するには、アプリからコードを入力してください。"
"twoFactorModalSetSuccess" = "二要素認証が正常に設定されました"
"twoFactorModalDeleteSuccess" = "二要素認証が正常に削除されました"
"twoFactorModalError" = "コードが間違っています"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "設定を変更" "modifySettings" = "設定を変更"

View file

@ -51,7 +51,7 @@
"install" = "Instalar" "install" = "Instalar"
"clients" = "Clientes" "clients" = "Clientes"
"usage" = "Uso" "usage" = "Uso"
"secretToken" = "Token Secreto" "twoFactorCode" = "Código"
"remained" = "Restante" "remained" = "Restante"
"security" = "Segurança" "security" = "Segurança"
"secAlertTitle" = "Alerta de Segurança" "secAlertTitle" = "Alerta de Segurança"
@ -87,7 +87,7 @@
"invalidFormData" = "O formato dos dados de entrada é inválido." "invalidFormData" = "O formato dos dados de entrada é inválido."
"emptyUsername" = "Nome de usuário é obrigatório" "emptyUsername" = "Nome de usuário é obrigatório"
"emptyPassword" = "Senha é obrigatória" "emptyPassword" = "Senha é obrigatória"
"wrongUsernameOrPassword" = "Nome de usuário, senha ou segredo inválidos." "wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."
"successLogin" = "Login realizado com sucesso" "successLogin" = "Login realizado com sucesso"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Credenciais de administrador" "admin" = "Credenciais de administrador"
"secret" = "Token Secreto" "twoFactor" = "Autenticação de dois fatores"
"loginSecurity" = "Login Seguro" "twoFactorEnable" = "Ativar 2FA"
"loginSecurityDesc" = "Adiciona uma camada extra de autenticação para fornecer mais segurança." "twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."
"secretToken" = "Token Secreto" "twoFactorModalSetTitle" = "Ativar autenticação de dois fatores"
"secretTokenDesc" = "Por favor, armazene este token em um local seguro. Este token é necessário para o login e não pode ser recuperado." "twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores"
"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:"
"twoFactorModalFirstStep" = "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo"
"twoFactorModalSecondStep" = "2. Digite o código do aplicativo"
"twoFactorModalRemoveStep" = "Digite o código do aplicativo para remover a autenticação de dois fatores."
"twoFactorModalSetSuccess" = "A autenticação de dois fatores foi estabelecida com sucesso"
"twoFactorModalDeleteSuccess" = "A autenticação de dois fatores foi excluída com sucesso"
"twoFactorModalError" = "Código incorreto"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Modificar Configurações" "modifySettings" = "Modificar Configurações"

View file

@ -51,7 +51,7 @@
"install" = "Установка" "install" = "Установка"
"clients" = "Клиенты" "clients" = "Клиенты"
"usage" = "Использование" "usage" = "Использование"
"secretToken" = "Секретный токен" "twoFactorCode" = "Код"
"remained" = "Остаток" "remained" = "Остаток"
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности" "secAlertTitle" = "Предупреждение системы безопасности"
@ -87,7 +87,7 @@
"invalidFormData" = "Недопустимый формат данных" "invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или секретный токен." "wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или код двухфакторной аутентификации."
"successLogin" = "Успешный вход" "successLogin" = "Успешный вход"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Учетные данные администратора" "admin" = "Учетные данные администратора"
"secret" = "Секретный токен" "twoFactor" = "Двухфакторная аутентификация"
"loginSecurity" = "Безопасность входа" "twoFactorEnable" = "Включить 2FA"
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя" "twoFactorEnableDesc" = "Добавляет дополнительный уровень аутентификации для повышения безопасности."
"secretToken" = "Секретный токен" "twoFactorModalSetTitle" = "Включить двухфакторную аутентификацию"
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью инструмента x-ui" "twoFactorModalDeleteTitle" = "Отключить двухфакторную аутентификацию"
"twoFactorModalSteps" = "Для настройки двухфакторной аутентификации выполните несколько шагов:"
"twoFactorModalFirstStep" = "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение"
"twoFactorModalSecondStep" = "2. Введите код из приложения"
"twoFactorModalRemoveStep" = "Введите код из приложения, чтобы отключить двухфакторную аутентификацию."
"twoFactorModalSetSuccess" = "Двухфакторная аутентификация была успешно установлена"
"twoFactorModalDeleteSuccess" = "Двухфакторная аутентификация была успешно удалена"
"twoFactorModalError" = "Неверный код"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Настройки изменены" "modifySettings" = "Настройки изменены"

View file

@ -51,7 +51,7 @@
"install" = "Yükle" "install" = "Yükle"
"clients" = "Müşteriler" "clients" = "Müşteriler"
"usage" = "Kullanım" "usage" = "Kullanım"
"secretToken" = "Gizli Anahtar" "twoFactorCode" = "Kod"
"remained" = "Kalan" "remained" = "Kalan"
"security" = "Güvenlik" "security" = "Güvenlik"
"secAlertTitle" = "Güvenlik Uyarısı" "secAlertTitle" = "Güvenlik Uyarısı"
@ -87,7 +87,7 @@
"invalidFormData" = "Girdi verisi formatı geçersiz." "invalidFormData" = "Girdi verisi formatı geçersiz."
"emptyUsername" = "Kullanıcı adı gerekli" "emptyUsername" = "Kullanıcı adı gerekli"
"emptyPassword" = "Şifre gerekli" "emptyPassword" = "Şifre gerekli"
"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı veya şifre veya gizli anahtar." "wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."
"successLogin" = "Giriş Başarılı" "successLogin" = "Giriş Başarılı"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Yönetici kimlik bilgileri" "admin" = "Yönetici kimlik bilgileri"
"secret" = "Gizli Anahtar" "twoFactor" = "İki adımlı doğrulama"
"loginSecurity" = "Güvenli Giriş" "twoFactorEnable" = "2FA'yı Etkinleştir"
"loginSecurityDesc" = "Daha fazla güvenlik sağlamak için ek bir kimlik doğrulama katmanı ekler." "twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."
"secretToken" = "Gizli Anahtar" "twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir"
"secretTokenDesc" = "Bu anahtarı güvenli bir yerde saklayın. Bu anahtar giriş için gereklidir ve geri alınamaz." "twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak"
"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:"
"twoFactorModalFirstStep" = "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın"
"twoFactorModalSecondStep" = "2. Uygulamadaki kodu girin"
"twoFactorModalRemoveStep" = "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin."
"twoFactorModalSetSuccess" = "İki faktörlü kimlik doğrulama başarıyla kuruldu"
"twoFactorModalDeleteSuccess" = "İki faktörlü kimlik doğrulama başarıyla silindi"
"twoFactorModalError" = "Yanlış kod"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Ayarları Değiştir" "modifySettings" = "Ayarları Değiştir"

View file

@ -51,7 +51,7 @@
"install" = "Встановити" "install" = "Встановити"
"clients" = "Клієнти" "clients" = "Клієнти"
"usage" = "Використання" "usage" = "Використання"
"secretToken" = "Секретний маркер" "twoFactorCode" = "Код"
"remained" = "Залишилося" "remained" = "Залишилося"
"security" = "Беспека" "security" = "Беспека"
"secAlertTitle" = "Попередження системи безпеки" "secAlertTitle" = "Попередження системи безпеки"
@ -87,7 +87,7 @@
"invalidFormData" = "Формат вхідних даних недійсний." "invalidFormData" = "Формат вхідних даних недійсний."
"emptyUsername" = "Потрібне ім'я користувача" "emptyUsername" = "Потрібне ім'я користувача"
"emptyPassword" = "Потрібен пароль" "emptyPassword" = "Потрібен пароль"
"wrongUsernameOrPassword" = "Невірне ім'я користувача або пароль." "wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."
"successLogin" = "Вхід" "successLogin" = "Вхід"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Облікові дані адміністратора" "admin" = "Облікові дані адміністратора"
"secret" = "Секретний маркер" "twoFactor" = "Двофакторна аутентифікація"
"loginSecurity" = "Безпечний вхід" "twoFactorEnable" = "Увімкнути 2FA"
"loginSecurityDesc" = "Додає додатковий рівень автентифікації для забезпечення більшої безпеки." "twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."
"secretToken" = "Секретний маркер" "twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію"
"secretTokenDesc" = "Будь ласка, надійно зберігайте цей маркер у безпечному місці. Цей маркер потрібен для входу, і його неможливо відновити." "twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію"
"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:"
"twoFactorModalFirstStep" = "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму"
"twoFactorModalSecondStep" = "2. Введіть код з програми"
"twoFactorModalRemoveStep" = "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію."
"twoFactorModalSetSuccess" = "Двофакторна аутентифікація була успішно встановлена"
"twoFactorModalDeleteSuccess" = "Двофакторна аутентифікація була успішно видалена"
"twoFactorModalError" = "Невірний код"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Змінити налаштування" "modifySettings" = "Змінити налаштування"

View file

@ -51,7 +51,7 @@
"install" = "Cài đặt" "install" = "Cài đặt"
"clients" = "Các khách hàng" "clients" = "Các khách hàng"
"usage" = "Sử dụng" "usage" = "Sử dụng"
"secretToken" = "Mã bí mật" "twoFactorCode" = "Mã"
"remained" = "Còn lại" "remained" = "Còn lại"
"security" = "Bảo vệ" "security" = "Bảo vệ"
"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7" "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
@ -87,7 +87,7 @@
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ." "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng." "emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu." "emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng hoặc mật khẩu không đúng." "wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"successLogin" = "Đăng nhập thành công." "successLogin" = "Đăng nhập thành công."
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Thông tin đăng nhập quản trị viên" "admin" = "Thông tin đăng nhập quản trị viên"
"secret" = "Mã thông báo bí mật" "twoFactor" = "Xác thực hai yếu tố"
"loginSecurity" = "Bảo mật đăng nhập" "twoFactorEnable" = "Bật 2FA"
"loginSecurityDesc" = "Bật bước bảo mật đăng nhập bổ sung cho người dùng" "twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"secretToken" = "Mã bí mật" "twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
"secretTokenDesc" = "Vui lòng sao chép và lưu trữ mã này một cách an toàn ở nơi an toàn. Mã này cần thiết để đăng nhập và không thể phục hồi từ công cụ lệnh x-ui." "twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
"twoFactorModalFirstStep" = "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng"
"twoFactorModalSecondStep" = "2. Nhập mã từ ứng dụng"
"twoFactorModalRemoveStep" = "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố."
"twoFactorModalSetSuccess" = "Xác thực hai yếu tố đã được thiết lập thành công"
"twoFactorModalDeleteSuccess" = "Xác thực hai yếu tố đã được xóa thành công"
"twoFactorModalError" = "Mã sai"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "Chỉnh sửa cài đặt " "modifySettings" = "Chỉnh sửa cài đặt "

View file

@ -51,7 +51,7 @@
"install" = "安装" "install" = "安装"
"clients" = "客户端" "clients" = "客户端"
"usage" = "使用情况" "usage" = "使用情况"
"secretToken" = "安全密钥" "twoFactorCode" = "代码"
"remained" = "剩余" "remained" = "剩余"
"security" = "安全" "security" = "安全"
"secAlertTitle" = "安全警报" "secAlertTitle" = "安全警报"
@ -87,7 +87,7 @@
"invalidFormData" = "数据格式错误" "invalidFormData" = "数据格式错误"
"emptyUsername" = "请输入用户名" "emptyUsername" = "请输入用户名"
"emptyPassword" = "请输入密码" "emptyPassword" = "请输入密码"
"wrongUsernameOrPassword" = "用户名或密码错误" "wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"
"successLogin" = "登录" "successLogin" = "登录"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理员凭据" "admin" = "管理员凭据"
"secret" = "安全令牌" "twoFactor" = "双重验证"
"loginSecurity" = "登录安全" "twoFactorEnable" = "启用2FA"
"loginSecurityDesc" = "添加额外的身份验证以提高安全性" "twoFactorEnableDesc" = "增加额外的验证层以提高安全性。"
"secretToken" = "安全令牌" "twoFactorModalSetTitle" = "啟用雙重認證"
"secretTokenDesc" = "请将此令牌存储在安全的地方。此令牌用于登录,丢失无法恢复。" "twoFactorModalDeleteTitle" = "停用雙重認證"
"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼或複製QR碼附近的令牌並貼到應用程式中"
"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼"
"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。"
"twoFactorModalSetSuccess" = "双因素认证已成功建立"
"twoFactorModalDeleteSuccess" = "双因素认证已成功删除"
"twoFactorModalError" = "驗證碼錯誤"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "修改设置" "modifySettings" = "修改设置"

View file

@ -51,7 +51,7 @@
"install" = "安裝" "install" = "安裝"
"clients" = "客戶端" "clients" = "客戶端"
"usage" = "使用情況" "usage" = "使用情況"
"secretToken" = "安全金鑰" "twoFactorCode" = "代碼"
"remained" = "剩餘" "remained" = "剩餘"
"security" = "安全" "security" = "安全"
"secAlertTitle" = "安全警報" "secAlertTitle" = "安全警報"
@ -87,7 +87,7 @@
"invalidFormData" = "資料格式錯誤" "invalidFormData" = "資料格式錯誤"
"emptyUsername" = "請輸入使用者名稱" "emptyUsername" = "請輸入使用者名稱"
"emptyPassword" = "請輸入密碼" "emptyPassword" = "請輸入密碼"
"wrongUsernameOrPassword" = "使用者名稱或密碼錯誤" "wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"
"successLogin" = "登入" "successLogin" = "登入"
[pages.index] [pages.index]
@ -503,11 +503,18 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理員憑證" "admin" = "管理員憑證"
"secret" = "安全令牌" "twoFactor" = "雙重驗證"
"loginSecurity" = "登入安全" "twoFactorEnable" = "啟用2FA"
"loginSecurityDesc" = "新增額外的身份驗證以提高安全性" "twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"
"secretToken" = "安全令牌" "twoFactorModalSetTitle" = "啟用雙重認證"
"secretTokenDesc" = "請將此令牌儲存在安全的地方。此令牌用於登入,丟失無法恢復。" "twoFactorModalDeleteTitle" = "停用雙重認證"
"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼或複製QR碼附近的令牌並貼到應用程式中"
"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼"
"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。"
"twoFactorModalSetSuccess" = "雙重身份驗證已成功建立"
"twoFactorModalDeleteSuccess" = "雙重身份驗證已成功刪除"
"twoFactorModalError" = "驗證碼錯誤"
[pages.settings.toasts] [pages.settings.toasts]
"modifySettings" = "修改設定" "modifySettings" = "修改設定"

View file

@ -184,10 +184,8 @@ reset_user() {
read -rp "Please set the login password [default is a random password]: " config_password read -rp "Please set the login password [default is a random password]: " config_password
[[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8) [[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8)
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1 /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1
/usr/local/x-ui/x-ui setting -remove_secret >/dev/null 2>&1
echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}"
echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}"
echo -e "${yellow} Panel login secret token disabled ${plain}"
echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}"
confirm_restart confirm_restart
} }
@ -1731,7 +1729,7 @@ show_menu() {
${green}4.${plain} Legacy Version │ ${green}4.${plain} Legacy Version │
${green}5.${plain} Uninstall │ ${green}5.${plain} Uninstall │
│────────────────────────────────────────────────│ │────────────────────────────────────────────────│
${green}6.${plain} Reset Username & Password & Secret Token ${green}6.${plain} Reset Username & Password
${green}7.${plain} Reset Web Base Path │ ${green}7.${plain} Reset Web Base Path │
${green}8.${plain} Reset Settings │ ${green}8.${plain} Reset Settings │
${green}9.${plain} Change Port │ ${green}9.${plain} Change Port │