Merge branch 'master' into feature/63-delete-account-frontend
This commit is contained in:
commit
a35289d9b8
6745
app/package-lock.json
generated
6745
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@
|
||||
"leaflet": "^1.5.1",
|
||||
"mapnik": "^4.2.1",
|
||||
"node-fs": "^0.1.7",
|
||||
"nodemailer": "^6.3.0",
|
||||
"pg-promise": "^8.7.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.8.2",
|
||||
@ -39,7 +40,8 @@
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/express-session": "^1.15.13",
|
||||
"@types/jest": "^24.0.17",
|
||||
"@types/node": "^12.7.1",
|
||||
"@types/node": "^8.10.52",
|
||||
"@types/nodemailer": "^6.2.1",
|
||||
"@types/prop-types": "^15.7.1",
|
||||
"@types/react": "^16.9.1",
|
||||
"@types/react-dom": "^16.8.5",
|
||||
|
@ -1,49 +1,20 @@
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
import { authUser, createUser, getUserById, getNewUserAPIKey, deleteUser } from './services/user';
|
||||
import { authUser, createUser, getUserById, getNewUserAPIKey, deleteUser, logout } from './services/user';
|
||||
import { queryLocation } from './services/search';
|
||||
|
||||
import buildingsRouter from './routes/buildingsRouter';
|
||||
import usersRouter from './routes/usersRouter';
|
||||
|
||||
|
||||
const server = express();
|
||||
const server = express.Router();
|
||||
|
||||
// parse POSTed json body
|
||||
server.use(bodyParser.json());
|
||||
|
||||
server.use('/buildings', buildingsRouter);
|
||||
|
||||
// POST new user
|
||||
server.post('/users', function (req, res) {
|
||||
const user = req.body;
|
||||
if (req.session.user_id) {
|
||||
res.send({ error: 'Already signed in' });
|
||||
return
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
if (user.email != user.confirm_email) {
|
||||
res.send({ error: 'Email did not match confirmation.' });
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user.email = null;
|
||||
}
|
||||
|
||||
createUser(user).then(function (result) {
|
||||
if (result.user_id) {
|
||||
req.session.user_id = result.user_id;
|
||||
res.send({ user_id: result.user_id });
|
||||
} else {
|
||||
req.session.user_id = undefined;
|
||||
res.send({ error: result.error });
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
res.send(err)
|
||||
});
|
||||
});
|
||||
server.use('/users', usersRouter);
|
||||
|
||||
// GET own user info
|
||||
server.route('/users/me')
|
||||
@ -98,16 +69,6 @@ server.post('/logout', function (req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
function logout(session) {
|
||||
return new Promise((resolve, reject) => {
|
||||
session.user_id = undefined;
|
||||
session.destroy(err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// POST generate API key
|
||||
server.post('/api/key', function (req, res) {
|
||||
if (!req.session.user_id) {
|
||||
@ -157,8 +118,20 @@ server.get('/search', function (req, res) {
|
||||
});
|
||||
})
|
||||
|
||||
server.use((err, req, res, next) => {
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (err != undefined) {
|
||||
console.log('Global error handler: ', err);
|
||||
res.status(500).send({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
server.use((req, res) => {
|
||||
res.status(404).json({ error: 'Resource not found'});
|
||||
})
|
||||
|
||||
export default server;
|
||||
|
||||
export default server;
|
||||
|
104
app/src/api/controllers/userController.ts
Normal file
104
app/src/api/controllers/userController.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { URL } from 'url';
|
||||
|
||||
import express from 'express';
|
||||
|
||||
import * as userService from '../services/user';
|
||||
import * as passwordResetService from '../services/passwordReset';
|
||||
import { TokenVerificationError } from '../services/passwordReset';
|
||||
|
||||
function createUser(req, res) {
|
||||
const user = req.body;
|
||||
if (req.session.user_id) {
|
||||
res.send({ error: 'Already signed in' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
if (user.email != user.confirm_email) {
|
||||
res.send({ error: 'Email did not match confirmation.' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
user.email = null;
|
||||
}
|
||||
|
||||
userService.createUser(user).then(function (result) {
|
||||
if (result.user_id) {
|
||||
req.session.user_id = result.user_id;
|
||||
res.send({ user_id: result.user_id });
|
||||
} else {
|
||||
req.session.user_id = undefined;
|
||||
res.send({ error: result.error });
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
res.send(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentUser(req, res) {
|
||||
if (!req.session.user_id) {
|
||||
res.send({ error: 'Must be logged in' });
|
||||
return;
|
||||
}
|
||||
|
||||
userService.getUserById(req.session.user_id).then(function (user) {
|
||||
res.send(user);
|
||||
}).catch(function (error) {
|
||||
res.send(error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCurrentUser(req, res) {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
console.log(`Deleting user ${req.session.user_id}`);
|
||||
|
||||
userService.deleteUser(req.session.user_id).then(
|
||||
() => userService.logout(req.session)
|
||||
).then(() => {
|
||||
res.send({ success: true });
|
||||
}).catch(err => {
|
||||
res.send({ error: err });
|
||||
});
|
||||
}
|
||||
|
||||
async function resetPassword(req: express.Request, res: express.Response) {
|
||||
if(req.body == undefined || (req.body.email == undefined && req.body.token == undefined)) {
|
||||
return res.send({ error: 'Expected an email address or password reset token in the request body' });
|
||||
}
|
||||
|
||||
if(req.body.email != undefined) {
|
||||
// first stage: send reset token to email address
|
||||
|
||||
// this relies on the API being on the same hostname as the frontend
|
||||
const { origin } = new URL(req.protocol + '://' + req.headers.host);
|
||||
await passwordResetService.sendPasswordResetToken(req.body.email, origin);
|
||||
|
||||
return res.status(202).send({ success: true });
|
||||
} else if (req.body.token != undefined) {
|
||||
// second stage: verify token and reset password
|
||||
if (req.body.password == undefined) {
|
||||
return res.send({ error: 'Expected a new password' });
|
||||
}
|
||||
try {
|
||||
await passwordResetService.resetPassword(req.body.token, req.body.password);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenVerificationError) {
|
||||
return res.send({ error: 'Could not verify token' });
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return res.send({ success: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
createUser,
|
||||
getCurrentUser,
|
||||
deleteCurrentUser,
|
||||
resetPassword
|
||||
};
|
21
app/src/api/routes/usersRouter.ts
Normal file
21
app/src/api/routes/usersRouter.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import express from 'express';
|
||||
|
||||
import userController from '../controllers/userController';
|
||||
|
||||
const asyncMiddleware = fn =>
|
||||
(req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next))
|
||||
.catch(next);
|
||||
};
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', userController.createUser);
|
||||
|
||||
router.route('/me')
|
||||
.get(userController.getCurrentUser)
|
||||
.delete(userController.deleteCurrentUser);
|
||||
|
||||
router.put('/password', asyncMiddleware(userController.resetPassword));
|
||||
|
||||
export default router;
|
11
app/src/api/services/email.ts
Normal file
11
app/src/api/services/email.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as nodemailer from 'nodemailer';
|
||||
|
||||
export const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_SERVER_HOST,
|
||||
port: parseInt(process.env.MAIL_SERVER_PORT),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.MAIL_SERVER_USER,
|
||||
pass: process.env.MAIL_SERVER_PASSWORD
|
||||
}
|
||||
});
|
121
app/src/api/services/passwordReset.ts
Normal file
121
app/src/api/services/passwordReset.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import url, { URL } from 'url';
|
||||
import { errors } from 'pg-promise';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import db from '../../db';
|
||||
import * as userService from './user';
|
||||
import { transporter } from './email';
|
||||
|
||||
|
||||
/**
|
||||
* Generate a password reset token for the specified account and send the password reset link by email
|
||||
* @param email the email address for which to generate a password reset token
|
||||
* @param siteOrigin the origin of the website, without a path element - e.g. https://beta.colouring.london
|
||||
*/
|
||||
export async function sendPasswordResetToken(email: string, siteOrigin: string): Promise<void> {
|
||||
const user = await userService.getUserByEmail(email);
|
||||
|
||||
// if no user found for email, do nothing
|
||||
if (user == undefined) return;
|
||||
|
||||
const token = await createPasswordResetToken(user.user_id);
|
||||
const message = getPasswordResetEmail(email, token, siteOrigin);
|
||||
await transporter.sendMail(message);
|
||||
}
|
||||
|
||||
export async function resetPassword(passwordResetToken: string, newPassword: string): Promise<void> {
|
||||
const userId = await verifyPasswordResetToken(passwordResetToken);
|
||||
if (userId != undefined) {
|
||||
await updatePasswordForUser(userId, newPassword);
|
||||
//TODO: expire other tokens of the user?
|
||||
} else {
|
||||
throw new TokenVerificationError('Password reset token could not be verified');
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenVerificationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "TokenVerificationError";
|
||||
}
|
||||
}
|
||||
|
||||
function getPasswordResetEmail(email: string, token: string, siteOrigin: string): nodemailer.SendMailOptions {
|
||||
const linkUrl = new URL(siteOrigin);
|
||||
linkUrl.pathname = '/password-reset.html';
|
||||
linkUrl.search = `?token=${token}`;
|
||||
|
||||
const linkString = url.format(linkUrl);
|
||||
|
||||
const messageBody = `Hi there,
|
||||
|
||||
Someone has requested a password reset for the Colouring London account associated with this email address.
|
||||
Click on the following link within the next 24 hours to reset your password:
|
||||
|
||||
${linkString}
|
||||
`;
|
||||
|
||||
return {
|
||||
text: messageBody,
|
||||
subject: 'Reset your Colouring London password',
|
||||
to: email,
|
||||
from: 'no-reply@colouring.london'
|
||||
};
|
||||
}
|
||||
|
||||
async function createPasswordResetToken(userId: string) {
|
||||
const { token } = await db.one(
|
||||
`INSERT INTO
|
||||
user_password_reset_tokens (user_id, expires_on)
|
||||
VALUES
|
||||
($1::uuid, now() at time zone 'utc' + INTERVAL '$2 day')
|
||||
RETURNING
|
||||
token
|
||||
`, [userId, 1]
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the password reset token is valid and expire the token
|
||||
* @param token password reset token to verify and use
|
||||
* @returns the UUID of the user whose token was used
|
||||
*/
|
||||
async function verifyPasswordResetToken(token: string): Promise<string> {
|
||||
try {
|
||||
// verify and deactivate the token in one operation
|
||||
const usedToken = await db.one(
|
||||
`UPDATE
|
||||
user_password_reset_tokens
|
||||
SET used = true
|
||||
WHERE
|
||||
token = $1::uuid
|
||||
AND NOT used
|
||||
AND now() at time zone 'utc' < expires_on
|
||||
RETURNING *`, [token]
|
||||
);
|
||||
console.log('verified');
|
||||
|
||||
return usedToken.user_id;
|
||||
} catch (err) {
|
||||
if (err instanceof errors.QueryResultError) {
|
||||
console.log(err.code);
|
||||
if (err.code === errors.queryResultErrorCode.noData) return undefined;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePasswordForUser(userId: string, newPassword: string): Promise<null> {
|
||||
return db.none(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
pass = crypt($1, gen_salt('bf'))
|
||||
WHERE
|
||||
user_id = $2::uuid
|
||||
`, [newPassword, userId]);
|
||||
}
|
||||
|
@ -86,6 +86,21 @@ function getUserById(id) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUserByEmail(email: string) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id, username, email
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1
|
||||
`, [email]
|
||||
).catch(function(error) {
|
||||
console.error('Error:', error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getNewUserAPIKey(id) {
|
||||
return db.one(
|
||||
`UPDATE
|
||||
@ -140,4 +155,23 @@ function deleteUser(id) {
|
||||
});
|
||||
}
|
||||
|
||||
export { getUserById, createUser, authUser, getNewUserAPIKey, authAPIUser, deleteUser }
|
||||
function logout(session: Express.Session) {
|
||||
return new Promise((resolve, reject) => {
|
||||
session.user_id = undefined;
|
||||
session.destroy(err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
getUserById,
|
||||
getUserByEmail,
|
||||
createUser,
|
||||
authUser,
|
||||
getNewUserAPIKey,
|
||||
authAPIUser,
|
||||
deleteUser,
|
||||
logout
|
||||
};
|
||||
|
@ -20,6 +20,8 @@ import Welcome from './welcome';
|
||||
import { parseCategoryURL } from '../parse';
|
||||
import PrivacyPolicyPage from './privacy-policy';
|
||||
import ContributorAgreementPage from './contributor-agreement';
|
||||
import ForgottenPassword from './forgotten-password';
|
||||
import PasswordReset from './password-reset';
|
||||
|
||||
/**
|
||||
* App component
|
||||
@ -247,6 +249,8 @@ class App extends React.Component<any, any> { // TODO: add proper types
|
||||
<Route exact path="/login.html">
|
||||
<Login user={this.state.user} login={this.login} />
|
||||
</Route>
|
||||
<Route exact path="/forgotten-password.html" component={ForgottenPassword} />
|
||||
<Route exact path="/password-reset.html" component={PasswordReset} />
|
||||
<Route exact path="/sign-up.html">
|
||||
<SignUp user={this.state.user} login={this.login} />
|
||||
</Route>
|
||||
|
84
app/src/frontend/forgotten-password.tsx
Normal file
84
app/src/frontend/forgotten-password.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { FormEvent, ChangeEvent } from 'react';
|
||||
import InfoBox from './info-box';
|
||||
import ErrorBox from './error-box';
|
||||
|
||||
interface ForgottenPasswordState {
|
||||
success: boolean;
|
||||
error: string;
|
||||
email: string;
|
||||
emailUsed: string;
|
||||
}
|
||||
|
||||
export default class ForgottenPassword extends React.Component<{}, ForgottenPasswordState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: undefined,
|
||||
success: undefined,
|
||||
email: undefined,
|
||||
emailUsed: undefined
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState({ [name]: value } as any);
|
||||
}
|
||||
|
||||
async handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
this.setState({ error: undefined, success: undefined });
|
||||
|
||||
const emailSent = this.state.email;
|
||||
try {
|
||||
const res = await fetch('/api/users/password', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ email: emailSent }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error != undefined) {
|
||||
this.setState({ error: data.error });
|
||||
} else if (data.success === true) {
|
||||
this.setState({ success: true, emailUsed: emailSent});
|
||||
} else {
|
||||
this.setState({ error: 'Unexpected result.' });
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ error: 'Something went wrong.' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1 className="h2">Forgotten password</h1>
|
||||
<p>Please provide the e-mail address associated with your account. A password reset link will be sent to your mailbox.</p>
|
||||
<ErrorBox msg={this.state.error} />
|
||||
<InfoBox msg="">
|
||||
{this.state.success ?
|
||||
`A password reset link has been sent to ${this.state.emailUsed}. Please check your inbox.` :
|
||||
null
|
||||
}
|
||||
</InfoBox>
|
||||
<form onSubmit={e => this.handleSubmit(e)}>
|
||||
<label htmlFor="email">E-mail</label>
|
||||
<input name="email" id="email"
|
||||
className="form-control" type="email"
|
||||
placeholder="Your e-mail address" required
|
||||
onChange={e => this.handleChange(e)}
|
||||
/>
|
||||
|
||||
<div className="buttons-container">
|
||||
<input type="submit" value="Request password reset" className="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
}
|
@ -111,6 +111,8 @@ class Login extends Component<any, any> { // TODO: add proper types
|
||||
<label htmlFor="show_password" className="form-check-label">Show password?</label>
|
||||
</div>
|
||||
|
||||
<Link to="/forgotten-password.html">Forgotten password?</Link>
|
||||
|
||||
<div className="buttons-container">
|
||||
<input type="submit" value="Log In" className="btn btn-primary" />
|
||||
</div>
|
||||
|
122
app/src/frontend/password-reset.tsx
Normal file
122
app/src/frontend/password-reset.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { ChangeEvent, FormEvent } from 'react';
|
||||
import { RouteComponentProps, Redirect } from 'react-router';
|
||||
|
||||
import ErrorBox from './error-box';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface PasswordResetState {
|
||||
error: string;
|
||||
success: boolean;
|
||||
token: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export default class PasswordReset extends React.Component<RouteComponentProps, PasswordResetState> {
|
||||
|
||||
static tokenMissingMessage = 'Password reset token is missing. Make sure to follow the link from a password reset email!';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: undefined,
|
||||
success: undefined,
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
confirmPassword: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const queryParams = new URLSearchParams(this.props.location.search);
|
||||
const token = queryParams.get('token');
|
||||
if(token == undefined) {
|
||||
this.setState({ error: PasswordReset.tokenMissingMessage });
|
||||
} else {
|
||||
this.setState({ token: token });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event: FormEvent<HTMLInputElement>) {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState({
|
||||
[name]: value,
|
||||
} as any);
|
||||
}
|
||||
|
||||
async handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({ error: undefined });
|
||||
|
||||
if(this.state.token == undefined) {
|
||||
this.setState({ error: PasswordReset.tokenMissingMessage });
|
||||
return;
|
||||
}
|
||||
if(this.state.password !== this.state.confirmPassword) {
|
||||
this.setState({ error: 'Passwords do not match' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const requestData = {
|
||||
token: this.state.token,
|
||||
password: this.state.password
|
||||
};
|
||||
|
||||
const res = await fetch('/api/users/password', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(requestData),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error != undefined) {
|
||||
this.setState({ error: data.error });
|
||||
} else if (data.success === true) {
|
||||
this.setState({ success: true });
|
||||
} else {
|
||||
this.setState({ error: 'Unexpected result.' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.success) {
|
||||
return <Redirect to="/my-account.html" />;
|
||||
}
|
||||
return (
|
||||
|
||||
<article>
|
||||
<section className="main-col">
|
||||
<h1 className="h2">Forgotten password</h1>
|
||||
<p>Please input the new password you want to set for your account. If your password reset token does not work, go back to the <Link to="/forgotten-password.html">forgotten password page</Link> to request a new link.</p>
|
||||
<ErrorBox msg={this.state.error} />
|
||||
<form onSubmit={e => this.handleSubmit(e)}>
|
||||
<label htmlFor="email">New password</label>
|
||||
<input name="password"
|
||||
className="form-control" type="password"
|
||||
placeholder="New password" required
|
||||
onChange={e => this.handleChange(e)}
|
||||
/>
|
||||
|
||||
<label htmlFor="email">Confirm new password</label>
|
||||
<input name="confirmPassword"
|
||||
className="form-control" type="password"
|
||||
placeholder="Confirm new password" required
|
||||
onChange={e => this.handleChange(e)}
|
||||
/>
|
||||
|
||||
<input name="token" id="token" type="hidden" value={this.state.token} />
|
||||
|
||||
<div className="buttons-container">
|
||||
<input type="submit" value="Change password" className="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
}
|
@ -21,7 +21,10 @@ module.exports = {
|
||||
PGUSER: "username",
|
||||
PGPASSWORD: "longrandomsecret",
|
||||
APP_COOKIE_SECRET: "longrandomsecret",
|
||||
|
||||
MAIL_SERVER_HOST: "mail_hostname",
|
||||
MAIL_SERVER_PORT: 587,
|
||||
MAIL_SERVER_USER: "mail_username",
|
||||
MAIL_SERVER_PASSWORD: "longrandompassword",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -17,7 +17,11 @@ module.exports = {
|
||||
PGUSER: "username",
|
||||
PGPASSWORD: "longrandomsecret",
|
||||
APP_COOKIE_SECRET: "longrandomsecret",
|
||||
TILECACHE_PATH: "/path/to/tile/cache"
|
||||
TILECACHE_PATH: "/path/to/tile/cache",
|
||||
MAIL_SERVER_HOST: "mail_hostname",
|
||||
MAIL_SERVER_PORT: 587,
|
||||
MAIL_SERVER_USER: "mail_username",
|
||||
MAIL_SERVER_PASSWORD: "longrandompassword",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
1
migrations/010.password-reset.down.sql
Normal file
1
migrations/010.password-reset.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS user_password_reset_tokens;
|
6
migrations/010.password-reset.up.sql
Normal file
6
migrations/010.password-reset.up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS user_password_reset_tokens (
|
||||
token uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid,
|
||||
expires_on timestamp NOT NULL,
|
||||
used boolean NOT NULL DEFAULT false
|
||||
)
|
Loading…
Reference in New Issue
Block a user