I had a similar requirement and didn’t want to have to expose the database URL to Next.JS or rely on a common password.
This is what I came up with:
File ./config/plugins.js
:
(I happen to use AWS SES to send the user’s new password but that’s obviously not a requirement)
module.exports = ({ env }) => ({
// ...
email: {
provider: 'amazon-ses',
providerOptions: {
key: env('AWS_SES_KEY'),
secret: env('AWS_SES_SECRET'),
amazon: 'https://email.us-east-1.amazonaws.com',
},
settings: {
defaultFrom: 'noreply@example.com',
defaultReplyTo: 'noreply@example.com',
},
},
// ...
});
File ./extensions/users-permissions/config/routes.json
:
{
"routes": [
{
"method": "POST",
"path": "/auth/passwordless",
"handler": "Passwordless.callback",
"config": {
"policies": ["plugins::users-permissions.ratelimit"],
"prefix": "",
"description": "Login a user using a passwordless authentication",
"tag": {
"plugin": "users-permissions",
"name": "User"
}
}
}
]
}
File ./extensions/users-permissions/controllers/Passwordless.js
:
"use strict";
/**
* Passwordless.js controller
*
* @description: A set of functions called "actions" to support passwordless auth.
*/
/* eslint-disable no-useless-escape */
const { sanitizeEntity } = require("strapi-utils");
const emailRegExp =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const formatError = (error) => [
{ messages: [{ id: error.id, message: error.message, field: error.field }] },
];
module.exports = {
async callback(ctx) {
const provider = "local";
const params = ctx.request.body;
// The identifier is required.
if (!params.identifier) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.provide",
message: "Please provide your username or your e-mail.",
})
);
}
const query = { provider };
// Check if the provided identifier is an email or not.
const isEmail = emailRegExp.test(params.identifier);
// Set the identifier to the appropriate query field.
if (isEmail) {
query.email = params.identifier.toLowerCase();
} else {
query.username = params.identifier;
}
// Check if the user exists.
const user = await strapi.query("user", "users-permissions").findOne(query);
if (!user) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.invalid",
message: "Identifier invalid.",
})
);
}
if (user.blocked === true) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.blocked",
message: "Your account has been blocked by an administrator",
})
);
}
const pluginStore = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions",
});
const settings = await pluginStore
.get({ key: "email" })
.then((storeEmail) => {
try {
return storeEmail["reset_password"].options;
} catch (error) {
return {};
}
});
const plainPassword = generateRandomPassword(6);
const password = await strapi.plugins[
"users-permissions"
].services.user.hashPassword({
password: plainPassword,
});
// Update the user.
await strapi
.query("user", "users-permissions")
.update({ id: user.id }, { resetPasswordToken: null, password });
const userInfo = sanitizeEntity(user, {
model: strapi.query("user", "users-permissions").model,
});
const displayPassword =
plainPassword.slice(0, 3) + "-" + plainPassword.slice(3, 6);
settings.message = await strapi.plugins[
"users-permissions"
].services.userspermissions.template(settings.message, {
CODE: displayPassword,
});
settings.object = await strapi.plugins[
"users-permissions"
].services.userspermissions.template(settings.object, {
CODE: displayPassword,
USER: userInfo,
});
try {
// Send an email to the user.
await strapi.plugins["email"].services.email.send({
to: user.email,
from:
settings.from.email || settings.from.name
? `${settings.from.name} <${settings.from.email}>`
: undefined,
replyTo: settings.response_email,
subject: settings.object,
text: settings.message,
html: settings.message,
});
} catch (err) {
return ctx.badRequest(null, err);
}
ctx.send({ status: "OK" });
},
};
function generateRandomPassword(length) {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var result = "";
for (var i = length; i > 0; --i)
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
Lastly, you need to make sure you allow access to the new route (/auth/passwordless
) through your admin panel ( Settings > Roles > Public > Users-Permissions > Passwordless > Callback).
Also, you want to update the Reset Password email template to something like this:
Subject: Your confirmation code: <%= CODE %>
<p>Your confirmation code is below — enter it in your open browser window and we'll help you get signed in.</p>
<%= CODE %>
<p>If you didn’t request this email, there’s nothing to worry about — you can safely ignore it.</p>
Testing:
curl -s \
-X POST \
-H "content-type:application/json" \
-d '{"identifier":"me@example.com"}' \
http://localhost:1337/auth/passwordless | jq .
(should send you an email with the new password)
curl -s \
-X POST \
-H "content-type:application/json" \
-d '{"identifier":"me@example.com","password":"PASSWORD FROM EMAIL"}' \
http://localhost:1337/auth/local | jq .