more comments
This commit is contained in:
parent
e92bcf5521
commit
c6824bff01
20
README.md
20
README.md
@ -12,21 +12,19 @@ Icy Network is a community network aimed at anyone who likes friendly discussion
|
||||
More to come!
|
||||
|
||||
## The Goal of this Application
|
||||
This application is used for authentication services such as OAuth2 in order to unite our websites with a single login and as a centeral news outlet for Icy Network services.
|
||||
This application is used for authentication services such as OAuth2 in order to unite our websites with a single login and as a central news outlet for Icy Network services.
|
||||
|
||||
## Setup
|
||||
The first time you run the application, it will migrate the database and that may take a while.
|
||||
The first time you run the application, it will migrate the database and that may take a while. **You will also need a running instance of `redis-server` for session storage!**
|
||||
### Development
|
||||
Clone this repository and then
|
||||
|
||||
1. `npm install` to get all the packages
|
||||
2. `cp config.example.toml config.toml` copy the configuration
|
||||
3. `npm run watch` to run the style and front-end script building watch task
|
||||
4. `npm start -- -d` to start the application in development mode
|
||||
1. Clone this repository and `cd` into it
|
||||
2. `npm install` - Get all the dependencies
|
||||
3. `cp config.example.toml config.toml` - Copy the configuration
|
||||
4. `npm run watch` - Run the style and front-end script watch task
|
||||
5. `npm start -- -d` - Start the application in development mode
|
||||
|
||||
There is also a watch mode for the server. To enable `server` file tree watching you must provide both `-d` and `-w` as parameters. This task will reset all workers when any file in the `server` directory changes, enabling for live debugging.
|
||||
|
||||
### Production
|
||||
|
||||
1. `npm run build`
|
||||
2. `npm start`
|
||||
1. `npm run build` - Build the front-end
|
||||
2. `npm start` - Start the application in production mode
|
||||
|
@ -4,11 +4,12 @@
|
||||
port=8282
|
||||
# Session key
|
||||
session_key="Session"
|
||||
# Session secret
|
||||
# Session secret (keep this a secret)
|
||||
session_secret="hackmysessions"
|
||||
# Number of worker processes (0 to use all CPU cores)
|
||||
workers=1
|
||||
# Domain of this application
|
||||
# Used for the links in emails
|
||||
domain="http://localhost:8282"
|
||||
|
||||
# Database
|
||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -2090,7 +2090,7 @@
|
||||
"merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"methods": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"parseurl": "1.3.1",
|
||||
"path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz",
|
||||
"qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
|
||||
@ -2131,7 +2131,7 @@
|
||||
"debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
|
||||
"depd": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
||||
"on-headers": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
|
||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"parseurl": "1.3.1",
|
||||
"uid-safe": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz",
|
||||
"utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
||||
},
|
||||
@ -2222,7 +2222,7 @@
|
||||
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"parseurl": "1.3.1",
|
||||
"statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||
"unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
},
|
||||
@ -3835,7 +3835,8 @@
|
||||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY="
|
||||
},
|
||||
"path-browserify": {
|
||||
@ -4639,7 +4640,7 @@
|
||||
"requires": {
|
||||
"encodeurl": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||
"escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||
"parseurl": "1.3.1",
|
||||
"send": "https://registry.npmjs.org/send/-/send-0.15.3.tgz"
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
import express from 'express'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import path from 'path'
|
||||
import multiparty from 'multiparty'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
@ -9,16 +8,17 @@ import News from '../api/news'
|
||||
import Image from '../api/image'
|
||||
import APIExtern from '../api/external'
|
||||
|
||||
// const userContent = path.join(__dirname, '../..', 'usercontent')
|
||||
|
||||
let router = express.Router()
|
||||
let dev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
// Restrict API usage
|
||||
let apiLimiter = new RateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 100,
|
||||
delayMs: 0
|
||||
})
|
||||
|
||||
// Restrict image uploads
|
||||
let uploadLimiter = new RateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
@ -81,7 +81,8 @@ function JsonData (req, res, error, redirect = '/') {
|
||||
* Ajax POST only <in-page javascript handeled>
|
||||
* No tokens saved in configs, everything works out-of-the-box
|
||||
*/
|
||||
router.post('/external/facebook/callback', wrap(async (req, res) => {
|
||||
router.post('/external/facebook/callback', wrap(async (req, res, next) => {
|
||||
if (!config.facebook || !config.facebook.client) return next()
|
||||
let sane = objectAssembler(req.body)
|
||||
sane.ip_address = req.realIP
|
||||
|
||||
@ -252,14 +253,19 @@ router.get('/external/discord/remove', wrap(async (req, res) => {
|
||||
* ========
|
||||
*/
|
||||
|
||||
// Get page of articles
|
||||
// Cache news for one day
|
||||
router.get('/news', (req, res, next) => {
|
||||
if (!dev) res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day
|
||||
next()
|
||||
})
|
||||
|
||||
// Get a page of articles
|
||||
router.get('/news/all/:page', wrap(async (req, res) => {
|
||||
if (!req.params.page || isNaN(parseInt(req.params.page))) {
|
||||
return res.status(400).jsonp({error: 'Invalid page number.'})
|
||||
}
|
||||
|
||||
let page = parseInt(req.params.page)
|
||||
|
||||
let articles = await News.listNews(page)
|
||||
|
||||
res.jsonp(articles)
|
||||
@ -277,8 +283,8 @@ router.get('/news/:id', wrap(async (req, res) => {
|
||||
}
|
||||
|
||||
let id = parseInt(req.params.id)
|
||||
|
||||
let article = await News.article(id)
|
||||
|
||||
res.jsonp(article)
|
||||
}))
|
||||
|
||||
@ -370,6 +376,7 @@ router.use('/avatar', (req, res) => {
|
||||
* =====================
|
||||
*/
|
||||
|
||||
// List authorizations
|
||||
router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
|
||||
@ -379,7 +386,8 @@ router.get('/oauth2/authorized-clients', wrap(async (req, res, next) => {
|
||||
res.jsonp(list)
|
||||
}))
|
||||
|
||||
router.post('/oauth2/authorized-clients/delete', wrap(async (req, res, next) => {
|
||||
// Revoke an authorization
|
||||
router.post('/oauth2/authorized-clients/revoke', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
|
||||
let clientId = parseInt(req.body.client_id)
|
||||
|
@ -2,7 +2,6 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import parseurl from 'parseurl'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
import http from '../../scripts/http'
|
||||
@ -15,6 +14,7 @@ import oauthRouter from './oauth2'
|
||||
|
||||
let router = express.Router()
|
||||
|
||||
// Restrict account creation
|
||||
let accountLimiter = new RateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10,
|
||||
@ -22,6 +22,7 @@ let accountLimiter = new RateLimit({
|
||||
message: 'Whoa, slow down there, buddy! You just hit our rate limits. Try again in 1 hour.'
|
||||
})
|
||||
|
||||
// Set the user session
|
||||
function setSession (req, user) {
|
||||
req.session.user = {
|
||||
id: user.id,
|
||||
@ -34,6 +35,7 @@ function setSession (req, user) {
|
||||
}
|
||||
|
||||
router.use(wrap(async (req, res, next) => {
|
||||
// Add form messages into the template rendering if present
|
||||
let messages = req.flash('message')
|
||||
if (!messages || !messages.length) {
|
||||
messages = {}
|
||||
@ -85,6 +87,8 @@ function extraButtons (req, res, next) {
|
||||
next()
|
||||
}
|
||||
|
||||
// Make sure the user is logged in
|
||||
// Redirect to login page and store the current path in the session for redirecting later
|
||||
function ensureLogin (req, res, next) {
|
||||
if (req.session.user) return next()
|
||||
req.session.redirectUri = req.originalUrl
|
||||
@ -124,6 +128,7 @@ router.get('/register', extraButtons, (req, res) => {
|
||||
res.render('user/register')
|
||||
})
|
||||
|
||||
// View for enabling Two-Factor Authentication
|
||||
router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
|
||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||
if (twoFaEnabled) return res.redirect('/')
|
||||
@ -134,6 +139,7 @@ router.get('/user/two-factor', ensureLogin, wrap(async (req, res) => {
|
||||
res.render('user/totp', { uri: newToken })
|
||||
}))
|
||||
|
||||
// View for disabling Two-Factor Authentication
|
||||
router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
|
||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||
|
||||
@ -141,10 +147,12 @@ router.get('/user/two-factor/disable', ensureLogin, wrap(async (req, res) => {
|
||||
res.render('user/password')
|
||||
}))
|
||||
|
||||
// Two-Factor Authentication verification on login
|
||||
router.get('/login/verify', (req, res) => {
|
||||
res.render('user/totp-check')
|
||||
})
|
||||
|
||||
// User settings page
|
||||
router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
|
||||
let totpEnabled = false
|
||||
let socialStatus = await API.User.socialStatus(req.session.user)
|
||||
@ -180,10 +188,12 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
|
||||
res.render('user/settings', {totp: totpEnabled, password: socialStatus.password})
|
||||
}))
|
||||
|
||||
// Change password
|
||||
router.get('/user/manage/password', ensureLogin, wrap(async (req, res) => {
|
||||
res.render('user/password_new')
|
||||
}))
|
||||
|
||||
// Change email
|
||||
router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
||||
let obfuscated = req.session.user.email
|
||||
if (obfuscated) {
|
||||
@ -203,6 +213,7 @@ router.get('/user/manage/email', ensureLogin, wrap(async (req, res) => {
|
||||
=================
|
||||
*/
|
||||
|
||||
// Used to display errors on forms and save data
|
||||
function formError (req, res, error, redirect) {
|
||||
// Security measures: never store any passwords in any session
|
||||
for (let key in req.body) {
|
||||
@ -213,9 +224,10 @@ function formError (req, res, error, redirect) {
|
||||
|
||||
req.flash('formkeep', req.body || {})
|
||||
req.flash('message', {error: true, text: error})
|
||||
res.redirect(redirect || parseurl(req).path)
|
||||
res.redirect(redirect || req.originalUrl)
|
||||
}
|
||||
|
||||
// Make sure characters are UTF-8
|
||||
function cleanString (input) {
|
||||
let output = ''
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
@ -224,6 +236,7 @@ function cleanString (input) {
|
||||
return output
|
||||
}
|
||||
|
||||
// Enabling 2fa
|
||||
router.post('/user/two-factor', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
if (!req.body.code) {
|
||||
@ -236,12 +249,13 @@ router.post('/user/two-factor', wrap(async (req, res, next) => {
|
||||
|
||||
let verified = await API.User.Login.totpCheck(req.session.user, req.body.code)
|
||||
if (!verified) {
|
||||
return formError(req, res, 'Try again!')
|
||||
return formError(req, res, 'Something went wrong! Try scanning the code again.')
|
||||
}
|
||||
|
||||
res.redirect('/')
|
||||
}))
|
||||
|
||||
// Disabling 2fa
|
||||
router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
if (req.body.csrf !== req.session.csrf) {
|
||||
@ -260,6 +274,7 @@ router.post('/user/two-factor/disable', wrap(async (req, res, next) => {
|
||||
res.redirect('/')
|
||||
}))
|
||||
|
||||
// Verify 2FA for login
|
||||
router.post('/login/verify', wrap(async (req, res, next) => {
|
||||
if (req.session.user) return next()
|
||||
if (req.session.totp_check === null) return res.redirect('/login')
|
||||
@ -302,6 +317,7 @@ router.post('/login/verify', wrap(async (req, res, next) => {
|
||||
res.redirect(uri)
|
||||
}))
|
||||
|
||||
// Log the user in
|
||||
router.post('/login', wrap(async (req, res, next) => {
|
||||
if (req.session.user) return next()
|
||||
if (!req.body.username || !req.body.password || req.body.username === '') {
|
||||
@ -323,6 +339,7 @@ router.post('/login', wrap(async (req, res, next) => {
|
||||
if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
|
||||
if (user.locked === 1) return formError(req, res, 'This account has been locked.')
|
||||
|
||||
// Redirect to the verification dialog if 2FA is enabled
|
||||
let totpRequired = await API.User.Login.totpTokenRequired(user)
|
||||
if (totpRequired) {
|
||||
req.session.totp_check = user.id
|
||||
@ -347,6 +364,7 @@ router.post('/login', wrap(async (req, res, next) => {
|
||||
res.redirect(uri)
|
||||
}))
|
||||
|
||||
// Protected & Limited resource: Account registration
|
||||
router.post('/register', accountLimiter, wrap(async (req, res, next) => {
|
||||
if (req.session.user) return next()
|
||||
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
|
||||
@ -423,10 +441,17 @@ router.post('/register', accountLimiter, wrap(async (req, res, next) => {
|
||||
return formError(req, res, newUser.error)
|
||||
}
|
||||
|
||||
req.flash('message', {error: false, text: 'Account created successfully! Please check your email for an activation link.'})
|
||||
// Do not include activation link message when the user is already activated
|
||||
let registerMessage = 'Account created successfully!'
|
||||
if (newUser.user && newUser.user.activated !== 1) {
|
||||
registerMessage += ' Please check your email for an activation link.'
|
||||
}
|
||||
|
||||
req.flash('message', {error: false, text: registerMessage})
|
||||
res.redirect('/login')
|
||||
}))
|
||||
|
||||
// Change display name
|
||||
router.post('/user/manage', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
|
||||
@ -460,10 +485,11 @@ router.post('/user/manage', wrap(async (req, res, next) => {
|
||||
|
||||
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.'})
|
||||
req.flash('message', {error: false, text: 'Settings changed successfully.'})
|
||||
res.redirect('/user/manage')
|
||||
}))
|
||||
|
||||
// Change user password
|
||||
router.post('/user/manage/password', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
|
||||
@ -514,6 +540,7 @@ router.post('/user/manage/password', wrap(async (req, res, next) => {
|
||||
return res.redirect('/user/manage')
|
||||
}))
|
||||
|
||||
// Change email address
|
||||
router.post('/user/manage/email', wrap(async (req, res, next) => {
|
||||
if (!req.session.user) return next()
|
||||
|
||||
@ -578,6 +605,7 @@ router.post('/user/manage/email', wrap(async (req, res, next) => {
|
||||
=============
|
||||
*/
|
||||
|
||||
// Serve a document form the documents directory, cache it.
|
||||
const docsDir = path.join(__dirname, '../../documents')
|
||||
router.get('/docs/:name', (req, res, next) => {
|
||||
let doc = path.join(docsDir, req.params.name + '.html')
|
||||
@ -591,9 +619,11 @@ router.get('/docs/:name', (req, res, next) => {
|
||||
return next(e)
|
||||
}
|
||||
|
||||
res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) // 1 week
|
||||
res.render('document', {doc: doc})
|
||||
})
|
||||
|
||||
// Serve news
|
||||
router.get('/news/:id?-*', wrap(async (req, res) => {
|
||||
let id = parseInt(req.params.id)
|
||||
if (isNaN(id)) {
|
||||
@ -605,6 +635,7 @@ router.get('/news/:id?-*', wrap(async (req, res) => {
|
||||
return res.status(404).render('article', {article: null})
|
||||
}
|
||||
|
||||
res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day
|
||||
res.render('article', {article: article})
|
||||
}))
|
||||
|
||||
@ -619,6 +650,7 @@ router.get('/news/', wrap(async (req, res) => {
|
||||
res.render('news', {news: news})
|
||||
}))
|
||||
|
||||
// Render partials
|
||||
router.get('/partials/:view', wrap(async (req, res, next) => {
|
||||
if (!req.params.view) return next()
|
||||
|
||||
@ -639,6 +671,7 @@ router.get('/logout', wrap(async (req, res) => {
|
||||
res.redirect('/')
|
||||
}))
|
||||
|
||||
// User activation endpoint (emailed link)
|
||||
router.get('/activate/:token', wrap(async (req, res) => {
|
||||
if (req.session.user) return res.redirect('/login')
|
||||
let token = req.params.token
|
||||
|
@ -11,7 +11,7 @@ router.use(oauth.express())
|
||||
|
||||
let oauthLimiter = new RateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 100,
|
||||
max: 10,
|
||||
delayMs: 0
|
||||
})
|
||||
|
||||
@ -23,10 +23,12 @@ function ensureLoggedIn (req, res, next) {
|
||||
res.redirect('/login')
|
||||
}
|
||||
|
||||
// Generic OAuth2 endpoints
|
||||
router.use('/authorize', ensureLoggedIn, oauth.controller.authorization)
|
||||
router.post('/token', oauth.controller.token)
|
||||
router.post('/introspect', oauth.controller.introspection)
|
||||
|
||||
// Protected user information resource
|
||||
router.get('/user', oauth.bearer, wrap(async (req, res) => {
|
||||
let accessToken = req.oauth2.accessToken
|
||||
let user = await uapi.User.get(accessToken.user_id)
|
||||
|
@ -31,17 +31,21 @@ app.use(session({
|
||||
}))
|
||||
|
||||
app.use((req, res, next) => {
|
||||
// Inject a cleaner version of the user's IP Address into the request
|
||||
let ipAddr = req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
||||
|
||||
if (ipAddr.indexOf('::ffff:') !== -1) {
|
||||
ipAddr = ipAddr.replace('::ffff:', '')
|
||||
}
|
||||
|
||||
req.realIP = ipAddr
|
||||
|
||||
// Make sure CSRF token is present in the session
|
||||
if (!req.session.csrf) {
|
||||
req.session.csrf = crypto.randomBytes(12).toString('hex')
|
||||
}
|
||||
|
||||
req.realIP = ipAddr
|
||||
// Add user and csrf token into rendering information
|
||||
res.locals = Object.assign(res.locals, {
|
||||
user: req.session.user || null,
|
||||
csrf: req.session.csrf
|
||||
@ -56,8 +60,10 @@ module.exports = (args) => {
|
||||
app.set('views', path.join(__dirname, '../views'))
|
||||
|
||||
if (args.dev) console.log('Worker is in development mode')
|
||||
let staticAge = args.dev ? 1000 : 7 * 24 * 60 * 60 * 1000
|
||||
let staticAge = args.dev ? 1000 : 7 * 24 * 60 * 60 * 1000 // 1 week of cache in production
|
||||
|
||||
// Static content directories, cache these requests.
|
||||
// It is also a good idea to use nginx to serve these directories in order to save on computing power
|
||||
app.use('/style', express.static(path.join(__dirname, '../build/style'), { maxAge: staticAge }))
|
||||
app.use('/script', express.static(path.join(__dirname, '../build/script'), { maxAge: staticAge }))
|
||||
app.use('/static', express.static(path.join(__dirname, '../static'), { maxAge: staticAge }))
|
||||
@ -67,6 +73,8 @@ module.exports = (args) => {
|
||||
|
||||
app.listen(args.port, () => {
|
||||
console.log('Listening on 0.0.0.0:' + args.port)
|
||||
|
||||
// Initialize the email transporter (if configured)
|
||||
email.init()
|
||||
})
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ $(document).ready(function () {
|
||||
function removeAuthorization (clientId) {
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: '/api/oauth2/authorized-clients/delete',
|
||||
url: '/api/oauth2/authorized-clients/revoke',
|
||||
data: { client_id: clientId },
|
||||
success: function (data) {
|
||||
loadAuthorizations()
|
||||
|
@ -458,6 +458,17 @@ span.load
|
||||
.option
|
||||
display: block
|
||||
|
||||
.specify
|
||||
display: inline-block
|
||||
width: 22px
|
||||
text-align: center
|
||||
margin-left: 5px
|
||||
background-color: #2196F3
|
||||
color: #fff
|
||||
border-radius: 100px
|
||||
cursor: help
|
||||
font-size: 19px
|
||||
|
||||
.dialog-drop
|
||||
display: block
|
||||
position: fixed
|
||||
|
@ -31,6 +31,7 @@ block body
|
||||
input(type="submit", value="Save Settings")
|
||||
.right
|
||||
h3 Social Media Accounts
|
||||
.specify(title="You can add social media accounts to your account for ease of login. Once added, logging in from linked sources logs you into this account automatically.") ?
|
||||
include ../includes/external.pug
|
||||
if twitter_auth == false
|
||||
a.option.accdisconnect(href="/api/external/twitter/remove")
|
||||
@ -61,7 +62,8 @@ block body
|
||||
i.fa.fa-fw.fa-envelope
|
||||
|Change Email Address
|
||||
.clients
|
||||
h2 OAuth2 Authorized Applications
|
||||
h2 Authorized Applications
|
||||
.specify(title="Applications which have access to basic user information. You may restrict access at any time by pressing the red icon on the top right of the application card.") ?
|
||||
.cl#clientlist
|
||||
span.load
|
||||
i.fa.fa-spin.fa-spinner.fa-2x
|
||||
|
Reference in New Issue
Block a user