From 497ac8698003fb629ded9685e7bf6b7be2ff3bb1 Mon Sep 17 00:00:00 2001 From: Evert Date: Thu, 24 Aug 2017 21:36:40 +0300 Subject: [PATCH] change license, add user settings, social account unlink --- LICENSE | 29 +++-- documents/privacy-policy.html | 8 +- documents/terms-of-service.html | 16 --- package.json | 2 +- scripts/logger.js | 5 +- server/api/external.js | 24 +++- server/api/index.js | 36 +++++- server/routes/api.js | 47 ++++++- server/routes/index.js | 176 ++++++++++++++++++++++++--- server/routes/oauth2.js | 4 +- src/script/main.js | 1 + src/style/main.styl | 24 ++-- templates/activate/html.pug | 1 + templates/password_alert/html.pug | 6 + templates/password_alert/subject.pug | 1 + views/404.pug | 12 ++ views/document.pug | 3 + views/password_new.pug | 28 +++++ views/settings.pug | 48 ++++++++ 19 files changed, 403 insertions(+), 68 deletions(-) create mode 100644 templates/password_alert/html.pug create mode 100644 templates/password_alert/subject.pug create mode 100644 views/404.pug create mode 100644 views/password_new.pug create mode 100644 views/settings.pug diff --git a/LICENSE b/LICENSE index 2d2f7f1..457524d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,21 @@ - Icy Network Primary Web Application - Authentication and News - Copyright (C) 2017 Icy Network - Evert Prants +MIT License - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +Copyright (c) 2017 Evert Prants - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - You should have received a copy of the GNU General Public License - along with this program. If not, see . +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/documents/privacy-policy.html b/documents/privacy-policy.html index 4f70628..88d180c 100644 --- a/documents/privacy-policy.html +++ b/documents/privacy-policy.html @@ -5,17 +5,17 @@

Icy Network may collect and save some information about our users.

Basic Account Information

Icy Network uses your username to identify you in our system. Please only enter Usernames which you are comfortable with other people seeing.

-

Your email addess is used to send you updates and important information about our Services and the status of your account. You may unsubscribe from update emails at any time. We will never provide, sell, or leak your email address to Third-Party sites nor send you malicious or spam emails. Please always verify that the sender is an IcyNet.eu email address before clicking on any links on emails claiming to be from us. We will never ask you for your personal information or passwords via email.

+

Your email addess is used to send you updates and important information about our Services and the status of your account. You may unsubscribe from update emails at any time. We will never provide, sell, or leak your email address to Third-Party sites nor send you malicious or spam emails. We will never ask you for your personal information or passwords via email.

Additional Information

We use your IP Address to track our visitor traffic in order for us to better allocate resources to the Services which are used the most. Your IP address is not available publicly and is only used internally within our systems.

Cookies

-

Like many websites, we use Cookies in our Services. A Cookie is a small file saved onto your computer by your web browser which contains a bit of information about your presence on Icy Network websites. Icy Network uses temporary session cookies in order to save log-in sessions, which means that you won't have to log in every time you visit our website.

+

Like many websites, we use Cookies in our Services. A Cookie is a small file saved onto your computer by your web browser which contains a bit of information about your presence on Icy Network websites. Icy Network uses temporary session cookies, which are used to save log-in sessions, which means that you won't have to log in every time you visit our website.

External Logins

By logging in from external websites, you agree to these Policies.

Twitter

-

By logging in with Twitter, we will only ask you for your Screen Name, Public Profile Name and Email Address for the above-mentioned purposes. We will never Tweet on your behalf nor see your Tweets.

+

By logging in with Twitter, we will only ask you for your Screen Name, Public Profile Name and Email Address for the above-mentioned purposes. We are unable to Tweet on your behalf nor see your Tweets.

Facebook

-

By logging in with Facebook, we will only ask you for your Public Profile and Email Address. We will use your Name as your Display Name, which can be changed from your Account Settings after logging in. Your profile picture may be downloaded onto our servers and used as your network-wide profile image. You may change your profile picture from your Account Settings at any time. Your Email Address will only be used to send you updates, which you can opt-out of. We can not and will not post on your behalf.

