Skip to content

Instantly share code, notes, and snippets.

@nickmarty
Created February 12, 2021 22:35
Show Gist options
  • Save nickmarty/4d348121c164863610cae828bc1c7930 to your computer and use it in GitHub Desktop.
Save nickmarty/4d348121c164863610cae828bc1c7930 to your computer and use it in GitHub Desktop.
Nakama Email Password Recovery
--[[
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
- "mailgun_from=noreply@yourdomainname.com" -- 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")
<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>
$( 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