mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-10-03 07:46:18 +00:00
Finished forgot password for clients app
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -7,6 +9,8 @@ import os
|
|||||||
|
|
||||||
from modules.WireguardConfiguration import WireguardConfiguration
|
from modules.WireguardConfiguration import WireguardConfiguration
|
||||||
from modules.DashboardConfig import DashboardConfig
|
from modules.DashboardConfig import DashboardConfig
|
||||||
|
from modules.Email import EmailSender
|
||||||
|
|
||||||
|
|
||||||
def ResponseObject(status=True, message=None, data=None, status_code = 200) -> Flask.response_class:
|
def ResponseObject(status=True, message=None, data=None, status_code = 200) -> Flask.response_class:
|
||||||
response = Flask.make_response(current_app, {
|
response = Flask.make_response(current_app, {
|
||||||
@@ -46,7 +50,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
|
|
||||||
@client.post(f'{prefix}/api/signup')
|
@client.post(f'{prefix}/api/signup')
|
||||||
def ClientAPI_SignUp():
|
def ClientAPI_SignUp():
|
||||||
data = request.json
|
data = request.get_json()
|
||||||
status, msg = dashboardClients.SignUp(**data)
|
status, msg = dashboardClients.SignUp(**data)
|
||||||
return ResponseObject(status, msg)
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
if not oidc:
|
if not oidc:
|
||||||
return ResponseObject(status=False, message="OIDC is disabled")
|
return ResponseObject(status=False, message="OIDC is disabled")
|
||||||
|
|
||||||
data = request.json
|
data = request.get_json()
|
||||||
status, oidcData = dashboardClients.SignIn_OIDC(**data)
|
status, oidcData = dashboardClients.SignIn_OIDC(**data)
|
||||||
if not status:
|
if not status:
|
||||||
return ResponseObject(status, oidcData)
|
return ResponseObject(status, oidcData)
|
||||||
@@ -77,7 +81,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
|
|
||||||
@client.post(f'{prefix}/api/signin')
|
@client.post(f'{prefix}/api/signin')
|
||||||
def ClientAPI_SignIn():
|
def ClientAPI_SignIn():
|
||||||
data = request.json
|
data = request.get_json()
|
||||||
status, msg = dashboardClients.SignIn(**data)
|
status, msg = dashboardClients.SignIn(**data)
|
||||||
if status:
|
if status:
|
||||||
session['Email'] = data.get('Email')
|
session['Email'] = data.get('Email')
|
||||||
@@ -85,6 +89,70 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
session['TotpVerified'] = False
|
session['TotpVerified'] = False
|
||||||
return ResponseObject(status, msg)
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
|
@client.post(f'{prefix}/api/resetPassword/generateResetToken')
|
||||||
|
def ClientAPI_ResetPassword_GenerateResetToken():
|
||||||
|
date = datetime.datetime.now(tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
|
emailSender = EmailSender(dashboardConfig)
|
||||||
|
if not emailSender.ready():
|
||||||
|
return ResponseObject(False, "We can't send you an email due to your Administrator has not setup email service. Please contact your administrator.")
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
email = data.get('Email', None)
|
||||||
|
if not email:
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
u = dashboardClients.SignIn_UserExistence(email)
|
||||||
|
if not u:
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
token = dashboardClients.GenerateClientPasswordResetToken(u.get('ClientID'))
|
||||||
|
|
||||||
|
status, msg = emailSender.send(
|
||||||
|
email, "[WGDashboard | Client] Reset Password",
|
||||||
|
f"Hi {email}, \n\nIt looks like you're trying to reset your password at {date} \n\nEnter this 6 digits code on the Forgot Password to continue:\n\n{token}\n\nThis code will expire in 30 minutes for your security. If you didn’t request a password reset, you can safely ignore this email—your current password will remain unchanged.\n\nIf you need help, feel free to contact support.\n\nBest regards,\n\nWGDashboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
|
@client.post(f'{prefix}/api/resetPassword/validateResetToken')
|
||||||
|
def ClientAPI_ResetPassword_ValidateResetToken():
|
||||||
|
data = request.get_json()
|
||||||
|
email = data.get('Email', None)
|
||||||
|
token = data.get('Token', None)
|
||||||
|
if not all([email, token]):
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
u = dashboardClients.SignIn_UserExistence(email)
|
||||||
|
if not u:
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
return ResponseObject(status=dashboardClients.ValidateClientPasswordResetToken(u.get('ClientID'), token))
|
||||||
|
|
||||||
|
@client.post(f'{prefix}/api/resetPassword')
|
||||||
|
def ClientAPI_ResetPassword():
|
||||||
|
data = request.get_json()
|
||||||
|
email = data.get('Email', None)
|
||||||
|
token = data.get('Token', None)
|
||||||
|
password = data.get('Password', None)
|
||||||
|
confirmPassword = data.get('ConfirmPassword', None)
|
||||||
|
if not all([email, token, password, confirmPassword]):
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
u = dashboardClients.SignIn_UserExistence(email)
|
||||||
|
if not u:
|
||||||
|
return ResponseObject(False, "Please provide a valid Email")
|
||||||
|
|
||||||
|
if not dashboardClients.ValidateClientPasswordResetToken(u.get('ClientID'), token):
|
||||||
|
return ResponseObject(False, "Verification code is either invalid or expired")
|
||||||
|
|
||||||
|
status, msg = dashboardClients.ResetClientPassword(u.get('ClientID'), password, confirmPassword)
|
||||||
|
|
||||||
|
dashboardClients.RevokeClientPasswordResetToken(u.get('ClientID'), token)
|
||||||
|
|
||||||
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
|
|
||||||
@client.get(f'{prefix}/api/signout')
|
@client.get(f'{prefix}/api/signout')
|
||||||
def ClientAPI_SignOut():
|
def ClientAPI_SignOut():
|
||||||
if session.get("SignInMethod") == "OIDC":
|
if session.get("SignInMethod") == "OIDC":
|
||||||
@@ -103,7 +171,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
|
|
||||||
@client.post(f'{prefix}/api/signin/totp')
|
@client.post(f'{prefix}/api/signin/totp')
|
||||||
def ClientAPI_SignIn_ValidateTOTP():
|
def ClientAPI_SignIn_ValidateTOTP():
|
||||||
data = request.json
|
data = request.get_json()
|
||||||
token = data.get('Token', None)
|
token = data.get('Token', None)
|
||||||
userProvidedTotp = data.get('UserProvidedTOTP', None)
|
userProvidedTotp = data.get('UserProvidedTOTP', None)
|
||||||
if not all([token, userProvidedTotp]):
|
if not all([token, userProvidedTotp]):
|
||||||
@@ -154,7 +222,7 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
|||||||
@login_required
|
@login_required
|
||||||
def ClientAPI_Settings_UpdatePassword():
|
def ClientAPI_Settings_UpdatePassword():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
status, message = dashboardClients.UpdateClientPassword(session['Email'], **data)
|
status, message = dashboardClients.UpdateClientPassword(session['ClientID'], **data)
|
||||||
|
|
||||||
return ResponseObject(status, message)
|
return ResponseObject(status, message)
|
||||||
|
|
||||||
|
@@ -591,6 +591,14 @@ def API_deletePeers(configName: str) -> ResponseObject:
|
|||||||
return ResponseObject(False, "Please specify one or more peers", status_code=400)
|
return ResponseObject(False, "Please specify one or more peers", status_code=400)
|
||||||
configuration = WireguardConfigurations.get(configName)
|
configuration = WireguardConfigurations.get(configName)
|
||||||
status, msg = configuration.deletePeers(peers, AllPeerJobs, AllPeerShareLinks)
|
status, msg = configuration.deletePeers(peers, AllPeerJobs, AllPeerShareLinks)
|
||||||
|
|
||||||
|
# Delete Assignment
|
||||||
|
|
||||||
|
for p in peers:
|
||||||
|
assignments = DashboardClients.DashboardClientsPeerAssignment.GetAssignedClients(configName, p)
|
||||||
|
for c in assignments:
|
||||||
|
DashboardClients.DashboardClientsPeerAssignment.UnassignClients(c.AssignmentID)
|
||||||
|
|
||||||
return ResponseObject(status, msg)
|
return ResponseObject(status, msg)
|
||||||
|
|
||||||
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
||||||
@@ -1412,7 +1420,7 @@ def API_Clients_GeneratePasswordResetLink():
|
|||||||
if not DashboardClients.GetClient(clientId):
|
if not DashboardClients.GetClient(clientId):
|
||||||
return ResponseObject(False, "Client does not exist")
|
return ResponseObject(False, "Client does not exist")
|
||||||
|
|
||||||
token = DashboardClients.GenerateClientPasswordResetLink(clientId)
|
token = DashboardClients.GenerateClientPasswordResetToken(clientId)
|
||||||
if token:
|
if token:
|
||||||
return ResponseObject(data=token)
|
return ResponseObject(data=token)
|
||||||
return ResponseObject(False, "Failed to generate link")
|
return ResponseObject(False, "Failed to generate link")
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
@@ -305,11 +306,45 @@ class DashboardClients:
|
|||||||
def GetClientAssignedPeers(self, ClientID):
|
def GetClientAssignedPeers(self, ClientID):
|
||||||
return self.DashboardClientsPeerAssignment.GetAssignedPeers(ClientID)
|
return self.DashboardClientsPeerAssignment.GetAssignedPeers(ClientID)
|
||||||
|
|
||||||
def UpdateClientPassword(self, Email, CurrentPassword, NewPassword, ConfirmNewPassword):
|
def ResetClientPassword(self, ClientID, NewPassword, ConfirmNewPassword) -> tuple[bool, str] | tuple[bool, None]:
|
||||||
|
c = self.GetClient(ClientID)
|
||||||
|
if c is None:
|
||||||
|
return False, "Client does not exist"
|
||||||
|
|
||||||
|
if NewPassword != ConfirmNewPassword:
|
||||||
|
return False, "New passwords does not match"
|
||||||
|
|
||||||
|
pwStrength, msg = ValidatePasswordStrength(NewPassword)
|
||||||
|
if not pwStrength:
|
||||||
|
return pwStrength, msg
|
||||||
|
try:
|
||||||
|
with self.engine.begin() as conn:
|
||||||
|
conn.execute(
|
||||||
|
self.dashboardClientsTable.update().values({
|
||||||
|
"TotpKeyVerified": None,
|
||||||
|
"TotpKey": pyotp.random_base32(),
|
||||||
|
"Password": bcrypt.hashpw(NewPassword.encode('utf-8'), bcrypt.gensalt()).decode("utf-8"),
|
||||||
|
}).where(
|
||||||
|
self.dashboardClientsTable.c.ClientID == ClientID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.logger.log(Message=f"User {ClientID} reset password and TOTP")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.log(Status="false", Message=f"User {ClientID} reset password failed, reason: {str(e)}")
|
||||||
|
return False, "Reset password failed."
|
||||||
|
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
def UpdateClientPassword(self, ClientID, CurrentPassword, NewPassword, ConfirmNewPassword) -> tuple[bool, str] | tuple[bool, None]:
|
||||||
|
c = self.GetClient(ClientID)
|
||||||
|
if c is None:
|
||||||
|
return False, "Client does not exist"
|
||||||
|
|
||||||
if not all([CurrentPassword, NewPassword, ConfirmNewPassword]):
|
if not all([CurrentPassword, NewPassword, ConfirmNewPassword]):
|
||||||
return False, "Please fill in all fields"
|
return False, "Please fill in all fields"
|
||||||
|
|
||||||
if not self.SignIn_ValidatePassword(Email, CurrentPassword):
|
if not self.SignIn_ValidatePassword(c.get('Email'), CurrentPassword):
|
||||||
return False, "Current password does not match"
|
return False, "Current password does not match"
|
||||||
|
|
||||||
if NewPassword != ConfirmNewPassword:
|
if NewPassword != ConfirmNewPassword:
|
||||||
@@ -324,13 +359,13 @@ class DashboardClients:
|
|||||||
self.dashboardClientsTable.update().values({
|
self.dashboardClientsTable.update().values({
|
||||||
"Password": bcrypt.hashpw(NewPassword.encode('utf-8'), bcrypt.gensalt()).decode("utf-8"),
|
"Password": bcrypt.hashpw(NewPassword.encode('utf-8'), bcrypt.gensalt()).decode("utf-8"),
|
||||||
}).where(
|
}).where(
|
||||||
self.dashboardClientsTable.c.Email == Email
|
self.dashboardClientsTable.c.ClientID == ClientID
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.logger.log(Message=f"User {Email} updated password")
|
self.logger.log(Message=f"User {ClientID} updated password")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log(Status="false", Message=f"Signed up failed, reason: {str(e)}")
|
self.logger.log(Status="false", Message=f"User {ClientID} update password failed, reason: {str(e)}")
|
||||||
return False, "Signed up failed."
|
return False, "Update password failed."
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
def UpdateClientProfile(self, ClientID, Name):
|
def UpdateClientProfile(self, ClientID, Name):
|
||||||
@@ -381,16 +416,17 @@ class DashboardClients:
|
|||||||
For WGDashboard Admin to Manage Clients
|
For WGDashboard Admin to Manage Clients
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def GenerateClientPasswordResetLink(self, ClientID) -> bool | str:
|
def GenerateClientPasswordResetToken(self, ClientID) -> bool | str:
|
||||||
c = self.GetClient(ClientID)
|
c = self.GetClient(ClientID)
|
||||||
if c is None:
|
if c is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
newToken = str(uuid.uuid4())
|
newToken = str(random.randint(0, 999999)).zfill(6)
|
||||||
with self.engine.begin() as conn:
|
with self.engine.begin() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
self.dashboardClientsPasswordResetLinkTable.update().values({
|
self.dashboardClientsPasswordResetLinkTable.update().values({
|
||||||
"ExpiryDate": db.func.now()
|
"ExpiryDate": datetime.datetime.now()
|
||||||
|
|
||||||
}).where(
|
}).where(
|
||||||
db.and_(
|
db.and_(
|
||||||
self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID,
|
self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID,
|
||||||
@@ -402,13 +438,38 @@ class DashboardClients:
|
|||||||
self.dashboardClientsPasswordResetLinkTable.insert().values({
|
self.dashboardClientsPasswordResetLinkTable.insert().values({
|
||||||
"ResetToken": newToken,
|
"ResetToken": newToken,
|
||||||
"ClientID": ClientID,
|
"ClientID": ClientID,
|
||||||
|
"CreatedDate": datetime.datetime.now(),
|
||||||
"ExpiryDate": datetime.datetime.now() + datetime.timedelta(minutes=30)
|
"ExpiryDate": datetime.datetime.now() + datetime.timedelta(minutes=30)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return newToken
|
return newToken
|
||||||
|
|
||||||
|
def ValidateClientPasswordResetToken(self, ClientID, Token):
|
||||||
|
c = self.GetClient(ClientID)
|
||||||
|
if c is None:
|
||||||
|
return False
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
t = conn.execute(
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.select().where(
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID,
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.c.ResetToken == Token,
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.c.ExpiryDate > datetime.datetime.now()
|
||||||
|
)
|
||||||
|
).mappings().fetchone()
|
||||||
|
return t is not None
|
||||||
|
|
||||||
|
def RevokeClientPasswordResetToken(self, ClientID, Token):
|
||||||
|
with self.engine.begin() as conn:
|
||||||
|
conn.execute(
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.update().values({
|
||||||
|
"ExpiryDate": datetime.datetime.now()
|
||||||
|
}).where(
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.c.ClientID == ClientID,
|
||||||
|
self.dashboardClientsPasswordResetLinkTable.c.ResetToken == Token
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def GetAssignedPeerClients(self, ConfigurationName, PeerID):
|
def GetAssignedPeerClients(self, ConfigurationName, PeerID):
|
||||||
c = self.DashboardClientsPeerAssignment.GetAssignedClients(ConfigurationName, PeerID)
|
c = self.DashboardClientsPeerAssignment.GetAssignedClients(ConfigurationName, PeerID)
|
||||||
|
@@ -47,7 +47,7 @@ class DashboardConfig:
|
|||||||
"dashboard_sort": "status",
|
"dashboard_sort": "status",
|
||||||
"dashboard_theme": "dark",
|
"dashboard_theme": "dark",
|
||||||
"dashboard_api_key": "false",
|
"dashboard_api_key": "false",
|
||||||
"dashboard_language": "en"
|
"dashboard_language": "en-US"
|
||||||
},
|
},
|
||||||
"Peers": {
|
"Peers": {
|
||||||
"peer_global_DNS": "1.1.1.1",
|
"peer_global_DNS": "1.1.1.1",
|
||||||
|
@@ -124,7 +124,6 @@ class DashboardOIDC:
|
|||||||
for k in providers.keys():
|
for k in providers.keys():
|
||||||
if all([providers[k]['client_id'], providers[k]['client_secret'], providers[k]['issuer']]):
|
if all([providers[k]['client_id'], providers[k]['client_secret'], providers[k]['issuer']]):
|
||||||
try:
|
try:
|
||||||
print("Requesting " + f"{providers[k]['issuer'].strip('/')}/.well-known/openid-configuration")
|
|
||||||
oidc_config = requests.get(
|
oidc_config = requests.get(
|
||||||
f"{providers[k]['issuer'].strip('/')}/.well-known/openid-configuration",
|
f"{providers[k]['issuer'].strip('/')}/.well-known/openid-configuration",
|
||||||
timeout=3,
|
timeout=3,
|
||||||
@@ -136,8 +135,9 @@ class DashboardOIDC:
|
|||||||
'openid_configuration': oidc_config
|
'openid_configuration': oidc_config
|
||||||
}
|
}
|
||||||
self.provider_secret[k] = providers[k]['client_secret']
|
self.provider_secret[k] = providers[k]['client_secret']
|
||||||
|
current_app.logger.info(f"Registered OIDC Provider: {k}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error("Failed to request OIDC config for this provider: " + providers[k]['issuer'].strip('/'), exc_info=e)
|
current_app.logger.error(f"Failed to register OIDC config for {k}", exc_info=e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e))
|
current_app.logger.error('Read OIDC file failed. Reason: ' + str(e))
|
||||||
return False
|
return False
|
@@ -35,7 +35,7 @@ class EmailSender:
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
return all([self.Server(), self.Port(), self.Encryption(), self.Username(), self.Password(), self.SendFrom()])
|
return all([self.Server(), self.Port(), self.Encryption(), self.Username(), self.Password(), self.SendFrom()])
|
||||||
|
|
||||||
def send(self, receiver, subject, body, includeAttachment = False, attachmentName = ""):
|
def send(self, receiver, subject, body, includeAttachment = False, attachmentName = "") -> tuple[bool, str] | tuple[bool, None]:
|
||||||
if self.ready():
|
if self.ready():
|
||||||
try:
|
try:
|
||||||
self.smtp = smtplib.SMTP(self.Server(), port=int(self.Port()))
|
self.smtp = smtplib.SMTP(self.Server(), port=int(self.Port()))
|
||||||
|
@@ -7,7 +7,7 @@ import OidcBtn from "@/components/SignIn/oidc/oidcBtn.vue";
|
|||||||
const providerExist = ref(false)
|
const providerExist = ref(false)
|
||||||
const providers = ref(undefined)
|
const providers = ref(undefined)
|
||||||
const getProviders = await axiosGet("/api/signin/oidc/providers")
|
const getProviders = await axiosGet("/api/signin/oidc/providers")
|
||||||
if (getProviders){
|
if (getProviders && Object.keys(getProviders.data).length > 0){
|
||||||
providerExist.value = true;
|
providerExist.value = true;
|
||||||
providers.value = getProviders.data
|
providers.value = getProviders.data
|
||||||
console.log(providers.value)
|
console.log(providers.value)
|
||||||
|
@@ -46,7 +46,7 @@ if (route.query.Email){
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="display-4">Welcome back</h1>
|
<h1 class="display-4">Welcome</h1>
|
||||||
<p class="text-muted">Sign in to access your <strong>WGDashboard Client</strong> account</p>
|
<p class="text-muted">Sign in to access your <strong>WGDashboard Client</strong> account</p>
|
||||||
</div>
|
</div>
|
||||||
<Oidc></Oidc>
|
<Oidc></Oidc>
|
||||||
@@ -79,9 +79,11 @@ if (route.query.Email){
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<a href="#" class="text-body text-decoration-none ms-auto btn btn-sm rounded-3">
|
<RouterLink to="forgotPassword"
|
||||||
|
|
||||||
|
class="text-body text-decoration-none ms-auto btn btn-sm rounded-3">
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</a>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
:disabled="!formFilled || loading"
|
:disabled="!formFilled || loading"
|
||||||
|
@@ -6,6 +6,7 @@ import axios from "axios";
|
|||||||
import {axiosGet, requestURl} from "@/utilities/request.js";
|
import {axiosGet, requestURl} from "@/utilities/request.js";
|
||||||
import {clientStore} from "@/stores/clientStore.js";
|
import {clientStore} from "@/stores/clientStore.js";
|
||||||
import Settings from "@/views/settings.vue";
|
import Settings from "@/views/settings.vue";
|
||||||
|
import ForgotPassword from "@/views/forgotPassword.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
@@ -39,6 +40,11 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/signout',
|
path: '/signout',
|
||||||
name: "Sign Out"
|
name: "Sign Out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/forgotPassword',
|
||||||
|
name: "Forgot Password",
|
||||||
|
component: ForgotPassword
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
231
src/static/client/src/views/forgotPassword.vue
Normal file
231
src/static/client/src/views/forgotPassword.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {axiosPost, requestURl} from "@/utilities/request.js";
|
||||||
|
const email = ref("")
|
||||||
|
const loading = ref(false)
|
||||||
|
const verifyCode = ref(false)
|
||||||
|
import {clientStore} from "@/stores/clientStore.js";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
const store = clientStore()
|
||||||
|
const resendInterval = ref(undefined)
|
||||||
|
const resendCountdown = ref(120)
|
||||||
|
const requestToken = async (e) => {
|
||||||
|
if (e) e.preventDefault()
|
||||||
|
loading.value = true
|
||||||
|
const result = await axiosPost("/api/resetPassword/generateResetToken", {
|
||||||
|
Email: email.value
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
if (result.status){
|
||||||
|
verifyCode.value = true
|
||||||
|
resendCountdown.value = 120;
|
||||||
|
resendInterval.value = setInterval(() => {
|
||||||
|
resendCountdown.value --;
|
||||||
|
if (resendCountdown.value === 0) clearInterval(resendInterval.value)
|
||||||
|
}, 1000)
|
||||||
|
}else{
|
||||||
|
store.newNotification(result.message, "danger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = ref("")
|
||||||
|
const parseCode = () => {
|
||||||
|
code.value = code.value.replace(/\D/i, "")
|
||||||
|
code.value = code.value.slice(0, 6)
|
||||||
|
}
|
||||||
|
const codeReady = computed(() => {
|
||||||
|
return /^[0-9]{6}$/.test(code.value)
|
||||||
|
})
|
||||||
|
const codeValidated = ref(false)
|
||||||
|
const validateCode = async (e) => {
|
||||||
|
if (e) e.preventDefault()
|
||||||
|
loading.value = true
|
||||||
|
let codeValidation = await axiosPost("/api/resetPassword/validateResetToken", {
|
||||||
|
Email: email.value,
|
||||||
|
Token: code.value
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
if (codeValidation.status){
|
||||||
|
codeValidated.value = true
|
||||||
|
}else{
|
||||||
|
store.newNotification("Your verification code is either invalid or expired", "danger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = ref("")
|
||||||
|
const confirmPassword = ref("")
|
||||||
|
const passwordReady = computed(() => {
|
||||||
|
return password.value && confirmPassword.value && password.value === confirmPassword.value
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
const resetPassword = async (e) => {
|
||||||
|
if (e) e.preventDefault()
|
||||||
|
loading.value = true;
|
||||||
|
let reset = await axiosPost("/api/resetPassword", {
|
||||||
|
Email: email.value,
|
||||||
|
Token: code.value,
|
||||||
|
Password: password.value,
|
||||||
|
ConfirmPassword: confirmPassword.value
|
||||||
|
})
|
||||||
|
if (reset.status){
|
||||||
|
store.newNotification("Password reset! Now you can sign in with your new password", "success")
|
||||||
|
|
||||||
|
await router.push('/signin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-3 p-sm-5">
|
||||||
|
<Transition name="app" mode="out-in">
|
||||||
|
<div v-if="!verifyCode">
|
||||||
|
<RouterLink to="signin"
|
||||||
|
role="button"
|
||||||
|
class="btn btn-outline-body btn-sm rounded-3">
|
||||||
|
<i class="me-2 bi bi-chevron-left"></i> Back
|
||||||
|
</RouterLink>
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-4">No worries</h1>
|
||||||
|
<p class="text-muted">Enter the email address of your <strong>WGDashboard Client</strong> account below to receive a verification code</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-4 d-flex flex-column gap-3" @submit="e => requestToken(e)">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text"
|
||||||
|
required
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
autofocus
|
||||||
|
class="form-control rounded-3 border-0" id="email" placeholder="email">
|
||||||
|
<label for="email" class="d-flex">
|
||||||
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:disabled="!email || loading"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary rounded-3 btn-body px-3 py-2 fw-bold">
|
||||||
|
<span v-if="!loading" class="d-block">
|
||||||
|
Continue <i class="bi bi-arrow-right ms-2"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else class="d-block">
|
||||||
|
Loading...<i class="ms-2 spinner-border spinner-border-sm"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="verifyCode && !codeValidated">
|
||||||
|
<a role="button" class="text-decoration-none text-body" @click="verifyCode = false; code = ''">
|
||||||
|
<i class="me-2 bi bi-chevron-left"></i> Back
|
||||||
|
</a>
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-4">Almost there</h1>
|
||||||
|
<p class="text-muted">Enter the code you received below to retrieve a reset your password</p>
|
||||||
|
<p class="text-muted" v-if="resendCountdown > 0">Didn't get the code? Maybe check your Spam/Junk mailbox. You can get another code in {{ resendCountdown }} seconds.</p>
|
||||||
|
<a role="button"
|
||||||
|
:class="{disabled: loading}"
|
||||||
|
@click="requestToken()" v-else-if="resendCountdown === 0 && !loading">Resend</a>
|
||||||
|
</div>
|
||||||
|
<form class="mt-4 d-flex flex-column gap-3" @submit="e => validateCode(e)">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
required
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="code"
|
||||||
|
@keyup="parseCode()"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
autofocus
|
||||||
|
class="form-control rounded-3 border-0" id="token" placeholder="token">
|
||||||
|
<label for="email" class="d-flex">
|
||||||
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
|
6 Digits Verification Code
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:disabled="!codeReady || loading"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary rounded-3 btn-body px-3 py-2 fw-bold">
|
||||||
|
<span v-if="!loading" class="d-block">
|
||||||
|
Continue <i class="bi bi-arrow-right ms-2"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else class="d-block">
|
||||||
|
Loading...<i class="ms-2 spinner-border spinner-border-sm"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="verifyCode && codeValidated">
|
||||||
|
<a role="button" class="text-decoration-none text-body" @click="verifyCode = false; code = ''; codeValidated = false">
|
||||||
|
<i class="me-2 bi bi-chevron-left"></i> Back
|
||||||
|
</a>
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-4">Last step</h1>
|
||||||
|
<p class="text-muted">Enter your new password below</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-4 d-flex flex-column gap-3" @submit="(e) => resetPassword(e)">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="password"
|
||||||
|
required
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
autofocus
|
||||||
|
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 class="form-floating">
|
||||||
|
<input type="password"
|
||||||
|
required
|
||||||
|
:disabled="loading"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
name="confirm_password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
autofocus
|
||||||
|
class="form-control rounded-3" id="confirm_password" placeholder="confirm_password">
|
||||||
|
<label for="confirm_password" class="d-flex">
|
||||||
|
<i class="bi bi-key me-2"></i>
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div id="validationServer03Feedback" class="invalid-feedback">
|
||||||
|
Passwords does not match
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
:disabled="!passwordReady || loading"
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary rounded-3 btn-body px-3 py-2 fw-bold">
|
||||||
|
<span v-if="!loading" class="d-block">
|
||||||
|
Continue <i class="bi bi-arrow-right ms-2"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else class="d-block">
|
||||||
|
Loading...<i class="ms-2 spinner-border spinner-border-sm"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<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 btn btn-sm btn-outline-body rounded-3">
|
||||||
|
Sign Up
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
Reference in New Issue
Block a user