diff --git a/packages/backend/src/routers/auth/configure-2fa.js b/packages/backend/src/routers/auth/configure-2fa.js
index a9aef1d06..0d0422a7f 100644
--- a/packages/backend/src/routers/auth/configure-2fa.js
+++ b/packages/backend/src/routers/auth/configure-2fa.js
@@ -31,6 +31,8 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
`UPDATE user SET otp_secret = ? WHERE uuid = ?`,
[result.secret, user.uuid]
);
+ // update cached user
+ req.user.otp_secret = result.secret;
return result;
};
diff --git a/packages/backend/src/routers/whoami.js b/packages/backend/src/routers/whoami.js
index 61ce36de3..924b589d0 100644
--- a/packages/backend/src/routers/whoami.js
+++ b/packages/backend/src/routers/whoami.js
@@ -41,6 +41,8 @@ const WHOAMI_GET = eggspress('/whoami', {
const is_user = actor.type instanceof UserActorType;
+ console.log('user?', req.user);
+
// send user object
const details = {
username: req.user.username,
@@ -54,6 +56,7 @@ const WHOAMI_GET = eggspress('/whoami', {
is_temp: (req.user.password === null && req.user.email === null),
taskbar_items: await get_taskbar_items(req.user),
referral_code: req.user.referral_code,
+ otp: !! req.user.otp_secret,
...(req.new_token ? { token: req.token } : {})
};
diff --git a/src/UI/Settings/UITabAccount.js b/src/UI/Settings/UITabAccount.js
index d05442065..2d933ef8e 100644
--- a/src/UI/Settings/UITabAccount.js
+++ b/src/UI/Settings/UITabAccount.js
@@ -64,14 +64,6 @@ export default {
h += ``;
}
- // session manager
- h += `
`;
h += `
${i18n("delete_account")}`;
diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js
new file mode 100644
index 000000000..305692b14
--- /dev/null
+++ b/src/UI/Settings/UITabSecurity.js
@@ -0,0 +1,85 @@
+import UIWindowQR from "../UIWindowQR.js";
+
+export default {
+ id: 'security',
+ title_i18n_key: 'security',
+ icon: 'shield.svg',
+ html: () => {
+ let h = `
${i18n('security')}
`;
+
+ // change password button
+ if(!user.is_temp){
+ h += `
`;
+ h += `
${i18n('password')}`;
+ h += `
`;
+ h += ``;
+ h += `
`;
+ h += `
`;
+ }
+
+ // session manager
+ h += `
`;
+ h += `
${i18n('sessions')}`;
+ h += `
`;
+ h += ``;
+ h += `
`;
+ h += `
`;
+
+ // configure 2FA
+ if(!user.is_temp){
+ h += `
`;
+ h += `
`;
+ h += `${i18n('two_factor')}`;
+ h += `${
+ i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')
+ }`;
+ h += `
`;
+ h += `
`;
+ h += ``;
+ h += ``;
+ h += `
`;
+ h += `
`;
+ }
+
+
+ return h;
+ },
+ init: ($el_window) => {
+ $el_window.find('.enable-2fa').on('click', async function (e) {
+ const resp = await fetch(`${api_origin}/auth/configure-2fa/enable`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${puter.authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ });
+ const data = await resp.json();
+
+ UIWindowQR({
+ message_i18n_key: 'scan_qr_2fa',
+ text: data.url,
+ text_below: data.secret,
+ });
+
+ $el_window.find('.enable-2fa').hide();
+ $el_window.find('.disable-2fa').show();
+ $el_window.find('.user-otp-state').text(i18n('two_factor_enabled'));
+ });
+
+ $el_window.find('.disable-2fa').on('click', async function (e) {
+ const resp = await fetch(`${api_origin}/auth/configure-2fa/disable`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${puter.authToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ });
+
+ $el_window.find('.enable-2fa').show();
+ $el_window.find('.disable-2fa').hide();
+ $el_window.find('.user-otp-state').text(i18n('two_factor_disabled'));
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/UI/Settings/UIWindowSettings.js b/src/UI/Settings/UIWindowSettings.js
index 280a4903d..133cca307 100644
--- a/src/UI/Settings/UIWindowSettings.js
+++ b/src/UI/Settings/UIWindowSettings.js
@@ -21,6 +21,7 @@ import UIWindow from '../UIWindow.js'
import AboutTab from './UITabAbout.js';
import UsageTab from './UITabUsage.js';
import AccountTab from './UITabAccount.js';
+import SecurityTab from './UITabSecurity.js';
import PersonalizationTab from './UITabPersonalization.js';
import LanguageTab from './UITabLanguage.js';
import ClockTab from './UITabClock.js';
@@ -33,6 +34,7 @@ async function UIWindowSettings(options){
AboutTab,
UsageTab,
AccountTab,
+ SecurityTab,
PersonalizationTab,
LanguageTab,
ClockTab,
diff --git a/src/UI/UIWindowLogin.js b/src/UI/UIWindowLogin.js
index e3e78ada8..355cbd6a4 100644
--- a/src/UI/UIWindowLogin.js
+++ b/src/UI/UIWindowLogin.js
@@ -21,6 +21,8 @@ import UIWindow from './UIWindow.js'
import UIWindowSignup from './UIWindowSignup.js'
import UIWindowRecoverPassword from './UIWindowRecoverPassword.js'
import UIWindowVerificationCode from './UIWindowVerificationCode.js';
+import TeePromise from '../util/TeePromise.js';
+import UIAlert from './UIAlert.js';
async function UIWindowLogin(options){
options = options ?? {};
@@ -165,11 +167,45 @@ async function UIWindowLogin(options){
contentType: "application/json",
data: data,
success: async function (data){
+ let p = Promise.resolve();
if ( data.next_step === 'otp' ) {
- const value = await UIWindowVerificationCode();
- console.log('got value', value);
+ p = new TeePromise();
+ UIWindowVerificationCode({
+ title_key: 'confirm_code_2fa_title',
+ instruction_key: 'confirm_code_2fa_instruction',
+ submit_btn_key: 'confirm_code_2fa_submit_btn',
+ on_value: async ({ actions, win, value }) => {
+ try {
+ const resp = await fetch(`${api_origin}/login/otp`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ token: data.otp_jwt_token,
+ code: value,
+ }),
+ });
+
+ data = await resp.json();
+
+ if ( ! data.proceed ) {
+ actions.clear();
+ actions.show_error(i18n('confirm_code_generic_incorrect'));
+ return;
+ }
+
+ $(win).close();
+ p.resolve();
+ } catch (e) {
+ actions.show_error(e.message ?? i18n('error_unknown_cause'));
+ }
+ },
+ });
}
+ await p;
+
window.update_auth_data(data.token, data.user);
if(options.reload_on_success){
diff --git a/src/UI/UIWindowVerificationCode.js b/src/UI/UIWindowVerificationCode.js
index 2d82bd193..356e9afcc 100644
--- a/src/UI/UIWindowVerificationCode.js
+++ b/src/UI/UIWindowVerificationCode.js
@@ -7,7 +7,6 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio
let is_checking_code = false;
const html_title = i18n(options.title_key || 'confirm_code_generic_title');
- const html_confirm = i18n(options.confirm_key || 'confirm_code_generic_confirm');
const html_instruction = i18n(options.instruction_key || 'confirm_code_generic_instruction');
const submit_btn_txt = i18n(options.submit_btn_key || 'confirm_code_generic_submit');
@@ -29,9 +28,6 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio
`;
h += `
`;
h += ``;
- h += `
`;
- h += `what is this text`;
- h += `
`;
h += `
`;
const el_window = await UIWindow({
@@ -69,11 +65,23 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio
}
});
-
- const p = new TeePromise();
-
$(el_window).find('.digit-input').first().focus();
+ const actions = {
+ clear: () => {
+ final_code = '';
+ $(el_window).find('.code-confirm-btn').prop('disabled', false);
+ $(el_window).find('.code-confirm-btn').html(submit_btn_txt);
+ $(el_window).find('.digit-input').val('');
+ $(el_window).find('.digit-input').first().focus();
+
+ },
+ show_error: (msg) => {
+ $(el_window).find('.error').html(html_encode(msg));
+ $(el_window).find('.error').fadeIn();
+ }
+ };
+
$(el_window).find('.code-confirm-btn').on('click submit', function(e){
e.preventDefault();
e.stopPropagation();
@@ -92,7 +100,11 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio
setTimeout(() => {
console.log('final code', final_code);
- p.resolve(final_code);
+ options.on_value({
+ actions,
+ value: final_code,
+ win: el_window
+ });
}, 1000);
})
diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js
index 37601523f..617a5b031 100644
--- a/src/i18n/translations/en.js
+++ b/src/i18n/translations/en.js
@@ -47,7 +47,10 @@ const en = {
color: 'Color',
hue: 'Hue',
confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.',
+ confirm_code_generic_incorrect: "Incorrect Code.",
confirm_code_generic_title: "Enter Confirmation Code",
+ confirm_code_2fa_instruction: "Enter the 6-digit code from your authenticator app.",
+ confirm_code_2fa_submit_btn: "Submit",
confirm_code_2fa_title: "Enter 2FA Code",
confirm_delete_multiple_items: 'Are you sure you want to permanently delete these items?',
confirm_delete_single_item: 'Do you want to permanently delete this item?',
@@ -83,6 +86,7 @@ const en = {
desktop_background_fit: "Fit",
developers: "Developers",
dir_published_as_website: `%strong% has been published to:`,
+ disable_2fa: 'Disable 2FA',
disassociate_dir: "Disassociate Directory",
download: 'Download',
download_file: 'Download File',
@@ -95,10 +99,12 @@ const en = {
empty_trash: 'Empty Trash',
empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`,
emptying_trash: 'Emptying Trash…',
+ enable_2fa: 'Enable 2FA',
end_hard: "End Hard",
end_process_force_confirm: "Are you sure you want to force-quit this process?",
end_soft: "End Soft",
enter_password_to_confirm_delete_user: "Enter your password to confirm account deletion",
+ error_unknown_cause: "An unknown error occurred.",
feedback: "Feedback",
feedback_c2a: "Please use the form below to send us your feedback, comments, and bug reports.",
feedback_sent_confirmation: "Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.",
@@ -206,6 +212,7 @@ const en = {
scan_qr_c2a: 'Scan the code below to log into this session from other devices',
scan_qr_generic: 'Scan this QR code using your phone or another device',
seconds: 'seconds',
+ security: "Security",
select: "Select",
selected: 'selected',
select_color: 'Select color…',
@@ -238,6 +245,9 @@ const en = {
tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`,
transparency: "Transparency",
trash: 'Trash',
+ two_factor: 'Two Factor Authentication',
+ two_factor_disabled: '2FA Disabled',
+ two_factor_enabled: '2FA Enabled',
type: 'Type',
type_confirm_to_delete_account: "Type 'confirm' to delete your account.",
ui_colors: "UI Colors",