change license, add user settings, social account unlink

This commit is contained in:
Evert Prants 2017-08-24 21:36:40 +03:00
parent 772d6aab1e
commit 497ac86980
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
19 changed files with 403 additions and 68 deletions

29
LICENSE
View File

@ -1,16 +1,21 @@
Icy Network Primary Web Application - Authentication and News MIT License
Copyright (C) 2017 Icy Network - Evert Prants <evert@lunasqu.ee>
This program is free software: you can redistribute it and/or modify Copyright (c) 2017 Evert Prants
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, Permission is hereby granted, free of charge, to any person obtaining a copy
but WITHOUT ANY WARRANTY; without even the implied warranty of of this software and associated documentation files (the "Software"), to deal
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the in the Software without restriction, including without limitation the rights
GNU General Public License for more details. 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 The above copyright notice and this permission notice shall be included in all
along with this program. If not, see <http://www.gnu.org/licenses/>. 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.

View File

@ -5,17 +5,17 @@
<p>Icy Network may collect and save some information about our users.</p> <p>Icy Network may collect and save some information about our users.</p>
<h3>Basic Account Information</h3> <h3>Basic Account Information</h3>
<p>Icy Network uses your username to identify you in our system. Please only enter Usernames which you are comfortable with other people seeing.</p> <p>Icy Network uses your username to identify you in our system. Please only enter Usernames which you are comfortable with other people seeing.</p>
<p>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.</p> <p>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.</p>
<h3>Additional Information</h3> <h3>Additional Information</h3>
<p>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.</p> <p>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.</p>
<h2>Cookies</h2> <h2>Cookies</h2>
<p>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.</p> <p>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.</p>
<h2>External Logins</h2> <h2>External Logins</h2>
<p>By logging in from external websites, you agree to these Policies.</p> <p>By logging in from external websites, you agree to these Policies.</p>
<h3>Twitter</h3> <h3>Twitter</h3>
<p>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.</p> <p>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.</p>
<h3>Facebook</h3> <h3>Facebook</h3>
<p>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.</p> <p>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.</p>
<h3>Discord</h3> <h3>Discord</h3>
<p>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.</p> <p>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.</p>
</div> </div>

View File

@ -14,20 +14,4 @@
<h4>Credits</h4> <h4>Credits</h4>
<p>Google Play and the Google Play logo are trademarks of Google Inc.</p> <p>Google Play and the Google Play logo are trademarks of Google Inc.</p>
<p>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.</p> <p>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.</p>
<h2>Icy Network Software License</h2>
<a href="https://github.com/IcyNet/IcyNet.eu" target="_blank">Icy Network Primary Web Application - Authentication and News</a><br>
Copyright (C) 2017 Icy Network - Evert Prants &lt;evert@lunasqu.ee&gt;<br>
<br>
This program is free software: you can redistribute it and/or modify<br>
it under the terms of the GNU General Public License as published by<br>
the Free Software Foundation, either version 3 of the License, or<br>
(at your option) any later version.<br>
<br>
This program is distributed in the hope that it will be useful,<br>
but WITHOUT ANY WARRANTY; without even the implied warranty of<br>
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the<br>
GNU General Public License for more details.<br>
<br>
You should have received a copy of the GNU General Public License<br>
along with this program. If not, see &lt;<a href="http://www.gnu.org/licenses/" target="_blank">http://www.gnu.org/licenses/</a>&gt;.<br>
</div> </div>

View File

@ -24,7 +24,7 @@
"authentication" "authentication"
], ],
"author": "Icy Network", "author": "Icy Network",
"license": "GPL-3.0", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/IcyNet/IcyNet.eu/issues" "url": "https://github.com/IcyNet/IcyNet.eu/issues"
}, },

View File

@ -35,9 +35,12 @@ module.exports = function () {
this.logProcess = (pid, msg) => { this.logProcess = (pid, msg) => {
if (msg.indexOf('warn') === 0) { if (msg.indexOf('warn') === 0) {
msg = msg.substring(5) msg = msg.substring(5)
console.warn('[%s] %s', pid, msg)
} else if (msg.indexOf('error') === 0) { } else if (msg.indexOf('error') === 0) {
msg = msg.substring(6) msg = msg.substring(6)
console.error('[%s] %s', pid, msg)
} else {
console.log('[%s] %s', pid, msg)
} }
console.log('[%s] %s', pid, msg)
} }
} }

View File

@ -35,6 +35,21 @@ const API = {
await await models.External.query().insert(data) await await models.External.query().insert(data)
return true 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: { Facebook: {
@ -97,12 +112,13 @@ const API = {
avatar_file: profilepic, avatar_file: profilepic,
activated: 1, activated: 1,
ip_address: data.ip_address, ip_address: data.ip_address,
created_at: new Date() created_at: new Date(),
updated_at: new Date()
} }
// Check if the username is already taken // Check if the username is already taken
if (await UAPI.User.get(udataLimited.username) != null) { 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, // Check if the email Facebook gave us is already registered, if so,
@ -209,12 +225,13 @@ const API = {
avatar_file: profilepic, avatar_file: profilepic,
activated: 1, activated: 1,
ip_address: ipAddress, ip_address: ipAddress,
updated_at: new Date(),
created_at: new Date() created_at: new Date()
} }
// Check if the username is already taken // Check if the username is already taken
if (await UAPI.User.get(udataLimited.username) != null) { 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, // Check if the email Twitter gave us is already registered, if so,
@ -320,6 +337,7 @@ const API = {
avatar_file: profilepic, avatar_file: profilepic,
activated: 1, activated: 1,
ip_address: ipAddress, ip_address: ipAddress,
updated_at: new Date(),
created_at: new Date() created_at: new Date()
} }

View File

@ -86,9 +86,42 @@ const API = {
return null 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: { Login: {
password: async function (user, password) { password: async function (user, password) {
user = await API.User.ensureObject(user) user = await API.User.ensureObject(user, ['password'])
if (!user.password) return false if (!user.password) return false
return bcryptTask({task: 'compare', password: password, hash: user.password}) return bcryptTask({task: 'compare', password: password, hash: user.password})
}, },
@ -193,6 +226,7 @@ const API = {
let email = config.email && config.email.enabled let email = config.email && config.email.enabled
let data = Object.assign(regdata, { let data = Object.assign(regdata, {
created_at: new Date(), created_at: new Date(),
updated_at: new Date(),
activated: email ? 0 : 1 activated: email ? 0 : 1
}) })

View File

@ -62,12 +62,7 @@ function createSession (req, user) {
// Either give JSON or make a redirect // Either give JSON or make a redirect
function JsonData (req, res, error, redirect = '/') { function JsonData (req, res, error, redirect = '/') {
if (req.headers['content-type'] === 'application/json') { res.jsonp({error: error, redirect: redirect})
return res.jsonp({error: error, redirect: redirect})
}
req.flash('message', {error: true, text: error})
res.redirect(redirect)
} }
/** FACEBOOK LOGIN /** FACEBOOK LOGIN
@ -94,6 +89,17 @@ router.post('/external/facebook/callback', wrap(async (req, res) => {
JsonData(req, res, null, uri) 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 /** TWITTER LOGIN
* OAuth1.0a flows * OAuth1.0a flows
* Tokens in configs * Tokens in configs
@ -147,6 +153,18 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
res.redirect(uri) 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 /** DISCORD LOGIN
* OAuth2 flows * OAuth2 flows
* Tokens in configs * Tokens in configs
@ -205,6 +223,18 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
res.redirect(uri) 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 * NEWS
* ======== * ========
@ -247,4 +277,9 @@ router.get('/news', wrap(async (req, res) => {
res.jsonp(articles) res.jsonp(articles)
})) }))
// 404
router.use((req, res) => {
res.status(404).jsonp({error: 'Not found'})
})
module.exports = router module.exports = router

View File

@ -8,6 +8,7 @@ import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http' import http from '../../scripts/http'
import API from '../api' import API from '../api'
import News from '../api/news' import News from '../api/news'
import emailer from '../api/emailer'
import apiRouter from './api' import apiRouter from './api'
import oauthRouter from './oauth2' 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.' 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) => { router.use(wrap(async (req, res, next) => {
let messages = req.flash('message') let messages = req.flash('message')
if (!messages || !messages.length) { if (!messages || !messages.length) {
@ -29,6 +41,18 @@ router.use(wrap(async (req, res, next) => {
messages = messages[0] 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 res.locals.message = messages
next() next()
})) }))
@ -117,6 +141,49 @@ router.get('/login/verify', (req, res) => {
res.render('totp-check') 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 POST HANDLING
@ -125,10 +192,9 @@ router.get('/login/verify', (req, res) => {
function formError (req, res, error, redirect) { function formError (req, res, error, redirect) {
// Security measures: never store any passwords in any session // Security measures: never store any passwords in any session
if (req.body.password) { for (let key in req.body) {
delete req.body.password if (key.indexOf('password') !== -1) {
if (req.body.password_repeat) { delete req.body[key]
delete req.body.password_repeat
} }
} }
@ -239,14 +305,7 @@ router.post('/login', wrap(async (req, res) => {
// TODO: Ban checks // TODO: Ban checks
// Set session // Set session
req.session.user = { setSession(req, 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
}
let uri = '/' let uri = '/'
if (req.session.redirectUri) { if (req.session.redirectUri) {
@ -340,6 +399,91 @@ router.post('/register', accountLimiter, wrap(async (req, res) => {
res.redirect('/login') 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 DOCUMENTS
@ -347,10 +491,10 @@ router.post('/register', accountLimiter, wrap(async (req, res) => {
*/ */
const docsDir = path.join(__dirname, '../../documents') 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') let doc = path.join(docsDir, req.params.name + '.html')
if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) { if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) {
return res.status(404).end() return next()
} }
doc = fs.readFileSync(doc, {encoding: 'utf8'}) doc = fs.readFileSync(doc, {encoding: 'utf8'})
@ -410,6 +554,10 @@ router.get('/activate/:token', wrap(async (req, res) => {
router.use('/api', apiRouter) router.use('/api', apiRouter)
router.use((req, res) => {
res.status(404).render('404')
})
router.use((err, req, res, next) => { router.use((err, req, res, next) => {
console.error(err) console.error(err)
next() next()

View File

@ -49,12 +49,12 @@ router.get('/user', oauth.bearer, wrap(async (req, res) => {
} }
// Include Email // Include Email
if (accessToken.scope.indexOf('email') != -1) { if (accessToken.scope.indexOf('email') !== -1) {
udata.email = user.email udata.email = user.email
} }
// Include privilege number // Include privilege number
if (accessToken.scope.indexOf('privilege') != -1) { if (accessToken.scope.indexOf('privilege') !== -1) {
udata.privilege = user.nw_privilege udata.privilege = user.nw_privilege
} }

View File

@ -109,6 +109,7 @@ $(document).ready(function () {
dataType: 'json', dataType: 'json',
data: response, data: response,
success: function (data) { success: function (data) {
console.log(data)
if (data.error) { if (data.error) {
$('.message').addClass('error') $('.message').addClass('error')
$('.message span').text(data.error) $('.message span').text(data.error)

View File

@ -231,6 +231,14 @@ input:not([type="submit"])
.boxcont .boxcont
.box .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 .left, .right
display: inline-block display: inline-block
width: 50% width: 50%
@ -241,16 +249,13 @@ input:not([type="submit"])
width: 46% width: 46%
&#login &#login
max-width: 700px max-width: 700px
&#settings
max-width: 700px
min-height: 380px
&#totpcheck &#totpcheck
max-width: 400px max-width: 400px
padding: 20px &#error
margin: auto width: 200px
margin-top: 5%
background-color: #fff
box-shadow: 5px 5px 15px #868686
border: 1px solid #ddd
h1, h2, h3
margin-top: 0
.pgn .pgn
display: inline-block display: inline-block
@ -423,6 +428,9 @@ input.invalid
border-color: #ffffff border-color: #ffffff
margin: 20px 0 margin: 20px 0
.option
display: block
@media all and (max-width: 800px) @media all and (max-width: 800px)
.navigator .navigator
padding: 0 10px padding: 0 10px

View File

@ -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 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 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. 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.

View File

@ -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.

View File

@ -0,0 +1 @@
|Your Password on Icy Network has been changed!

12
views/404.pug Normal file
View File

@ -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

View File

@ -1,5 +1,8 @@
extends layout.pug extends layout.pug
block title
|Icy Network - Legal Notice
block body block body
.document .document
.content !{doc} .content !{doc}

28
views/password_new.pug Normal file
View File

@ -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")

48
views/settings.pug Normal file
View File

@ -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