mirror of https://github.com/Budibase/budibase.git
60 changed files with 4268 additions and 374 deletions
@ -1,5 +1,16 @@ |
|||
function isTest() { |
|||
return ( |
|||
process.env.NODE_ENV === "jest" || |
|||
process.env.NODE_ENV === "cypress" || |
|||
process.env.JEST_WORKER_ID != null |
|||
) |
|||
} |
|||
|
|||
module.exports = { |
|||
JWT_SECRET: process.env.JWT_SECRET, |
|||
COUCH_DB_URL: process.env.COUCH_DB_URL, |
|||
SALT_ROUNDS: process.env.SALT_ROUNDS, |
|||
REDIS_URL: process.env.REDIS_URL, |
|||
REDIS_PASSWORD: process.env.REDIS_PASSWORD, |
|||
isTest, |
|||
} |
|||
|
|||
@ -0,0 +1,152 @@ |
|||
const env = require("../environment") |
|||
// ioredis mock is all in memory
|
|||
const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") |
|||
const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils") |
|||
|
|||
const CLUSTERED = false |
|||
|
|||
// for testing just generate the client once
|
|||
let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null |
|||
|
|||
/** |
|||
* Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise |
|||
* will return the ioredis client which will be ready to use. |
|||
* @return {Promise<object>} The ioredis client. |
|||
*/ |
|||
function init() { |
|||
return new Promise((resolve, reject) => { |
|||
// testing uses a single in memory client
|
|||
if (env.isTest()) { |
|||
return resolve(CLIENT) |
|||
} |
|||
// if a connection existed, close it and re-create it
|
|||
if (CLIENT) { |
|||
CLIENT.disconnect() |
|||
CLIENT = null |
|||
} |
|||
const { opts, host, port } = getRedisOptions(CLUSTERED) |
|||
if (CLUSTERED) { |
|||
CLIENT = new Redis.Cluster([{ host, port }], opts) |
|||
} else { |
|||
CLIENT = new Redis(opts) |
|||
} |
|||
CLIENT.on("end", err => { |
|||
reject(err) |
|||
}) |
|||
CLIENT.on("error", err => { |
|||
reject(err) |
|||
}) |
|||
CLIENT.on("connect", () => { |
|||
resolve(CLIENT) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Utility function, takes a redis stream and converts it to a promisified response - |
|||
* this can only be done with redis streams because they will have an end. |
|||
* @param stream A redis stream, specifically as this type of stream will have an end. |
|||
* @return {Promise<object>} The final output of the stream |
|||
*/ |
|||
function promisifyStream(stream) { |
|||
return new Promise((resolve, reject) => { |
|||
const outputKeys = new Set() |
|||
stream.on("data", keys => { |
|||
keys.forEach(key => { |
|||
outputKeys.add(key) |
|||
}) |
|||
}) |
|||
stream.on("error", err => { |
|||
reject(err) |
|||
}) |
|||
stream.on("end", async () => { |
|||
const keysArray = Array.from(outputKeys) |
|||
try { |
|||
let getPromises = [] |
|||
for (let key of keysArray) { |
|||
getPromises.push(CLIENT.get(key)) |
|||
} |
|||
const jsonArray = await Promise.all(getPromises) |
|||
resolve( |
|||
keysArray.map(key => ({ |
|||
key: removeDbPrefix(key), |
|||
value: JSON.parse(jsonArray.shift()), |
|||
})) |
|||
) |
|||
} catch (err) { |
|||
reject(err) |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
class RedisWrapper { |
|||
constructor(db) { |
|||
this._db = db |
|||
} |
|||
|
|||
async init() { |
|||
this._client = await init() |
|||
return this |
|||
} |
|||
|
|||
async finish() { |
|||
this._client.disconnect() |
|||
} |
|||
|
|||
async scan() { |
|||
const db = this._db, |
|||
client = this._client |
|||
let stream |
|||
if (CLUSTERED) { |
|||
let node = client.nodes("master") |
|||
stream = node[0].scanStream({ match: db + "-*", count: 100 }) |
|||
} else { |
|||
stream = client.scanStream({ match: db + "-*", count: 100 }) |
|||
} |
|||
return promisifyStream(stream) |
|||
} |
|||
|
|||
async get(key) { |
|||
const db = this._db, |
|||
client = this._client |
|||
let response = await client.get(addDbPrefix(db, key)) |
|||
// overwrite the prefixed key
|
|||
if (response != null && response.key) { |
|||
response.key = key |
|||
} |
|||
// if its not an object just return the response
|
|||
try { |
|||
return JSON.parse(response) |
|||
} catch (err) { |
|||
return response |
|||
} |
|||
} |
|||
|
|||
async store(key, value, expirySeconds = null) { |
|||
const db = this._db, |
|||
client = this._client |
|||
if (typeof value === "object") { |
|||
value = JSON.stringify(value) |
|||
} |
|||
const prefixedKey = addDbPrefix(db, key) |
|||
await client.set(prefixedKey, value) |
|||
if (expirySeconds) { |
|||
await client.expire(prefixedKey, expirySeconds) |
|||
} |
|||
} |
|||
|
|||
async delete(key) { |
|||
const db = this._db, |
|||
client = this._client |
|||
await client.del(addDbPrefix(db, key)) |
|||
} |
|||
|
|||
async clear() { |
|||
const db = this._db |
|||
let items = await this.scan(db) |
|||
await Promise.all(items.map(obj => this.delete(db, obj.key))) |
|||
} |
|||
} |
|||
|
|||
module.exports = RedisWrapper |
|||
@ -0,0 +1,46 @@ |
|||
const env = require("../environment") |
|||
|
|||
const SLOT_REFRESH_MS = 2000 |
|||
const CONNECT_TIMEOUT_MS = 10000 |
|||
const SEPARATOR = "-" |
|||
const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL |
|||
const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD |
|||
|
|||
exports.Databases = { |
|||
PW_RESETS: "pwReset", |
|||
INVITATIONS: "invitation", |
|||
} |
|||
|
|||
exports.getRedisOptions = (clustered = false) => { |
|||
const [host, port] = REDIS_URL.split(":") |
|||
const opts = { |
|||
connectTimeout: CONNECT_TIMEOUT_MS, |
|||
} |
|||
if (clustered) { |
|||
opts.redisOptions = {} |
|||
opts.redisOptions.tls = {} |
|||
opts.redisOptions.password = REDIS_PASSWORD |
|||
opts.slotsRefreshTimeout = SLOT_REFRESH_MS |
|||
opts.dnsLookup = (address, callback) => callback(null, address) |
|||
} else { |
|||
opts.host = host |
|||
opts.port = port |
|||
opts.password = REDIS_PASSWORD |
|||
} |
|||
return { opts, host, port } |
|||
} |
|||
|
|||
exports.addDbPrefix = (db, key) => { |
|||
return `${db}${SEPARATOR}${key}` |
|||
} |
|||
|
|||
exports.removeDbPrefix = key => { |
|||
let parts = key.split(SEPARATOR) |
|||
if (parts.length >= 2) { |
|||
parts.shift() |
|||
return parts.join(SEPARATOR) |
|||
} else { |
|||
// return the only part
|
|||
return parts[0] |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<script> |
|||
export let wide = false |
|||
</script> |
|||
|
|||
<div class:wide> |
|||
<slot /> |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
display: grid; |
|||
grid-template-columns: 1fr; |
|||
max-width: 80ch; |
|||
margin: 0 auto; |
|||
padding: calc(var(--spacing-xl) * 2); |
|||
} |
|||
|
|||
.wide { |
|||
max-width: none; |
|||
margin: 0; |
|||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import { Page } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<Page wide> |
|||
<slot /> |
|||
</Page> |
|||
@ -0,0 +1,27 @@ |
|||
<script> |
|||
import { Heading, Layout } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<Layout noPadding> |
|||
<div> |
|||
<Heading>Apps</Heading> |
|||
</div> |
|||
<div class="appList"> |
|||
{#each new Array(10) as _} |
|||
<div class="app" /> |
|||
{/each} |
|||
</div> |
|||
</Layout> |
|||
|
|||
<style> |
|||
.appList { |
|||
display: grid; |
|||
grid-gap: 50px; |
|||
grid-template-columns: repeat(auto-fill, 300px); |
|||
} |
|||
.app { |
|||
height: 130px; |
|||
border-radius: 4px; |
|||
background-color: var(--spectrum-global-color-gray-200); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,4 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
$goto("./apps") |
|||
</script> |
|||
@ -0,0 +1,7 @@ |
|||
<script> |
|||
import { Page } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<Page> |
|||
<slot /> |
|||
</Page> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,5 @@ |
|||
const env = require("../src/environment") |
|||
|
|||
env._set("NODE_ENV", "jest") |
|||
env._set("JWT_SECRET", "test-jwtsecret") |
|||
env._set("LOG_LEVEL", "silent") |
|||
@ -0,0 +1,125 @@ |
|||
const authPkg = require("@budibase/auth") |
|||
const { google } = require("@budibase/auth/src/middleware") |
|||
const { Configs, EmailTemplatePurpose } = require("../../../constants") |
|||
const CouchDB = require("../../../db") |
|||
const { sendEmail, isEmailConfigured } = require("../../../utilities/email") |
|||
const { clearCookie, getGlobalUserByEmail, hash } = authPkg.utils |
|||
const { Cookies } = authPkg.constants |
|||
const { passport } = authPkg.auth |
|||
const { checkResetPasswordCode } = require("../../../utilities/redis") |
|||
|
|||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name |
|||
|
|||
function authInternal(ctx, user, err = null) { |
|||
if (err) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
const expires = new Date() |
|||
expires.setDate(expires.getDate() + 1) |
|||
|
|||
if (!user) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
ctx.cookies.set(Cookies.Auth, user.token, { |
|||
expires, |
|||
path: "/", |
|||
httpOnly: false, |
|||
overwrite: true, |
|||
}) |
|||
} |
|||
|
|||
exports.authenticate = async (ctx, next) => { |
|||
return passport.authenticate("local", async (err, user) => { |
|||
authInternal(ctx, user, err) |
|||
|
|||
delete user.token |
|||
|
|||
ctx.body = { user } |
|||
})(ctx, next) |
|||
} |
|||
|
|||
/** |
|||
* Reset the user password, used as part of a forgotten password flow. |
|||
*/ |
|||
exports.reset = async ctx => { |
|||
const { email } = ctx.request.body |
|||
const configured = await isEmailConfigured() |
|||
if (!configured) { |
|||
ctx.throw( |
|||
400, |
|||
"Please contact your platform administrator, SMTP is not configured." |
|||
) |
|||
} |
|||
try { |
|||
const user = await getGlobalUserByEmail(email) |
|||
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user }) |
|||
} catch (err) { |
|||
// don't throw any kind of error to the user, this might give away something
|
|||
} |
|||
ctx.body = { |
|||
message: "Please check your email for a reset link.", |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Perform the user password update if the provided reset code is valid. |
|||
*/ |
|||
exports.resetUpdate = async ctx => { |
|||
const { resetCode, password } = ctx.request.body |
|||
try { |
|||
const userId = await checkResetPasswordCode(resetCode) |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
const user = await db.get(userId) |
|||
user.password = await hash(password) |
|||
await db.put(user) |
|||
ctx.body = { |
|||
message: "password reset successfully.", |
|||
} |
|||
} catch (err) { |
|||
ctx.throw(400, "Cannot reset password.") |
|||
} |
|||
} |
|||
|
|||
exports.logout = async ctx => { |
|||
clearCookie(ctx, Cookies.Auth) |
|||
ctx.body = { message: "User logged out." } |
|||
} |
|||
|
|||
/** |
|||
* The initial call that google authentication makes to take you to the google login screen. |
|||
* On a successful login, you will be redirected to the googleAuth callback route. |
|||
*/ |
|||
exports.googlePreAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
const config = await authPkg.db.getScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate(strategy, { |
|||
scope: ["profile", "email"], |
|||
})(ctx, next) |
|||
} |
|||
|
|||
exports.googleAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
|
|||
const config = await authPkg.db.getScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate( |
|||
strategy, |
|||
{ successRedirect: "/", failureRedirect: "/error" }, |
|||
async (err, user) => { |
|||
authInternal(ctx, user, err) |
|||
|
|||
ctx.redirect("/") |
|||
} |
|||
)(ctx, next) |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
const authPkg = require("@budibase/auth") |
|||
const { google } = require("@budibase/auth/src/middleware") |
|||
const { Configs } = require("../../constants") |
|||
const CouchDB = require("../../db") |
|||
const { clearCookie } = authPkg.utils |
|||
const { Cookies } = authPkg.constants |
|||
const { passport } = authPkg.auth |
|||
|
|||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name |
|||
|
|||
exports.authenticate = async (ctx, next) => { |
|||
return passport.authenticate("local", async (err, user) => { |
|||
if (err) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
const expires = new Date() |
|||
expires.setDate(expires.getDate() + 1) |
|||
|
|||
if (!user) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
ctx.cookies.set(Cookies.Auth, user.token, { |
|||
expires, |
|||
path: "/", |
|||
httpOnly: false, |
|||
overwrite: true, |
|||
}) |
|||
|
|||
delete user.token |
|||
|
|||
ctx.body = { user } |
|||
})(ctx, next) |
|||
} |
|||
|
|||
exports.logout = async ctx => { |
|||
clearCookie(ctx, Cookies.Auth) |
|||
ctx.body = { message: "User logged out" } |
|||
} |
|||
|
|||
/** |
|||
* The initial call that google authentication makes to take you to the google login screen. |
|||
* On a successful login, you will be redirected to the googleAuth callback route. |
|||
*/ |
|||
exports.googlePreAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
const config = await authPkg.db.determineScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate(strategy, { |
|||
scope: ["profile", "email"], |
|||
})(ctx, next) |
|||
} |
|||
|
|||
exports.googleAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
|
|||
const config = await authPkg.db.determineScopedConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate( |
|||
strategy, |
|||
{ successRedirect: "/", failureRedirect: "/error" }, |
|||
async (err, user) => { |
|||
if (err) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
const expires = new Date() |
|||
expires.setDate(expires.getDate() + 1) |
|||
|
|||
if (!user) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
ctx.cookies.set(Cookies.Auth, user.token, { |
|||
expires, |
|||
path: "/", |
|||
httpOnly: false, |
|||
overwrite: true, |
|||
}) |
|||
|
|||
ctx.redirect("/") |
|||
} |
|||
)(ctx, next) |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
const Router = require("@koa/router") |
|||
const authController = require("../../controllers/admin/auth") |
|||
const joiValidator = require("../../../middleware/joi-validator") |
|||
const Joi = require("joi") |
|||
|
|||
const router = Router() |
|||
|
|||
function buildAuthValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
username: Joi.string().required(), |
|||
password: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
function buildResetValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
email: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
function buildResetUpdateValidation() { |
|||
// prettier-ignore
|
|||
return joiValidator.body(Joi.object({ |
|||
resetCode: Joi.string().required(), |
|||
password: Joi.string().required(), |
|||
}).required().unknown(false)) |
|||
} |
|||
|
|||
router |
|||
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate) |
|||
.post("/api/admin/auth/reset", buildResetValidation(), authController.reset) |
|||
.post( |
|||
"/api/admin/auth/reset/update", |
|||
buildResetUpdateValidation(), |
|||
authController.resetUpdate |
|||
) |
|||
.post("/api/admin/auth/logout", authController.logout) |
|||
.get("/api/admin/auth/google", authController.googlePreAuth) |
|||
.get("/api/admin/auth/google/callback", authController.googleAuth) |
|||
|
|||
module.exports = router |
|||
@ -1,12 +0,0 @@ |
|||
const Router = require("@koa/router") |
|||
const authController = require("../controllers/auth") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.post("/api/admin/auth", authController.authenticate) |
|||
.get("/api/admin/auth/google", authController.googlePreAuth) |
|||
.get("/api/admin/auth/google/callback", authController.googleAuth) |
|||
.post("/api/admin/auth/logout", authController.logout) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,49 @@ |
|||
const setup = require("./utilities") |
|||
|
|||
jest.mock("nodemailer") |
|||
const sendMailMock = setup.emailMock() |
|||
|
|||
describe("/api/admin/auth", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let code |
|||
|
|||
beforeAll(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
it("should be able to generate password reset email", async () => { |
|||
// initially configure settings
|
|||
await config.saveSmtpConfig() |
|||
await config.saveSettingsConfig() |
|||
await config.createUser("test@test.com") |
|||
const res = await request |
|||
.post(`/api/admin/auth/reset`) |
|||
.send({ |
|||
email: "test@test.com", |
|||
}) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toEqual({ message: "Please check your email for a reset link." }) |
|||
expect(sendMailMock).toHaveBeenCalled() |
|||
const emailCall = sendMailMock.mock.calls[0][0] |
|||
// after this URL there should be a code
|
|||
const parts = emailCall.html.split("http://localhost:10000/reset?code=") |
|||
code = parts[1].split("\"")[0] |
|||
expect(code).toBeDefined() |
|||
}) |
|||
|
|||
it("should allow resetting user password with code", async () => { |
|||
const res = await request |
|||
.post(`/api/admin/auth/reset/update`) |
|||
.send({ |
|||
password: "newpassword", |
|||
resetCode: code, |
|||
}) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toEqual({ message: "password reset successfully." }) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,52 @@ |
|||
const setup = require("./utilities") |
|||
|
|||
jest.mock("nodemailer") |
|||
const sendMailMock = setup.emailMock() |
|||
|
|||
describe("/api/admin/users", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let code |
|||
|
|||
beforeAll(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
it("should be able to generate an invitation", async () => { |
|||
// initially configure settings
|
|||
await config.saveSmtpConfig() |
|||
await config.saveSettingsConfig() |
|||
const res = await request |
|||
.post(`/api/admin/users/invite`) |
|||
.send({ |
|||
email: "invite@test.com", |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toEqual({ message: "Invitation has been sent." }) |
|||
expect(sendMailMock).toHaveBeenCalled() |
|||
const emailCall = sendMailMock.mock.calls[0][0] |
|||
// after this URL there should be a code
|
|||
const parts = emailCall.html.split("http://localhost:10000/invite?code=") |
|||
code = parts[1].split("\"")[0] |
|||
expect(code).toBeDefined() |
|||
}) |
|||
|
|||
it("should be able to create new user from invite", async () => { |
|||
const res = await request |
|||
.post(`/api/admin/users/invite/accept`) |
|||
.send({ |
|||
password: "newpassword", |
|||
inviteCode: code, |
|||
}) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body._id).toBeDefined() |
|||
const user = await config.getUser("invite@test.com") |
|||
expect(user).toBeDefined() |
|||
expect(user._id).toEqual(res.body._id) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,85 @@ |
|||
const { Client, utils } = require("@budibase/auth").redis |
|||
const { newid } = require("@budibase/auth").utils |
|||
|
|||
function getExpirySecondsForDB(db) { |
|||
switch (db) { |
|||
case utils.Databases.PW_RESETS: |
|||
// a hour
|
|||
return 3600 |
|||
case utils.Databases.INVITATIONS: |
|||
// a day
|
|||
return 86400 |
|||
} |
|||
} |
|||
|
|||
async function getClient(db) { |
|||
return await new Client(db).init() |
|||
} |
|||
|
|||
async function writeACode(db, value) { |
|||
const client = await getClient(db) |
|||
const code = newid() |
|||
await client.store(code, value, getExpirySecondsForDB(db)) |
|||
client.finish() |
|||
return code |
|||
} |
|||
|
|||
async function getACode(db, code, deleteCode = true) { |
|||
const client = await getClient(db) |
|||
const value = await client.get(code) |
|||
if (!value) { |
|||
throw "Invalid code." |
|||
} |
|||
if (deleteCode) { |
|||
await client.delete(code) |
|||
} |
|||
client.finish() |
|||
return value |
|||
} |
|||
|
|||
/** |
|||
* Given a user ID this will store a code (that is returned) for an hour in redis. |
|||
* The user can then return this code for resetting their password (through their reset link). |
|||
* @param {string} userId the ID of the user which is to be reset. |
|||
* @return {Promise<string>} returns the code that was stored to redis. |
|||
*/ |
|||
exports.getResetPasswordCode = async userId => { |
|||
return writeACode(utils.Databases.PW_RESETS, userId) |
|||
} |
|||
|
|||
/** |
|||
* Given a reset code this will lookup to redis, check if the code is valid and delete if required. |
|||
* @param {string} resetCode The code provided via the email link. |
|||
* @param {boolean} deleteCode If the code is used/finished with this will delete it - defaults to true. |
|||
* @return {Promise<string>} returns the user ID if it is found |
|||
*/ |
|||
exports.checkResetPasswordCode = async (resetCode, deleteCode = true) => { |
|||
try { |
|||
return getACode(utils.Databases.PW_RESETS, resetCode, deleteCode) |
|||
} catch (err) { |
|||
throw "Provided information is not valid, cannot reset password - please try again." |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Generates an invitation code and writes it to redis - which can later be checked for user creation. |
|||
* @param {string} email the email address which the code is being sent to (for use later). |
|||
* @return {Promise<string>} returns the code that was stored to redis. |
|||
*/ |
|||
exports.getInviteCode = async email => { |
|||
return writeACode(utils.Databases.INVITATIONS, email) |
|||
} |
|||
|
|||
/** |
|||
* Checks that the provided invite code is valid - will return the email address of user that was invited. |
|||
* @param {string} inviteCode the invite code that was provided as part of the link. |
|||
* @param {boolean} deleteCode whether or not the code should be deleted after retrieval - defaults to true. |
|||
* @return {Promise<string>} If the code is valid then an email address will be returned. |
|||
*/ |
|||
exports.checkInviteCode = async (inviteCode, deleteCode = true) => { |
|||
try { |
|||
return getACode(utils.Databases.INVITATIONS, inviteCode, deleteCode) |
|||
} catch (err) { |
|||
throw "Invitation is not valid or has expired, please request a new one." |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue