-
-
Save novabyte/36d3b24c567c0e6242ffbed85c197fef to your computer and use it in GitHub Desktop.
Nakama Email Password Recovery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Email Password Recovery module. | |
Using this module, you can request a password reset link from the game using 'recover_email_password' RPC. | |
--]] | |
local nk = require("nakama") | |
local HTTPS_PREFIX = "https://" | |
local MAILGUN_API_BASE_URL = "api.mailgun.net/v3" | |
--[[ | |
This function expects the following information to come from Runtime environment variables: | |
runtime: | |
env: | |
- "mailgun_domain=mg.yourdomainname.com" -- your domain as configured in mailgun | |
- "mailgun_api_key=your_mailgun_key" -- your mailgun key | |
- "[email protected]" -- will show up in the recovery email in the 'from' field | |
- "mailgun_recovery_email_subject=GAME_NAME Password Reset Link" -- the recovery email subject | |
- "mailgun_recovery_base_link=http://yourdomainname.com/reset_password.html" -- link to password reset webpage | |
- "mailgun_callback_ip=127.0.0.1" -- nakama ip address, if your nakama instance runs on your local machine use localhost | |
- "http_key=defaulthttpkey" -- duplicate your socket.http_key so that the password reset webpage could access your nakama instance | |
See mailgun quickstart for 'domain' and 'api_key' details: | |
https://documentation.mailgun.com/en/latest/quickstart-sending.html | |
Client must send through the following information: | |
{ | |
email = "" -- email to reset password for | |
} | |
The response object will be: | |
{ | |
"success": true | |
"result": {} | |
} | |
or in case of an error: | |
{ | |
"success": false | |
"error": "" | |
} | |
--]] | |
local function recover_email_password(context, payload) | |
local mailgun_domain = context.env["mailgun_domain"] | |
local mailgun_api_key = context.env["mailgun_api_key"] | |
local mailgun_from = context.env["mailgun_from"] | |
local subject = context.env["mailgun_recovery_email_subject"] | |
local recovery_base_link = context.env["mailgun_recovery_base_link"] | |
local callback_ip = context.env["mailgun_callback_ip"] | |
local http_key = context.env["http_key"] | |
local url = string.format("%sapi:%s@%s/%s/messages", HTTPS_PREFIX, mailgun_api_key, MAILGUN_API_BASE_URL, mailgun_domain) | |
local json_payload = nk.json_decode(payload) | |
local email = json_payload.email | |
local query = [[SELECT id FROM users WHERE email = $1 LIMIT 1]] | |
local query_result = nk.sql_query(query, { email }) | |
if next(query_result) == nil then | |
return nk.json_encode({ | |
["success"] = false, | |
["error"] = "Email does not exist!" | |
}) | |
end | |
local user_id = query_result[1].id | |
local reset_link_data = string.format("id=%s&key=%s&ip=%s", user_id, http_key, callback_ip) | |
local reset_link_data_encoded = nk.base64_encode(reset_link_data) | |
local reset_link_data_decoded = nk.base64_decode(reset_link_data_encoded) | |
local reset_link = string.format("%s?data=%s", recovery_base_link, reset_link_data_encoded) | |
local html = [[ | |
<table cellpadding=0 cellspacing=0 style=margin-left:auto;margin-right:auto><tr><td><table cellpadding=0 cellspacing=0><tr><td><table cellpadding=0 cellspacing=0 style=text-align:left;padding-bottom:88px;width:100%;padding-left:25px;padding-right:25px class=page-center><tr><td style=padding-top:72px;color:#000;font-size:48px;font-style:normal;font-weight:600;line-height:52px>Reset your password<tr><td style=color:#000;font-size:16px;line-height:24px;width:100%>You're receiving this e-mail because you requested a password reset for your account.<tr></tr><td style=padding-top:24px;color:#000;font-size:16px;line-height:24px;width:100%>Tap the button below to choose a new password.<tr><td> | |
<a href="]] .. reset_link .. [[" style=margin-top:36px;color:#fff;font-size:16px;font-weight:600;line-height:48px;width:220px;background-color:#0cf;border-radius:.5rem;margin-left:auto;margin-right:auto;display:block;text-decoration:none;font-style:normal;text-align:center target=_blank>RESET PASSWORD</a></table></table> | |
]] | |
local text = "Follow the link to reset your password:" .. reset_link | |
local content = string.format("from=%s&to=%s&subject=%s&text=%s&html=%s", mailgun_from, email, subject, text, html) | |
local method = "POST" | |
local headers = { | |
["Content-Type"] = "application/x-www-form-urlencoded", | |
} | |
local success, code, headers, body = pcall(nk.http_request, url, method, headers, content) | |
if (not success) then | |
nk.logger_error(string.format("Failed %q", code)) | |
return nk.json_encode({ | |
["success"] = false, | |
["error"] = code | |
}) | |
elseif (code >= 400) then | |
nk.logger_error(string.format("Failed %q %q", code, body)) | |
return nk.json_encode({ | |
["success"] = false, | |
["error"] = code, | |
["response"] = body | |
}) | |
else | |
nk.logger_info(string.format("Success %q %q", code, body)) | |
return nk.json_encode({ | |
["success"] = true, | |
["response"] = nk.json_decode(body) | |
}) | |
end | |
end | |
nk.register_rpc(recover_email_password, "recover_email_password") | |
--[[ | |
This function will be called from "mailgun_recovery_base_link" to update the password | |
--]] | |
local function reset_email_password(context, payload) | |
local json_payload = nk.json_decode(payload) | |
local user_id = json_payload.id | |
local new_password = json_payload.password | |
local update_query = [[UPDATE users SET password = $1 WHERE id = $2 LIMIT 1]] | |
local new_password_hash = nk.bcrypt_hash(new_password) | |
local exec_result = nk.sql_exec(update_query, { new_password_hash, user_id }) | |
if (exec_result == 1) then | |
return nk.json_encode({ | |
["success"] = true | |
}) | |
else | |
return nk.json_encode({ | |
["success"] = false | |
}) | |
end | |
end | |
nk.register_rpc(reset_email_password, "reset_email_password") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<style> | |
.body { | |
color: #212529; | |
background: rgba(0, 0, 0, 0.76); | |
padding-top: 4.2rem; | |
padding-bottom: 4.2rem; | |
} | |
.container { | |
display: block; | |
margin-left: auto; | |
margin-right: auto; | |
padding: 1rem; | |
width: 100%; | |
max-width: 310px; | |
background-color: #fff; | |
border: 1px solid rgba(0,0,0,.2); | |
border-radius: 1.1rem; | |
text-align: center; | |
font-family: Inter, system-ui; | |
} | |
.button { | |
background-color: #00ccff; | |
color: #fff; | |
width: 50%; | |
max-width: 150px; | |
font-size: 1rem; | |
font-weight: bold; | |
border-radius: 0.5rem; | |
padding: 2%; | |
line-height: 1.5; | |
border: none; | |
} | |
.password { | |
margin: auto; | |
display: block; | |
width: 100%; | |
max-width: 300px; | |
padding: .375rem .75rem; | |
font-size: 1rem; | |
line-height: 1.5; | |
color: #495057; | |
background-color: #fff; | |
border: 1px solid #ced4da; | |
border-radius: .25rem; | |
margin-top: .5rem; | |
margin-bottom: 1rem; | |
} | |
.passwordTitle { | |
font-size: 1rem; | |
margin-top: .5rem; | |
margin-bottom: -.2rem; | |
margin-left: .5rem; | |
text-align: left; | |
} | |
.correct { | |
display: none; | |
} | |
.incorrect { | |
font-size: 1rem; | |
display: block; | |
color: #b43c42; | |
text-align: center; | |
} | |
.success { | |
font-size: 1rem; | |
display: block; | |
color: #3cb460; | |
text-align: center; | |
} | |
.logo { | |
font-size: 1rem; | |
margin-top: 1rem; | |
margin-bottom: 2rem; | |
} | |
</style> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
<script src="reset_password.js"></script> | |
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"> | |
<body class="body"> | |
<div class="container"> | |
<div > | |
<h1>Reset Password</h1> | |
</div> | |
<div> | |
<div id="passwords"> | |
<div> | |
<p class="passwordTitle">New password</p> | |
<input id="password1" name="password1" class="password" type="password" placeholder="New Password" autocomplete="off"/> | |
</div> | |
<div> | |
<p class="passwordTitle">Confirm password</p> | |
<input id="password2" name="password2" class="password" type="password" placeholder="Repeat Password" autocomplete="off"/> | |
</div> | |
</div> | |
<div id="footer" class="correct"> | |
<p id="footerText"></p> | |
</div> | |
<div> | |
<button id="submitButton" class="button">SUBMIT</button> | |
</div> | |
</div> | |
</div> | |
</body> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$( document ).ready( function(){ | |
var data = getURLParameter( "data" ); | |
var data_decoded = atob( data ); | |
var userId = getParameter( data_decoded, "id" ); | |
var serverKey = getParameter( data_decoded, "key" ); | |
var serverIp = getParameter( data_decoded, "ip" ); | |
if( userId == null || serverKey == null || serverIp == null ){ | |
console.log( "'id' or/and 'key' or/and 'ip' query parameter(s) are missing!" ); | |
$( "#footer" ).removeClass( "correct" ).addClass( "incorrect" ); | |
$( "#footerText" ).html( "Please copy the link from your recovery email and try again." ); | |
hideForm(); | |
return; | |
} | |
$( "#submitButton" ).click( onSubmitClicked ); | |
$( "input" ).keyup( function(){ | |
checkPasswordsValidity(); | |
} ); | |
} ); | |
function onSubmitClicked(){ | |
var data = getURLParameter( "data" ); | |
var data_decoded = atob( data ); | |
var userId = getParameter( data_decoded, "id" ); | |
var serverKey = getParameter( data_decoded, "key" ); | |
var serverIp = getParameter( data_decoded, "ip" ); | |
var password = $( "#password2" ).val(); | |
var payload = JSON.stringify( { | |
"id": userId, | |
"password": password | |
} ); | |
$.ajax( { | |
url: 'http://' + serverIp + ':7350/v2/rpc/reset_email_password?http_key=' + serverKey + "&unwrap=true", | |
type: 'POST', | |
dataType: 'json', | |
data: payload, | |
contentType: "application/json", | |
success: function( data ){ | |
if( data.success ){ | |
$( "#footer" ).removeClass( "correct" ).addClass( "success" ); | |
$( "#footerText" ).html( "Your password has been updated!" ); | |
}else{ | |
$( "#footer" ).removeClass( "correct" ).addClass( "incorrect" ); | |
$( "#footerText" ).html( "Sorry there was an error... Please try again later..." ); | |
} | |
hideForm(); | |
}, | |
failure: function( data ){ | |
$( "#footer" ).removeClass( "correct" ).addClass( "incorrect" ); | |
$( "#footerText" ).html( "Sorry there was an error... Please try again later..." ); | |
hideForm(); | |
} | |
} ); | |
} | |
function getURLParameter( key ){ | |
return getParameter( window.location.search.substring( 1 ), key ); | |
} | |
function getParameter( data, key ){ | |
var urlVariables = data.split( '&' ); | |
for( var i = 0; i < urlVariables.length; i++ ){ | |
var parameters = urlVariables[ i ].split( '=' ); | |
if( parameters[ 0 ] == key ){ | |
return parameters[ 1 ]; | |
} | |
} | |
return null; | |
} | |
function hideForm(){ | |
var submitButton = document.getElementById( "submitButton" ); | |
submitButton.style = "display: none;"; | |
submitButton.disabled = true; | |
var passwords = document.getElementById( "passwords" ); | |
passwords.style = "display: none;"; | |
} | |
function setSubmitButtonDisabled( disabled ){ | |
var submitButton = document.getElementById( "submitButton" ); | |
submitButton.disabled = disabled; | |
} | |
function checkPasswordsValidity(){ | |
var passOne = $( "#password1" ).val(); | |
var passTwo = $( "#password2" ).val(); | |
if( $( "#footer" ).hasClass( "success" ) ){ | |
$( "#footer" ).removeClass( "success" ); | |
} | |
if( passOne == undefined || passOne.length < 8 ){ | |
if( $( "#footer" ).hasClass( "correct" ) ){ | |
$( "#footer" ).removeClass( "correct" ).addClass( "incorrect" ); | |
$( "#footerText" ).html( "Password should be at least 8 characters long." ); | |
}else{ | |
$( "#footerText" ).html( "Password should be at least 8 characters long." ); | |
} | |
setSubmitButtonDisabled( true ); | |
}else if( $( "#footer" ).hasClass( "incorrect" ) ){ | |
if( passOne == passTwo){ | |
$( "#footer" ).removeClass( "incorrect" ).addClass( "correct" ); | |
setSubmitButtonDisabled( false ); | |
}else{ | |
$( "#footerText" ).html( "Passwords don't match" ); | |
setSubmitButtonDisabled( true ); | |
} | |
}else{ | |
if( passOne != passTwo ){ | |
$( "#footer" ).removeClass( "correct" ).addClass( "incorrect" ); | |
$( "#footerText" ).html( "Passwords don't match" ); | |
setSubmitButtonDisabled( true ); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment