diff --git a/server/api/index.js b/server/api/index.js index c5baed3..b389436 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -297,6 +297,47 @@ const API = { return {error: null, user: user} } + }, + OAuth2: { + getUserAuthorizations: async function (user) { + user = await API.User.ensureObject(user) + let auths = await models.OAuth2AuthorizedClient.query().where('user_id', user.id) + + let nicelist = [] + + for (let i in auths) { + let auth = auths[i] + let client = await models.OAuth2Client.query().where('id', auth.client_id) + + if (!client.length) continue + client = client[0] + + let obj = { + id: client.id, + title: client.title, + description: client.description, + url: client.url, + icon: client.icon, + scope: client.scope.split(' '), + created_at: auth.created_at, + expires_at: auth.expires_at + } + nicelist.push(obj) + } + + return nicelist + }, + removeUserAuthorization: async function (user, clientId) { + user = await API.User.ensureObject(user) + let auth = await models.OAuth2AuthorizedClient.query().where('user_id', user.id).andWhere('client_id', clientId) + if (!auth.length) return false + + for (let i in auth) { + await models.OAuth2AuthorizedClient.query().delete().where('id', auth[i].id) + } + + return true + } } } } diff --git a/server/routes/api.js b/server/routes/api.js index f433a80..2fc9774 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -9,7 +9,7 @@ import News from '../api/news' import Image from '../api/image' import APIExtern from '../api/external' -const userContent = path.join(__dirname, '../..', 'usercontent') +// const userContent = path.join(__dirname, '../..', 'usercontent') let router = express.Router() @@ -289,6 +289,11 @@ router.get('/news', wrap(async (req, res) => { res.jsonp(articles) })) +/* ========== + * AVATAR + * ========== + */ +// Promisify multiparty form parser async function promiseForm (req) { let form = new multiparty.Form() return new Promise(function (resolve, reject) { @@ -299,6 +304,7 @@ async function promiseForm (req) { }) } +// Upload avatar image router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => { if (!req.session.user) return next() let data = await promiseForm(req) @@ -320,6 +326,7 @@ router.post('/avatar', uploadLimiter, wrap(async (req, res, next) => { res.status(200).jsonp({}) })) +// Remove avatar image router.post('/avatar/remove', wrap(async (req, res, next) => { if (!req.session.user) return next() @@ -329,6 +336,7 @@ router.post('/avatar/remove', wrap(async (req, res, next) => { res.status(200).jsonp({done: true}) })) +// Get latest avatar of logged in user router.get('/avatar', wrap(async (req, res, next) => { if (!req.session.user) return next() let user = req.session.user @@ -339,6 +347,7 @@ router.get('/avatar', wrap(async (req, res, next) => { res.redirect('/usercontent/images/' + user.avatar_file) })) +// Get latest avatar of user by id router.get('/avatar/:id', wrap(async (req, res, next) => { let id = parseInt(req.params.id) if (isNaN(id)) return next() @@ -351,10 +360,37 @@ router.get('/avatar/:id', wrap(async (req, res, next) => { res.redirect('/usercontent/images/' + user.avatar_file) })) +// Redirect to no avatar on 404 router.use('/avatar', (req, res) => { res.redirect('/static/image/avatar.png') }) +/* ===================== + * OAuth2 Management + * ===================== + */ + +router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => { + if (!req.session.user) return next() + + let list = await API.User.OAuth2.getUserAuthorizations(req.session.user) + if (!list) return next() + + res.jsonp(list) +})) + +router.post('/oauth2/authorized-clients/delete', wrap(async (req, res, next) => { + if (!req.session.user) return next() + + let clientId = parseInt(req.body.client_id) + if (isNaN(clientId)) return res.status(400).jsonp({error: 'Missing Client ID parameter'}) + + let done = await API.User.OAuth2.removeUserAuthorization(req.session.user, clientId) + if (!done) return res.status(400).jsonp({error: 'Failed to remove client authorization'}) + + res.status(204).end() +})) + // 404 router.use((req, res) => { res.status(404).jsonp({error: 'Not found'}) diff --git a/server/routes/index.js b/server/routes/index.js index 89bea9d..4eaf15c 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -102,7 +102,7 @@ router.get('/login', extraButtons, (req, res) => { return res.redirect(uri) } - res.render('login') + res.render('user/login') }) router.get('/register', extraButtons, (req, res) => { @@ -121,7 +121,7 @@ router.get('/register', extraButtons, (req, res) => { res.locals.recaptcha = config.security.recaptcha.site_key } - res.render('register') + res.render('user/register') }) router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => { @@ -131,18 +131,18 @@ router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => { let newToken = await API.User.Login.totpAquire(req.session.user) if (!newToken) return res.redirect('/') - res.render('totp', { uri: newToken }) + res.render('user/totp', { uri: newToken }) })) router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => { let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user) if (!twoFaEnabled) return res.redirect('/') - res.render('password') + res.render('user/password') })) router.get('/login/verify', (req, res) => { - res.render('totp-check') + res.render('user/totp-check') }) router.get('/user/manage', ensureLogin, wrap(async (req, res) => { @@ -177,11 +177,11 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => { } } - res.render('settings', {totp: totpEnabled, password: socialStatus.password}) + res.render('user/settings', {totp: totpEnabled, password: socialStatus.password}) })) router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => { - res.render('password_new') + res.render('user/password_new') })) router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => { @@ -194,7 +194,7 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => { let socialStatus = await API.User.socialStatus(req.session.user) - res.render('email_change', {email: obfuscated, password: socialStatus.password}) + res.render('user/email_change', {email: obfuscated, password: socialStatus.password}) })) /* @@ -622,7 +622,7 @@ router.get('/news/', wrap(async (req, res) => { router.get('/partials/:view', wrap(async (req, res, next) => { if (!req.params.view) return next() - res.render('partials/' + req.params.view) + res.render('user/partials/' + req.params.view) })) /* diff --git a/src/script/main.js b/src/script/main.js index b5e877d..8065132 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -36,6 +36,61 @@ $(document).ready(function () { return newWindow } + function removeAuthorization (clientId) { + $.ajax({ + type: 'post', + url: '/api/oauth2/authorized-clients/delete', + data: { client_id: clientId }, + success: function (data) { + loadAuthorizations() + } + }) + } + + function loadAuthorizations () { + $.get({ + url: '/api/oauth2/authorized-clients', + dataType: 'json', + success: function (data) { + if (!data.length) { + return $('#clientlist').html('There is nothing to show at this moment.') + } + var html = '' + for (var i in data) { + var client = data[i] + html += '
' + html += '
' + html += '
' + + if (client.icon) { + html += '' + } else { + html += '
' + } + + html += '
' + html += '
' + html += '
' + client.title + '
' + html += '
' + client.description + '
' + html += '' + client.url + '' + html += '
Authorized ' + new Date(client.created_at) + '
' + html += '
' + } + + $('#clientlist').html(html) + + for (let i in data) { + $('#client-' + data[i].id + ' #deleteclient').click(function (e) { + let clid = $(this).parent().data('client-id') + if (clid != null) { + removeAuthorization(clid) + } + }) + } + } + }) + } + window.Dialog = $('#dialog') window.Dialog.open = function (title, content, pad) { $('#dialog #title').text(title) @@ -152,6 +207,10 @@ $(document).ready(function () { }) } + if ($('#clientlist').length) { + loadAuthorizations() + } + if ($('#newAvatar').length) { $('#newAvatar').click(function (e) { e.preventDefault() diff --git a/src/style/main.styl b/src/style/main.styl index 0db3408..cc7e7e2 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -237,6 +237,7 @@ input:not([type="submit"]) background-color: #fff box-shadow: 5px 5px 15px #868686 border: 1px solid #ddd + margin-bottom: 10% h1:first-child, h2:first-child, h3:first-child margin-top: 0 .left, .right @@ -360,6 +361,11 @@ span.divider .application height: 140px + #deleteclient + float: right + color: red + font-size: 120% + cursor: pointer .picture width: 120px height: 120px @@ -387,6 +393,7 @@ span.divider display: block text-overflow: ellipsis overflow: hidden + max-width: 200px input.authorize background-color: #00b0ff @@ -421,12 +428,13 @@ input.invalid margin-top: 20px margin-bottom: 20px +span.load + font-size: 120% + span + vertical-align: super + margin-left: 10px + .newsfeed - .load - font-size: 120% - span - vertical-align: super - margin-left: 10px .prvarticle margin-bottom: 10px .title diff --git a/views/email_change.pug b/views/user/email_change.pug similarity index 97% rename from views/email_change.pug rename to views/user/email_change.pug index 6af4295..79385ee 100644 --- a/views/email_change.pug +++ b/views/user/email_change.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Change User Email diff --git a/views/login.pug b/views/user/login.pug similarity index 91% rename from views/login.pug rename to views/user/login.pug index 2a32240..fe6e6a8 100644 --- a/views/login.pug +++ b/views/user/login.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Log In @@ -24,4 +24,4 @@ block body input(type="submit", value="Log in") a#create(href="/register") Create an account .right - include includes/external.pug + include ../includes/external.pug diff --git a/views/partials/avatar.pug b/views/user/partials/avatar.pug similarity index 99% rename from views/partials/avatar.pug rename to views/user/partials/avatar.pug index bca2935..4661223 100644 --- a/views/partials/avatar.pug +++ b/views/user/partials/avatar.pug @@ -4,7 +4,7 @@ .otherdata h3 Current Avatar .avatar - include ../includes/avatar.pug + include ../../includes/avatar.pug .inputting h3 Upload new .message.error diff --git a/views/password.pug b/views/user/password.pug similarity index 96% rename from views/password.pug rename to views/user/password.pug index eadfbe2..a4e4ee0 100644 --- a/views/password.pug +++ b/views/user/password.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Password Required diff --git a/views/password_new.pug b/views/user/password_new.pug similarity index 97% rename from views/password_new.pug rename to views/user/password_new.pug index d0381de..eed795f 100644 --- a/views/password_new.pug +++ b/views/user/password_new.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Change User Password diff --git a/views/register.pug b/views/user/register.pug similarity index 95% rename from views/register.pug rename to views/user/register.pug index 1f18486..6b62a99 100644 --- a/views/register.pug +++ b/views/user/register.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Register @@ -34,4 +34,4 @@ block body input(type="submit", value="Register") a#create(href="/login") Log in with an existing account .right - include includes/external.pug + include ../includes/external.pug diff --git a/views/settings.pug b/views/user/settings.pug similarity index 88% rename from views/settings.pug rename to views/user/settings.pug index a7a1db6..7031bf3 100644 --- a/views/settings.pug +++ b/views/user/settings.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - User Settings @@ -23,7 +23,7 @@ block body input(type="text", name="display_name", id="display_name", value=user.display_name) label Avatar .avatarCont - include includes/avatar.pug + include ../includes/avatar.pug .options a#newAvatar(href='#') Change Avatar if user.avatar_file @@ -31,7 +31,7 @@ block body input(type="submit", value="Save Settings") .right h3 Social Media Accounts - include includes/external.pug + include ../includes/external.pug if twitter_auth == false a.option.accdisconnect(href="/api/external/twitter/remove") i.fa.fa-fw.fa-times @@ -60,3 +60,9 @@ block body a.option(href="/user/manage/email") i.fa.fa-fw.fa-envelope |Change Email Address + .clients + h2 OAuth2 Authorized Clients + .cl#clientlist + span.load + i.fa.fa-spin.fa-spinner.fa-2x + span Loading list diff --git a/views/totp-check.pug b/views/user/totp-check.pug similarity index 96% rename from views/totp-check.pug rename to views/user/totp-check.pug index b3beef3..1bdc165 100644 --- a/views/totp-check.pug +++ b/views/user/totp-check.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Log In - Verification Required diff --git a/views/totp.pug b/views/user/totp.pug similarity index 98% rename from views/totp.pug rename to views/user/totp.pug index 1629c0c..474df40 100644 --- a/views/totp.pug +++ b/views/user/totp.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - Activate Authenticator