+

By logging in with Facebook, we will only ask you for your Public Profile and Email Address. We will use your Name as your Display Name, which can be changed from your Account Settings after logging in. Your profile picture may be downloaded onto our servers and used as your network-wide profile image. You may change your profile picture from your Account Settings at any time. Your Email Address will only be used to send you updates, which you can opt-out of. We can not: post on your behalf, see your friends list nor see your posts.

Discord

By logging in with Discord, we will only ask you for your Username and Email Address for the above-mentioned purposes. We do not ask you for any other information and we will not know which Discord Servers you're on.

diff --git a/documents/terms-of-service.html b/documents/terms-of-service.html index 574f6b1..2aba74a 100644 --- a/documents/terms-of-service.html +++ b/documents/terms-of-service.html @@ -14,20 +14,4 @@

Credits

Google Play and the Google Play logo are trademarks of Google Inc.

Apple and the Apple logo are trademarks of Apple Inc., registered in the U.S. and other countries. App Store is a service mark of Apple Inc., registered in the U.S. and other countries.

-

Icy Network Software License

-Icy Network Primary Web Application - Authentication and News
-Copyright (C) 2017 Icy Network - Evert Prants <evert@lunasqu.ee>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/package.json b/package.json index dd76b36..292a783 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "authentication" ], "author": "Icy Network", - "license": "GPL-3.0", + "license": "MIT", "bugs": { "url": "https://github.com/IcyNet/IcyNet.eu/issues" }, diff --git a/scripts/logger.js b/scripts/logger.js index d7fb207..a4c9dd2 100644 --- a/scripts/logger.js +++ b/scripts/logger.js @@ -35,9 +35,12 @@ module.exports = function () { this.logProcess = (pid, msg) => { if (msg.indexOf('warn') === 0) { msg = msg.substring(5) + console.warn('[%s] %s', pid, msg) } else if (msg.indexOf('error') === 0) { msg = msg.substring(6) + console.error('[%s] %s', pid, msg) + } else { + console.log('[%s] %s', pid, msg) } - console.log('[%s] %s', pid, msg) } } diff --git a/server/api/external.js b/server/api/external.js index 1eb78f8..ee1bd80 100644 --- a/server/api/external.js +++ b/server/api/external.js @@ -35,6 +35,21 @@ const API = { await await models.External.query().insert(data) return true + }, + remove: async (user, service) => { + user = await UAPI.User.ensureObject(user, ['password']) + let userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id) + + if (!userExterns.length) { + return false + } + + // Do not remove the service the user signed up with + if (userExterns[0] && (user.password === '' || user.password === null) && userExterns[0].service === service) { + return false + } + + return models.External.query().delete().where('user_id', user.id).andWhere('service', service) } }, Facebook: { @@ -97,12 +112,13 @@ const API = { avatar_file: profilepic, activated: 1, ip_address: data.ip_address, - created_at: new Date() + created_at: new Date(), + updated_at: new Date() } // Check if the username is already taken if (await UAPI.User.get(udataLimited.username) != null) { - udataLimited.username = 'FB' + UAPI.Hash(4) + udataLimited.username = udataLimited.username + UAPI.Hash(4) } // Check if the email Facebook gave us is already registered, if so, @@ -209,12 +225,13 @@ const API = { avatar_file: profilepic, activated: 1, ip_address: ipAddress, + updated_at: new Date(), created_at: new Date() } // Check if the username is already taken if (await UAPI.User.get(udataLimited.username) != null) { - udataLimited.username = 'Tw' + UAPI.Hash(4) + udataLimited.username = udataLimited.username + UAPI.Hash(4) } // Check if the email Twitter gave us is already registered, if so, @@ -320,6 +337,7 @@ const API = { avatar_file: profilepic, activated: 1, ip_address: ipAddress, + updated_at: new Date(), created_at: new Date() } diff --git a/server/api/index.js b/server/api/index.js index 78f5ae5..3e96192 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -86,9 +86,42 @@ const API = { return null }, + socialStatus: async function (user) { + user = await API.User.ensureObject(user, ['password']) + if (!user) return null + let external = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id) + let enabled = {} + + for (let i in external) { + let ext = external[i] + enabled[ext.service] = true + } + + let accountSourceIsExternal = user.password === null || user.password === '' + let obj = { + enabled: enabled, + password: !accountSourceIsExternal + } + + if (accountSourceIsExternal) { + obj.source = external[0].service + } + + return obj + }, + update: async function (user, data) { + user = await API.User.ensureObject(user) + if (!user) return {error: 'No such user.'} + + data = Object.assign({ + updated_at: new Date() + }, data) + + return models.User.query().patchAndFetchById(user.id, data) + }, Login: { password: async function (user, password) { - user = await API.User.ensureObject(user) + user = await API.User.ensureObject(user, ['password']) if (!user.password) return false return bcryptTask({task: 'compare', password: password, hash: user.password}) }, @@ -193,6 +226,7 @@ const API = { let email = config.email && config.email.enabled let data = Object.assign(regdata, { created_at: new Date(), + updated_at: new Date(), activated: email ? 0 : 1 }) diff --git a/server/routes/api.js b/server/routes/api.js index a236db6..06e07f9 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -62,12 +62,7 @@ function createSession (req, user) { // Either give JSON or make a redirect function JsonData (req, res, error, redirect = '/') { - if (req.headers['content-type'] === 'application/json') { - return res.jsonp({error: error, redirect: redirect}) - } - - req.flash('message', {error: true, text: error}) - res.redirect(redirect) + res.jsonp({error: error, redirect: redirect}) } /** FACEBOOK LOGIN @@ -94,6 +89,17 @@ router.post('/external/facebook/callback', wrap(async (req, res) => { JsonData(req, res, null, uri) })) +router.get('/external/facebook/remove', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + let done = await APIExtern.Common.remove(req.session.user, 'fb') + + if (!done) { + req.flash('message', {error: true, text: 'Unable to unlink social media account'}) + } + + res.redirect('/user/manage') +})) + /** TWITTER LOGIN * OAuth1.0a flows * Tokens in configs @@ -147,6 +153,18 @@ router.get('/external/twitter/callback', wrap(async (req, res) => { res.redirect(uri) })) +router.get('/external/twitter/remove', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + let done = await APIExtern.Common.remove(req.session.user, 'twitter') + + if (!done) { + req.flash('message', {error: true, text: 'Unable to unlink social media account'}) + } + + res.redirect('/user/manage') +})) + /** DISCORD LOGIN * OAuth2 flows * Tokens in configs @@ -205,6 +223,18 @@ router.get('/external/discord/callback', wrap(async (req, res) => { res.redirect(uri) })) +router.get('/external/discord/remove', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + let done = await APIExtern.Common.remove(req.session.user, 'discord') + + if (!done) { + req.flash('message', {error: true, text: 'Unable to unlink social media account'}) + } + + res.redirect('/user/manage') +})) + /* ======== * NEWS * ======== @@ -247,4 +277,9 @@ router.get('/news', wrap(async (req, res) => { res.jsonp(articles) })) +// 404 +router.use((req, res) => { + res.status(404).jsonp({error: 'Not found'}) +}) + module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 71b5e48..b1c2fa8 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -8,6 +8,7 @@ import wrap from '../../scripts/asyncRoute' import http from '../../scripts/http' import API from '../api' import News from '../api/news' +import emailer from '../api/emailer' import apiRouter from './api' import oauthRouter from './oauth2' @@ -21,6 +22,17 @@ let accountLimiter = new RateLimit({ message: 'Whoa, slow down there, buddy! You just hit our rate limits. Try again in 1 hour.' }) +function setSession (req, user) { + req.session.user = { + id: user.id, + username: user.username, + display_name: user.display_name, + email: user.email, + avatar_file: user.avatar_file, + session_refresh: Date.now() + 1800000 // 30 minutes + } +} + router.use(wrap(async (req, res, next) => { let messages = req.flash('message') if (!messages || !messages.length) { @@ -29,6 +41,18 @@ router.use(wrap(async (req, res, next) => { messages = messages[0] } + // Update user session every 30 minutes + if (req.session.user) { + if (!req.session.user.session_refresh) { + req.session.user.session_refresh = Date.now() + 1800000 + } + + if (req.session.user.session_refresh < Date.now()) { + let udata = await API.User.get(req.session.user.id) + setSession(req, udata) + } + } + res.locals.message = messages next() })) @@ -117,6 +141,49 @@ router.get('/login/verify', (req, res) => { res.render('totp-check') }) +router.get('/user/manage', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + let totpEnabled = false + let socialStatus = await API.User.socialStatus(req.session.user) + + if (socialStatus.password) { + totpEnabled = await API.User.Login.totpTokenRequired(req.session.user) + } + + if (config.twitter && config.twitter.api) { + if (!socialStatus.enabled.twitter) { + res.locals.twitter_auth = true + } else if (!socialStatus.source && socialStatus.source !== 'twitter') { + res.locals.twitter_auth = false + } + } + + if (config.discord && config.discord.api) { + if (!socialStatus.enabled.discord) { + res.locals.discord_auth = true + } else if (!socialStatus.source && socialStatus.source !== 'discord') { + res.locals.discord_auth = false + } + } + + if (config.facebook && config.facebook.client) { + if (!socialStatus.enabled.fb) { + res.locals.facebook_auth = config.facebook.client + } else if (!socialStatus.source && socialStatus.source !== 'fb') { + res.locals.facebook_auth = false + } + } + + res.render('settings', {totp: totpEnabled, password: socialStatus.password}) +})) + +router.get('/user/manage/password', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + res.render('password_new') +})) + /* ================= POST HANDLING @@ -125,10 +192,9 @@ router.get('/login/verify', (req, res) => { function formError (req, res, error, redirect) { // Security measures: never store any passwords in any session - if (req.body.password) { - delete req.body.password - if (req.body.password_repeat) { - delete req.body.password_repeat + for (let key in req.body) { + if (key.indexOf('password') !== -1) { + delete req.body[key] } } @@ -239,14 +305,7 @@ router.post('/login', wrap(async (req, res) => { // TODO: Ban checks // Set session - req.session.user = { - id: user.id, - username: user.username, - display_name: user.display_name, - email: user.email, - avatar_file: user.avatar_file, - session_refresh: Date.now() + 1800000 // 30 minutes - } + setSession(req, user) let uri = '/' if (req.session.redirectUri) { @@ -340,6 +399,91 @@ router.post('/register', accountLimiter, wrap(async (req, res) => { res.redirect('/login') })) +router.post('/user/manage', wrap(async (req, res, next) => { + if (!req.session.user) return next() + + if (req.body.csrf !== req.session.csrf) { + return formError(req, res, 'Invalid session! Try reloading the page.') + } + + if (!req.body.display_name) { + return formError(req, res, 'Display Name cannot be blank.') + } + + let displayName = req.body.display_name + if (!displayName || !displayName.match(/^([^\\`]{3,32})$/i)) { + return formError(req, res, 'Invalid display name!') + } + + // No change + if (displayName === req.session.user.display_name) { + return res.redirect('/user/manage') + } + + let success = await API.User.update(req.session.user, { + display_name: displayName + }) + + if (success.error) { + return formError(req, res, success.error) + } + + req.session.user.display_name = displayName + + req.flash('message', {error: false, text: 'Settings changed successfully. Please note that it may take time to update on other websites and devices.'}) + res.redirect('/user/manage') +})) + +router.post('/user/manage/password', wrap(async (req, res, next) => { + if (!req.session.user) return next() + + if (req.body.csrf !== req.session.csrf) { + return formError(req, res, 'Invalid session! Try reloading the page.') + } + + if (!req.body.password_old) { + return formError(req, res, 'Please enter your current password.') + } + + let passwordMatch = await API.User.Login.password(req.session.user, req.body.password_old) + if (!passwordMatch) { + return formError(req, res, 'The password you provided is incorrect.') + } + + let password = req.body.password + if (!password || password.length < 8 || password.length > 32) { + return formError(req, res, 'Invalid password! Keep it between 8 and 32 characters!') + } + + let passwordAgain = req.body.password_repeat + if (!passwordAgain || password !== passwordAgain) { + return formError(req, res, 'The passwords do not match!') + } + + password = await API.User.Register.hashPassword(password) + + let success = await API.User.update(req.session.user, { + password: password + }) + + if (success.error) { + return formError(req, res, success.error) + } + + let user = req.session.user + console.warn('[SECURITY AUDIT] User \'%s\' password has been changed from %s', user.username, req.realIP) + + if (config.email && config.email.enabled) { + await emailer.pushMail('password_alert', user.email, { + display_name: user.display_name, + ip: req.realIP + }) + } + + req.flash('message', {error: false, text: 'Password changed successfully.'}) + return res.redirect('/user/manage') +})) + /* ============= DOCUMENTS @@ -347,10 +491,10 @@ router.post('/register', accountLimiter, wrap(async (req, res) => { */ const docsDir = path.join(__dirname, '../../documents') -router.get('/docs/:name', (req, res) => { +router.get('/docs/:name', (req, res, next) => { let doc = path.join(docsDir, req.params.name + '.html') if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) { - return res.status(404).end() + return next() } doc = fs.readFileSync(doc, {encoding: 'utf8'}) @@ -410,6 +554,10 @@ router.get('/activate/:token', wrap(async (req, res) => { router.use('/api', apiRouter) +router.use((req, res) => { + res.status(404).render('404') +}) + router.use((err, req, res, next) => { console.error(err) next() diff --git a/server/routes/oauth2.js b/server/routes/oauth2.js index af0f6be..fec7965 100644 --- a/server/routes/oauth2.js +++ b/server/routes/oauth2.js @@ -49,12 +49,12 @@ router.get('/user', oauth.bearer, wrap(async (req, res) => { } // Include Email - if (accessToken.scope.indexOf('email') != -1) { + if (accessToken.scope.indexOf('email') !== -1) { udata.email = user.email } // Include privilege number - if (accessToken.scope.indexOf('privilege') != -1) { + if (accessToken.scope.indexOf('privilege') !== -1) { udata.privilege = user.nw_privilege } diff --git a/src/script/main.js b/src/script/main.js index 422a2d6..34315c7 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -109,6 +109,7 @@ $(document).ready(function () { dataType: 'json', data: response, success: function (data) { + console.log(data) if (data.error) { $('.message').addClass('error') $('.message span').text(data.error) diff --git a/src/style/main.styl b/src/style/main.styl index 084598f..2a38c13 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -231,6 +231,14 @@ input:not([type="submit"]) .boxcont .box + padding: 20px + margin: auto + margin-top: 5% + background-color: #fff + box-shadow: 5px 5px 15px #868686 + border: 1px solid #ddd + h1:first-child, h2:first-child, h3:first-child + margin-top: 0 .left, .right display: inline-block width: 50% @@ -241,16 +249,13 @@ input:not([type="submit"]) width: 46% &#login max-width: 700px + &#settings + max-width: 700px + min-height: 380px &#totpcheck max-width: 400px - padding: 20px - margin: auto - margin-top: 5% - background-color: #fff - box-shadow: 5px 5px 15px #868686 - border: 1px solid #ddd - h1, h2, h3 - margin-top: 0 + &#error + width: 200px .pgn display: inline-block @@ -423,6 +428,9 @@ input.invalid border-color: #ffffff margin: 20px 0 +.option + display: block + @media all and (max-width: 800px) .navigator padding: 0 10px diff --git a/templates/activate/html.pug b/templates/activate/html.pug index 07417fe..8a05ee1 100644 --- a/templates/activate/html.pug +++ b/templates/activate/html.pug @@ -3,3 +3,4 @@ p Before you can log in, you must activate your account. p Click on or copy the following link into your URL bar in order to activate your Icy Network account a.activate(href=domain + "/activate/" + activation_token, target="_blank", rel="nofollow")= domain + "/activate/" + activation_token p If you did not register for an account on Icy Network, please ignore this email. +small This email has been sent to you because of an action performed on the IcyNet.eu website. diff --git a/templates/password_alert/html.pug b/templates/password_alert/html.pug new file mode 100644 index 0000000..7d85c3f --- /dev/null +++ b/templates/password_alert/html.pug @@ -0,0 +1,6 @@ +h3 Hello, #{display_name}! +p This email was sent to you because your password on Icy Network has been changed. +p If you did indeed change the password of this account yourself, you may safely ignore this email. +p + b However, if you did not change your password on Icy Network, contact an Icy Network administrator immediately! +small This email has been sent to you because of an action performed on the IcyNet.eu website. diff --git a/templates/password_alert/subject.pug b/templates/password_alert/subject.pug new file mode 100644 index 0000000..800ad01 --- /dev/null +++ b/templates/password_alert/subject.pug @@ -0,0 +1 @@ +|Your Password on Icy Network has been changed! diff --git a/views/404.pug b/views/404.pug new file mode 100644 index 0000000..69ebcba --- /dev/null +++ b/views/404.pug @@ -0,0 +1,12 @@ +extends layout.pug +block title + |Icy Network - 404 + +block body + .wrapper + .boxcont + .box#error + h1 404 + p Page not found + a(href="..") Go back + diff --git a/views/document.pug b/views/document.pug index a0e4d26..4159a25 100644 --- a/views/document.pug +++ b/views/document.pug @@ -1,5 +1,8 @@ extends layout.pug +block title + |Icy Network - Legal Notice + block body .document .content !{doc} diff --git a/views/password_new.pug b/views/password_new.pug new file mode 100644 index 0000000..806a952 --- /dev/null +++ b/views/password_new.pug @@ -0,0 +1,28 @@ +extends layout.pug +block title + |Icy Network - Change User Password + +block body + .wrapper + .boxcont + .box#totpcheck + h1 Change Your Password + if message + if message.error + .message.error + else + .message + span #{message.text} + form#loginForm(method="POST", action="") + input(type="hidden", name="csrf", value=csrf) + if !token + label(for="password_old") Current Password + input(type="password", name="password_old", id="password_old") + else + input(type="hidden", name="token", value=token) + label(for="password") New Password + input(type="password", name="password", id="password") + label(for="password_repeat") Repeat New Password + input(type="password", name="password_repeat", id="password_repeat") + div#repeatcheck(style="display: none") + input(type="submit", value="Change") diff --git a/views/settings.pug b/views/settings.pug new file mode 100644 index 0000000..b92d0eb --- /dev/null +++ b/views/settings.pug @@ -0,0 +1,48 @@ +extends layout.pug +block title + |Icy Network - User Settings + +block body + .wrapper + .boxcont + .box#settings + h1 User Settings + .left + if message + if message.error + .message.error + else + .message + span #{message.text} + form#loginForm(method="POST", action="") + input(type="hidden", name="csrf", value=csrf) + label(for="username") Username + input(type="text", name="username", id="username", value=user.username, disabled) + label(for="display_name") Display Name + input(type="text", name="display_name", id="display_name", value=user.display_name) + input(type="submit", value="Save Settings") + .right + h3 Social Media Accounts + include includes/external.pug + if twitter_auth == false + a.option(href="/api/external/twitter/remove") Unlink Twitter + if facebook_auth == false + a.option(href="/api/external/facebook/remove") Unlink Facebook + if discord_auth == false + a.option(href="/api/external/discord/remove") Unlink Discord + h3 Other Options + if password + a.option(href="/user/manage/password") + i.fa.fa-fw.fa-lock + |Change Password + if totp + a.option(href="/user/two-factor/disable") + i.fa.fa-fw.fa-lock + |Disable Two-Factor Authentication + else + a.option(href="/user/two-factor") + i.fa.fa-fw.fa-lock + |Enable Two-Factor Authentication + a.option(href="/user/manage/email") + i.fa.fa-fw.fa-envelope + |Change Email Address