Sign In and TOTP is done

This commit is contained in:
Donald Zou 2025-06-03 03:02:06 +08:00
parent 0300c26952
commit 832513a7fc
13 changed files with 698 additions and 136 deletions

View File

@ -19,7 +19,7 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
def login_required(f):
@wraps(f)
def func(*args, **kwargs):
if session.get("username") is None or session.get("role") != "client":
if session.get("username") is None or session.get("totpVerified") is None or not session.get("totpVerified") or session.get("role") != "client":
return ResponseObject(False, "Unauthorized access.", data=None, status_code=401)
return f(*args, **kwargs)
return func
@ -47,12 +47,49 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
def ClientAPI_SignIn():
data = request.json
status, msg = DashboardClients.SignIn(**data)
if status:
session['username'] = data.get('Email')
session['role'] = 'client'
session['totpVerified'] = False
return ResponseObject(status, msg)
@client.get(f'{prefix}/api/signin/totp')
def ClientAPI_SignIn_TOTP():
token = request.args.get('Token', None)
if not token:
return ResponseObject(False, "Please provide TOTP token")
status, msg = DashboardClients.SignIn_GetTotp(token)
return ResponseObject(status, msg)
@client.post(f'{prefix}/api/signin/totp')
def ClientAPI_SignIn_ValidateTOTP():
data = request.json
token = data.get('Token', None)
userProvidedTotp = data.get('UserProvidedTOTP', None)
if not all([token, userProvidedTotp]):
return ResponseObject(False, "Please fill in all fields")
status, msg = DashboardClients.SignIn_GetTotp(token, userProvidedTotp)
if status:
if session.get('username') is None:
return ResponseObject(False, "Sign in status is invalid", status_code=401)
session['totpVerified'] = True
return ResponseObject(status, msg)
@client.get(prefix)
@login_required
def ClientIndex():
print(wireguardConfigurations.keys())
return render_template('client.html')
@client.get(f'{prefix}/api/validateAuthentication')
@login_required
def ClientAPI_ValidateAuthentication():
return ResponseObject(True)
@client.get(f'{prefix}/api/configurations')
@login_required
def ClientAPI_Configurations():
return ResponseObject(True, "Ping Pong!")
return client

View File

@ -70,10 +70,35 @@ class DashboardClients:
checkPwd = bcrypt.checkpw(Password.encode("utf-8"), existingClient.get("Password").encode("utf-8"))
if checkPwd:
return True, self.DashboardClientsTOTP.GenerateToken(existingClient.get("ClientID"))
return False, "Email or Password is incorrect"
def SignIn_GetTotp(self, Token: str, UserProvidedTotp: str = None) -> tuple[bool, str] or tuple[bool, None, str]:
status, data = self.DashboardClientsTOTP.GetTotp(Token)
if not status:
return False, "TOTP Token is invalid"
if UserProvidedTotp is None:
if data.get('TotpKeyVerified') is None:
return True, pyotp.totp.TOTP(data.get('TotpKey')).provisioning_uri(name=data.get('Email'),
issuer_name="WGDashboard Client")
else:
totpMatched = pyotp.TOTP(data.get('TotpKey')).verify(UserProvidedTotp)
if not totpMatched:
return False, "TOTP is does not match"
if data.get('TotpKeyVerified') is None:
with self.engine.begin() as conn:
conn.execute(
self.dashboardClientsTable.update().values({
'TotpKeyVerified': 1
}).where(
self.dashboardClientsTable.c.ClientID == data.get('ClientID')
)
)
return True, None
def SignUp(self, Email, Password, ConfirmPassword) -> tuple[bool, str] or tuple[bool, None]:
try:
if not all([Email, Password, ConfirmPassword]):

View File

