mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 21:01:27 +00:00
Move change_email/start to password-protected endpoint
This commit is contained in:
@@ -28,72 +28,6 @@ const config = require('../config.js');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { invalidate_cached_user_by_id } = require('../helpers.js');
|
||||
|
||||
const CHANGE_EMAIL_START = eggspress('/change_email/start', {
|
||||
subdomain: 'api',
|
||||
auth: true,
|
||||
verified: true,
|
||||
allowedMethods: ['POST'],
|
||||
}, async (req, res, next) => {
|
||||
const user = req.user;
|
||||
const new_email = req.body.new_email;
|
||||
|
||||
// TODO: DRY: signup.js
|
||||
// validation
|
||||
if( ! new_email ) {
|
||||
throw APIError.create('field_missing', null, { key: 'new_email' });
|
||||
}
|
||||
if ( typeof new_email !== 'string' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'new_email', expected: 'a valid email address' });
|
||||
}
|
||||
if ( ! validator.isEmail(new_email) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'new_email', expected: 'a valid email address' });
|
||||
}
|
||||
|
||||
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
|
||||
if ( ! svc_edgeRateLimit.check('change-email-start') ) {
|
||||
return res.status(429).send('Too many requests.');
|
||||
}
|
||||
|
||||
// check if email is already in use
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
const rows = await db.read(
|
||||
'SELECT COUNT(*) AS `count` FROM `user` WHERE `email` = ?',
|
||||
[new_email]
|
||||
);
|
||||
if ( rows[0].count > 0 ) {
|
||||
throw APIError.create('email_already_in_use', null, { email: new_email });
|
||||
}
|
||||
|
||||
// generate confirmation token
|
||||
const token = crypto.randomBytes(4).toString('hex');
|
||||
const jwt_token = jwt.sign({
|
||||
user_id: user.id,
|
||||
token,
|
||||
}, config.jwt_secret, { expiresIn: '24h' });
|
||||
|
||||
// send confirmation email
|
||||
const svc_email = req.services.get('email');
|
||||
await svc_email.send_email({ email: new_email }, 'email_change_request', {
|
||||
confirm_url: `${config.origin}/change_email/confirm?token=${jwt_token}`,
|
||||
username: user.username,
|
||||
});
|
||||
const old_email = user.email;
|
||||
// TODO: NotificationService
|
||||
await svc_email.send_email({ email: old_email }, 'email_change_notification', {
|
||||
new_email: new_email,
|
||||
});
|
||||
|
||||
// update user
|
||||
await db.write(
|
||||
'UPDATE `user` SET `unconfirmed_change_email` = ?, `change_email_confirm_token` = ? WHERE `id` = ?',
|
||||
[new_email, token, user.id]
|
||||
);
|
||||
|
||||
res.send({ success: true });
|
||||
});
|
||||
|
||||
const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
|
||||
allowedMethods: ['GET'],
|
||||
}, async (req, res, next) => {
|
||||
@@ -137,6 +71,5 @@ const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
|
||||
});
|
||||
|
||||
module.exports = app => {
|
||||
app.use(CHANGE_EMAIL_START);
|
||||
app.use(CHANGE_EMAIL_CONFIRM);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
const APIError = require("../../api/APIError");
|
||||
const { DB_WRITE } = require("../../services/database/consts");
|
||||
const jwt = require('jsonwebtoken');
|
||||
const validator = require('validator');
|
||||
const crypto = require('crypto');
|
||||
const config = require("../../config");
|
||||
|
||||
module.exports = {
|
||||
route: '/change-email',
|
||||
methods: ['POST'],
|
||||
handler: async (req, res, next) => {
|
||||
const user = req.user;
|
||||
const new_email = req.body.new_email;
|
||||
|
||||
console.log('DID REACH HERE');
|
||||
|
||||
// TODO: DRY: signup.js
|
||||
// validation
|
||||
if( ! new_email ) {
|
||||
throw APIError.create('field_missing', null, { key: 'new_email' });
|
||||
}
|
||||
if ( typeof new_email !== 'string' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'new_email', expected: 'a valid email address' });
|
||||
}
|
||||
if ( ! validator.isEmail(new_email) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'new_email', expected: 'a valid email address' });
|
||||
}
|
||||
|
||||
// check if email is already in use
|
||||
const db = req.services.get('database').get(DB_WRITE, 'auth');
|
||||
const rows = await db.read(
|
||||
'SELECT COUNT(*) AS `count` FROM `user` WHERE `email` = ?',
|
||||
[new_email]
|
||||
);
|
||||
if ( rows[0].count > 0 ) {
|
||||
throw APIError.create('email_already_in_use', null, { email: new_email });
|
||||
}
|
||||
|
||||
// generate confirmation token
|
||||
const token = crypto.randomBytes(4).toString('hex');
|
||||
const jwt_token = jwt.sign({
|
||||
user_id: user.id,
|
||||
token,
|
||||
}, config.jwt_secret, { expiresIn: '24h' });
|
||||
|
||||
// send confirmation email
|
||||
const svc_email = req.services.get('email');
|
||||
await svc_email.send_email({ email: new_email }, 'email_change_request', {
|
||||
confirm_url: `${config.origin}/change_email/confirm?token=${jwt_token}`,
|
||||
username: user.username,
|
||||
});
|
||||
const old_email = user.email;
|
||||
// TODO: NotificationService
|
||||
await svc_email.send_email({ email: old_email }, 'email_change_notification', {
|
||||
new_email: new_email,
|
||||
});
|
||||
|
||||
// update user
|
||||
await db.write(
|
||||
'UPDATE `user` SET `unconfirmed_change_email` = ?, `change_email_confirm_token` = ? WHERE `id` = ?',
|
||||
[new_email, token, user.id]
|
||||
);
|
||||
|
||||
res.send({ success: true });
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,12 @@ const { quot } = require("../../util/strutil");
|
||||
const { MINUTE, HOUR } = require('../../util/time.js');
|
||||
const BaseService = require("../BaseService");
|
||||
|
||||
/* INCREMENTAL CHANGES
|
||||
The first scopes are of the form 'name-of-endpoint', but later it was
|
||||
decided that they're of the form `/path/to/endpoint`. New scopes should
|
||||
follow the latter form.
|
||||
*/
|
||||
|
||||
class EdgeRateLimitService extends BaseService {
|
||||
_construct () {
|
||||
this.scopes = {
|
||||
@@ -60,6 +66,10 @@ class EdgeRateLimitService extends BaseService {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['/user-protected/change-email']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['login-otp']: {
|
||||
limit: 15,
|
||||
window: 30 * MINUTE,
|
||||
|
||||
@@ -82,7 +82,11 @@ class UserProtectedEndpointsService extends BaseService {
|
||||
});
|
||||
|
||||
Endpoint(
|
||||
require('../../routers/user-protected/change-password.js')
|
||||
require('../../routers/user-protected/change-password.js'),
|
||||
).attach(router);
|
||||
|
||||
Endpoint(
|
||||
require('../../routers/user-protected/change-email.js'),
|
||||
).attach(router);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Placeholder from '../../util/Placeholder.js';
|
||||
import PasswordEntry from '../Components/PasswordEntry.js';
|
||||
import UIWindow from '../UIWindow.js'
|
||||
|
||||
// TODO: DRY: We could specify a validator and endpoint instead of writing
|
||||
// a DOM tree and event handlers for each of these. (low priority)
|
||||
async function UIWindowChangeEmail(options){
|
||||
options = options ?? {};
|
||||
|
||||
const password_entry = new PasswordEntry({});
|
||||
const place_password_entry = Placeholder();
|
||||
|
||||
const internal_id = window.uuidv4();
|
||||
let h = '';
|
||||
h += `<div class="change-email" style="padding: 20px; border-bottom: 1px solid #ced7e1;">`;
|
||||
@@ -34,6 +41,11 @@ async function UIWindowChangeEmail(options){
|
||||
h += `<label for="confirm-new-email-${internal_id}">${i18n('new_email')}</label>`;
|
||||
h += `<input id="confirm-new-email-${internal_id}" type="text" name="new-email" class="new-email" autocomplete="off" />`;
|
||||
h += `</div>`;
|
||||
// password confirmation
|
||||
h += `<div style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">`;
|
||||
h += `<label>${i18n('account_password')}</label>`;
|
||||
h += `${place_password_entry.html}`;
|
||||
h += `</div>`;
|
||||
|
||||
// Change Email
|
||||
h += `<button class="change-email-btn button button-primary button-block button-normal">${i18n('change_email')}</button>`;
|
||||
@@ -73,11 +85,14 @@ async function UIWindowChangeEmail(options){
|
||||
...options.window_options
|
||||
})
|
||||
|
||||
password_entry.attach(place_password_entry);
|
||||
|
||||
$(el_window).find('.change-email-btn').on('click', function(e){
|
||||
// hide previous error/success msg
|
||||
$(el_window).find('.form-success-msg, .form-success-msg').hide();
|
||||
|
||||
const new_email = $(el_window).find('.new-email').val();
|
||||
const password = $(el_window).find('.password').val();
|
||||
|
||||
if(!new_email){
|
||||
$(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
|
||||
@@ -93,7 +108,7 @@ async function UIWindowChangeEmail(options){
|
||||
$(el_window).find('.new-email').attr('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: window.api_origin + "/change_email/start",
|
||||
url: window.api_origin + "/user-protected/change-email",
|
||||
type: 'POST',
|
||||
async: true,
|
||||
headers: {
|
||||
@@ -102,6 +117,7 @@ async function UIWindowChangeEmail(options){
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({
|
||||
new_email: new_email,
|
||||
password: password_entry.get('value'),
|
||||
}),
|
||||
success: function (data){
|
||||
$(el_window).find('.form-success-msg').html(i18n('email_change_confirmation_sent'));
|
||||
|
||||
@@ -23,6 +23,7 @@ const en = {
|
||||
dictionary: {
|
||||
about: "About",
|
||||
account: "Account",
|
||||
account_password: "Verify Account Password",
|
||||
access_granted_to: "Access Granted To",
|
||||
add_existing_account: "Add Existing Account",
|
||||
all_fields_required: 'All fields are required.',
|
||||
|
||||
Reference in New Issue
Block a user