@ -19,6 +19,8 @@ class DashboardClientsTOTP:
)
)
self.metadata.create_all(self.engine)
self.metadata.reflect(self.engine)
self.dashboardClientsTable = self.metadata.tables['DashboardClients']
def GenerateToken(self, ClientID) -> str:
token = hashlib.sha512(f"{ClientID}_{datetime.datetime.now()}_{uuid.uuid4()}".encode()).hexdigest()
@ -30,8 +32,6 @@ class DashboardClientsTOTP:
db.and_(self.dashboardClientsTOTPTable.c.ClientID == ClientID, self.dashboardClientsTOTPTable.c.ExpireTime > datetime.datetime.now())
)
)
conn.execute(
self.dashboardClientsTOTPTable.insert().values({
"Token": token,
@ -42,3 +42,28 @@ class DashboardClientsTOTP:
return token
def GetTotp(self, token: str) -> tuple[bool, dict] or tuple[bool, None]:
with self.engine.connect() as conn:
totp = conn.execute(
db.select(
self.dashboardClientsTable.c.ClientID,
self.dashboardClientsTable.c.Email,
self.dashboardClientsTable.c.TotpKey,
self.dashboardClientsTable.c.TotpKeyVerified,
).select_from(
self.dashboardClientsTOTPTable
).where(
db.and_(
self.dashboardClientsTOTPTable.c.Token == token,
self.dashboardClientsTOTPTable.c.ExpireTime > datetime.datetime.now()
)
).join(
self.dashboardClientsTable,
self.dashboardClientsTOTPTable.c.ClientID == self.dashboardClientsTable.c.ClientID
)
).mappings().fetchone()
if totp:
return True, dict(totp)
return False, None

View File

@ -13,6 +13,7 @@
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"pinia": "^3.0.2",
"qrcode": "^1.5.4",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
@ -1554,6 +1555,30 @@
"integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
@ -1677,6 +1702,15 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001720",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz",
@ -1698,6 +1732,35 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1777,6 +1840,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/default-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz",
@ -1829,6 +1901,12 @@
"node": ">=0.4.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1850,6 +1928,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@ -2032,6 +2116,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -2116,6 +2213,15 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -2270,6 +2376,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz",
@ -2416,6 +2531,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2562,6 +2689,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz",
@ -2575,6 +2738,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
@ -2638,6 +2810,15 @@
}
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.4",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.4.tgz",
@ -2688,6 +2869,38 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
@ -2757,6 +2970,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2826,6 +3045,32 @@
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
@ -3166,6 +3411,32 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
@ -3173,6 +3444,41 @@
"dev": true,
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yoctocolors": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.1.tgz",

View File

@ -14,6 +14,7 @@
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"pinia": "^3.0.2",
"qrcode": "^1.5.4",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1"

View File

@ -16,21 +16,10 @@ import NotificationList from "@/components/notification/notificationList.vue";
</Suspense>
</div>
</div>
<NotificationList></NotificationList>
</div>
</template>
<style scoped>
.app-enter-active,
.app-leave-active {
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
}
.app-enter-from,
.app-leave-to{
opacity: 0;
filter: blur(5px);
transform: scale(0.97);
}
</style>

View File

@ -73,3 +73,14 @@
.slide-right-leave-to{
transform: translateX(20px);
}
.app-enter-active,
.app-leave-active {
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
}
.app-enter-from,
.app-leave-to{
opacity: 0;
filter: blur(5px);
transform: scale(0.97);
}

View File

@ -0,0 +1,112 @@
<script setup>
import {computed, reactive, ref} from "vue";
import {clientStore} from "@/stores/clientStore.js";
import axios from "axios";
import {requestURl} from "@/utilities/request.js";
import {useRoute, useRouter} from "vue-router";
const loading = ref(false)
const formData = reactive({
Email: "",
Password: ""
});
const emits = defineEmits(['totpToken'])
const totpToken = ref("")
const store = clientStore()
const signIn = async (e) => {
e.preventDefault();
if (!formFilled){
store.newNotification("Please fill in all fields", "warning")
return;
}
loading.value = true;
await axios.post(requestURl("/client/api/signin"), formData).then(res => {
let data = res.data;
if (!data.status){
store.newNotification(data.message, "danger")
loading.value = false;
}else{
emits("totpToken", data.message)
}
})
}
const formFilled = computed(() => {
return Object.values(formData).find(x => !x) === undefined
})
// const router = useRouter()
const route = useRoute()
if (route.query.Email){
formData.Email = route.query.Email
}
</script>
<template>
<div>
<h1>Sign In</h1>
<p>to your WGDashboard Client account</p>
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
<div class="form-floating">
<input type="text"
required
:disabled="loading"
v-model="formData.Email"
name="email"
autocomplete="email"
autofocus
class="form-control rounded-3" id="email" placeholder="email">
<label for="email" class="d-flex">
<i class="bi bi-person-circle me-2"></i>
Email
</label>
</div>
<div class="form-floating">
<input type="password"
required
:disabled="loading"
v-model="formData.Password"
name="password"
autocomplete="current-password"
class="form-control rounded-3" id="password" placeholder="Password">
<label for="password" class="d-flex">
<i class="bi bi-key me-2"></i>
Password
</label>
</div>
<div>
<a href="#" class="text-body text-decoration-none ms-0">
Forgot Password?
</a>
</div>
<button
:disabled="!formFilled || loading"
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
<Transition name="slide-right" mode="out-in">
<span v-if="!loading" class="d-block">
Continue <i class="ms-2 bi bi-arrow-right"></i>
</span>
<span v-else class="d-block">
Loading...
<i class="spinner-border spinner-border-sm"></i>
</span>
</Transition>
</button>
</form>
<div>
<hr class="my-4">
<div class="d-flex align-items-center">
<span class="text-muted">
Don't have an account yet?
</span>
<RouterLink to="/signup" class="text-body text-decoration-none ms-auto fw-bold">
Sign Up
</RouterLink>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,136 @@
<script setup async>
import {computed, onMounted, reactive, ref} from "vue";
import axios from "axios";
import {requestURl} from "@/utilities/request.js";
import {useRouter} from "vue-router";
import {clientStore} from "@/stores/clientStore.js";
import QRCode from "qrcode";
const props = defineProps([
'totpToken'
])
const totpKey = ref("")
const formData = reactive({
TOTP: ""
})
const loading = ref(false)
const replace = () => {
formData.TOTP = formData.TOTP.replace(/\D/i, "")
}
const formFilled = computed(() => {
return /^[0-9]{6}$/.test(formData.TOTP)
})
const store = clientStore()
const router = useRouter()
await axios.get(requestURl('/client/api/signin/totp'), {
params: {
Token: props.totpToken
}
}).then(res => {
let data = res.data
if (data.status){
if (data.message){
totpKey.value = data.message
}
}else{
store.newNotification(data.message, "danger")
router.push('/signin')
}
})
const emits = defineEmits(['clearToken'])
onMounted(() => {
if (totpKey.value){
QRCode.toCanvas(document.getElementById('qrcode'), totpKey.value, function (error) {})
}
})
const verify = async (e) => {
e.preventDefault()
if (formFilled){
loading.value = true
await axios.post(requestURl('/client/api/signin/totp'), {
Token: props.totpToken,
UserProvidedTOTP: formData.TOTP
}).then(res => {
loading.value = false
let data = res.data
if (data.status){
router.push('/')
}else{
store.newNotification(data.message, "danger")
}
}).catch(() => {
store.newNotification("Sign in status is invalid", "danger")
emits('clearToken')
})
}
}
</script>
<template>
<form class="d-flex flex-column gap-3" @submit="e => verify(e)">
<div>
<a role="button" @click="emits('clearToken')">
<i class="me-2 bi bi-chevron-left"></i> Back
</a>
</div>
<div class="">
<h1 class="mb-3">Multi-Factor Authentication (MFA)</h1>
<div class="card rounded-3" v-if="totpKey">
<div class="card-body d-flex gap-3 flex-column">
<h2 class="mb-0">Initial Setup</h2>
<p class="mb-0">Please scan the following QR Code to generate TOTP with your choice of authenticator</p>
<canvas id="qrcode" class="rounded-3 shadow "></canvas>
<p class="mb-0">Or you can click the link below:</p>
<div class="card rounded-3 ">
<div class="card-body">
<a :href="totpKey">
{{ totpKey }}
</a>
</div>
</div>
<div class="alert alert-warning mb-0">
<strong>
Please note: You won't be able to see this QR Code again, so please save it somewhere safe in case you need to recover your TOTP key
</strong>
</div>
</div>
</div>
</div>
<hr v-if="totpKey">
<div class="d-flex flex-column gap-3">
<label for="totp">Enter the TOTP generated by your authenticator to verify</label>
<input class="form-control form-control-lg rounded-3 text-center"
id="totp"
:disabled="loading"
autofocus
@keyup="replace()"
maxlength="6" type="text" inputmode="numeric"
placeholder="- - - - - -"
autocomplete="one-time-code" v-model="formData.TOTP">
<button
:disabled="!formFilled || loading"
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
<Transition name="slide-right" mode="out-in">
<span v-if="!loading" class="d-block">
Continue <i class="ms-2 bi bi-arrow-right"></i>
</span>
<span v-else class="d-block">
<i class="spinner-border spinner-border-sm"></i>
</span>
</Transition>
</button>
</div>
</form>
</template>
<style scoped>
</style>

View File

@ -2,7 +2,9 @@ import {createWebHashHistory, createRouter} from "vue-router";
import Index from "@/views/index.vue";
import SignIn from "@/views/signin.vue";
import SignUp from "@/views/signup.vue";
import Totp from "@/views/totp.vue";
import axios from "axios";
import {requestURl} from "@/utilities/request.js";
import {clientStore} from "@/stores/clientStore.js";
const router = createRouter({
history: createWebHashHistory(),
@ -10,6 +12,9 @@ const router = createRouter({
{
path: '/',
component: Index,
meta: {
auth: true
},
name: "Home"
},
{
@ -21,15 +26,24 @@ const router = createRouter({
path: '/signup',
component: SignUp,
name: "Sign Up"
},
{
path: '/totp',
component: Totp,
name: "Verify TOTP"
}
]
})
router.beforeEach(async (to, from, next) => {
if (to.meta.auth){
await axios.get(requestURl('/client/api/validateAuthentication')).then(res => {
next()
}).catch(() => {
const store = clientStore()
store.newNotification("Sign in session ended, please sign in again", "warning")
next('/signin')
})
}else{
next()
}
})
router.afterEach((to, from, next) => {
document.title = to.name + ' | WGDashboard Client'
})

View File

@ -1,107 +1,23 @@
<script setup>
import {computed, reactive, ref} from "vue";
import {clientStore} from "@/stores/clientStore.js";
import axios from "axios";
import {requestURl} from "@/utilities/request.js";
import {useRoute, useRouter} from "vue-router";
const loading = ref(false)
const formData = reactive({
Email: "",
Password: ""
});
const store = clientStore()
const signIn = async (e) => {
e.preventDefault();
if (!formFilled){
store.newNotification("Please fill in all fields", "warning")
return;
}
loading.value = true;
await axios.post(requestURl("/client/api/signin"), formData).then(res => {
let data = res.data;
if (!data.status){
store.newNotification(data.message, "danger")
loading.value = false;
}else{
import SignInForm from "@/components/SignIn/signInForm.vue";
import {ref} from "vue";
import TotpForm from "@/components/SignIn/totpForm.vue";
}
})
}
const formFilled = computed(() => {
return Object.values(formData).find(x => !x) === undefined
})
// const router = useRouter()
const route = useRoute()
if (route.query.Email){
formData.Email = route.query.Email
}
const checkTotp = ref("")
</script>
<template>
<div>
<h1>Sign In</h1>
<p>to your WGDashboard Client account</p>
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
<div class="form-floating">
<input type="text"
required
:disabled="loading"
v-model="formData.Email"
name="email"
autocomplete="email"
autofocus
class="form-control rounded-3" id="email" placeholder="email">
<label for="email" class="d-flex">
<i class="bi bi-person-circle me-2"></i>
Email
</label>
</div>
<div class="form-floating">
<input type="password"
required
:disabled="loading"
v-model="formData.Password"
name="password"
autocomplete="current-password"
class="form-control rounded-3" id="password" placeholder="Password">
<label for="password" class="d-flex">
<i class="bi bi-key me-2"></i>
Password
</label>
</div>
<div>
<a href="#" class="text-body text-decoration-none ms-0">
Forgot Password?
</a>
</div>
<button
:disabled="!formFilled"
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
<Transition name="slide-right" mode="out-in">
<span v-if="!loading" class="d-block">
Continue <i class="ms-2 bi bi-arrow-right"></i>
</span>
<span v-else class="d-block">
<i class="spinner-border spinner-border-sm"></i>
</span>
<Transition name="app" mode="out-in">
<SignInForm
@totpToken="token => { checkTotp = token }"
v-if="!checkTotp"></SignInForm>
<TotpForm
@clearToken="checkTotp = ''"
:totp-token="checkTotp"
v-else></TotpForm>
</Transition>
</button>
</form>
<div>
<hr class="my-4">
<div class="d-flex align-items-center">
<span class="text-muted">
Don't have an account yet?
</span>
<RouterLink to="/signup" class="text-body text-decoration-none ms-auto fw-bold">
Sign Up
</RouterLink>
</div>
</div>
</div>
</template>

View File

@ -127,6 +127,7 @@ onMounted(() => {
Continue <i class="ms-2 bi bi-arrow-right"></i>
</span>
<span v-else class="d-block">
Loading...
<i class="spinner-border spinner-border-sm"></i>
</span>
</Transition>

View File

@ -1,11 +0,0 @@
<script setup>
</script>
<template>
</template>
<style scoped>
</style>