Skip to content

Commit 4deae1e

Browse files
authored
Merge branch 'master' into feature/check-quota
2 parents 3dcddce + 6b2be22 commit 4deae1e

File tree

23 files changed

+3288
-2724
lines changed

23 files changed

+3288
-2724
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ your copy.
1212
$ git clone [email protected]:your_username/node-solid-server.git
1313
$ cd node-solid-server
1414
$ git remote add upstream git://github.com/solid/node-solid-server.git
15+
$ npm install
1516
```
1617

1718

bin/lib/cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const program = require('commander')
22
const loadInit = require('./init')
33
const loadStart = require('./start')
4+
const loadInvalidUsernames = require('./invalidUsernames')
45
const { spawnSync } = require('child_process')
56
const path = require('path')
67

@@ -9,6 +10,7 @@ module.exports = function startCli (server) {
910

1011
loadInit(program)
1112
loadStart(program, server)
13+
loadInvalidUsernames(program)
1214

1315
program.parse(process.argv)
1416
if (program.args.length === 0) program.help()

bin/lib/common.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const fs = require('fs')
2+
const extend = require('extend')
3+
const { cyan, bold } = require('colorette')
4+
5+
module.exports.loadConfig = loadConfig
6+
7+
function loadConfig (program, options) {
8+
let argv = extend({}, options, { version: program.version() })
9+
let configFile = argv['configFile'] || './config.json'
10+
11+
try {
12+
const file = fs.readFileSync(configFile)
13+
14+
// Use flags with priority over config file
15+
const config = JSON.parse(file)
16+
Object.keys(config).forEach((option) => {
17+
argv[option] = argv[option] || config[option]
18+
})
19+
} catch (err) {
20+
// No file exists, not a problem
21+
console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`')
22+
}
23+
24+
return argv
25+
}

bin/lib/invalidUsernames.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
const fs = require('fs-extra')
2+
const Handlebars = require('handlebars')
3+
const path = require('path')
4+
const { URL } = require('url')
5+
6+
const { loadConfig } = require('./common')
7+
const { isValidUsername } = require('../../lib/common/user-utils')
8+
const blacklistService = require('../../lib/services/blacklist-service')
9+
const { initConfigDir, initTemplateDirs } = require('../../lib/server-config')
10+
const { fromServerConfig } = require('../../lib/models/oidc-manager')
11+
12+
const AccountManager = require('../../lib/models/account-manager')
13+
const EmailService = require('../../lib/services/email-service')
14+
const LDP = require('../../lib/ldp')
15+
const SolidHost = require('../../lib/models/solid-host')
16+
17+
module.exports = function (program) {
18+
program
19+
.command('invalidusernames')
20+
.option('--notify', 'Will notify users with usernames that are invalid')
21+
.option('--delete', 'Will delete users with usernames that are invalid')
22+
.description('Manage usernames that are invalid')
23+
.action(async (options) => {
24+
const config = loadConfig(program, options)
25+
if (!config.multiuser) {
26+
return console.error('You are running a single user server, no need to check for invalid usernames')
27+
}
28+
29+
const invalidUsernames = getInvalidUsernames(config)
30+
const host = SolidHost.from({ port: config.port, serverUri: config.serverUri })
31+
const accountManager = getAccountManager(config, host)
32+
33+
if (options.notify) {
34+
return notifyUsers(invalidUsernames, accountManager, config)
35+
}
36+
37+
if (options.delete) {
38+
return deleteUsers(invalidUsernames, accountManager, config, host)
39+
}
40+
41+
listUsernames(invalidUsernames)
42+
})
43+
}
44+
45+
function backupIndexFile (username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail) {
46+
const userDirectory = accountManager.accountDirFor(username)
47+
const currentIndex = path.join(userDirectory, 'index.html')
48+
const currentIndexExists = fs.existsSync(currentIndex)
49+
const backupIndex = path.join(userDirectory, 'index.backup.html')
50+
const backupIndexExists = fs.existsSync(backupIndex)
51+
if (currentIndexExists && !backupIndexExists) {
52+
fs.renameSync(currentIndex, backupIndex)
53+
createNewIndexAcl(userDirectory)
54+
createNewIndex(username, invalidUsernameTemplate, dateOfRemoval, supportEmail, currentIndex)
55+
console.info(`index.html updated for user ${username}`)
56+
}
57+
}
58+
59+
function createNewIndex (username, invalidUsernameTemplate, dateOfRemoval, supportEmail, currentIndex) {
60+
const newIndexSource = invalidUsernameTemplate({
61+
username,
62+
dateOfRemoval,
63+
supportEmail
64+
})
65+
fs.writeFileSync(currentIndex, newIndexSource, 'utf-8')
66+
}
67+
68+
function createNewIndexAcl (userDirectory) {
69+
const currentIndexAcl = path.join(userDirectory, 'index.html.acl')
70+
const backupIndexAcl = path.join(userDirectory, 'index.backup.html.acl')
71+
const currentIndexSource = fs.readFileSync(currentIndexAcl, 'utf-8')
72+
const backupIndexSource = currentIndexSource.replace(/index.html/g, 'index.backup.html')
73+
fs.writeFileSync(backupIndexAcl, backupIndexSource, 'utf-8')
74+
}
75+
76+
async function deleteUsers (usernames, accountManager, config, host) {
77+
const oidcManager = fromServerConfig({
78+
...config,
79+
host
80+
})
81+
const deletingUsers = usernames
82+
.map(async username => {
83+
try {
84+
const user = accountManager.userAccountFrom({ username })
85+
await oidcManager.users.deleteUser(user)
86+
} catch (error) {
87+
if (error.message !== 'No email given') {
88+
// 'No email given' is an expected error that we want to ignore
89+
throw error
90+
}
91+
}
92+
const userDirectory = accountManager.accountDirFor(username)
93+
await fs.remove(userDirectory)
94+
})
95+
await Promise.all(deletingUsers)
96+
console.info(`Deleted ${deletingUsers.length} users succeeded`)
97+
}
98+
99+
function getAccountManager (config, host) {
100+
const ldp = new LDP(config)
101+
return AccountManager.from({
102+
host,
103+
store: ldp,
104+
multiuser: config.multiuser
105+
})
106+
}
107+
108+
function getInvalidUsernames (config) {
109+
const files = fs.readdirSync(config.root)
110+
const hostname = new URL(config.serverUri).hostname
111+
const isUserDirectory = new RegExp(`.${hostname}$`)
112+
return files
113+
.filter(file => isUserDirectory.test(file))
114+
.map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1))
115+
.filter(username => !isValidUsername(username) || !blacklistService.validate(username))
116+
}
117+
118+
function listUsernames (usernames) {
119+
if (usernames.length === 0) {
120+
return console.info('No invalid usernames was found')
121+
}
122+
console.info(`${usernames.length} invalid usernames were found:${usernames.map(username => `\n- ${username}`)}`)
123+
}
124+
125+
async function notifyUsers (usernames, accountManager, config) {
126+
const twoWeeksFromNow = Date.now() + 14 * 24 * 60 * 60 * 1000
127+
const dateOfRemoval = (new Date(twoWeeksFromNow)).toLocaleDateString()
128+
const { supportEmail } = config
129+
130+
updateIndexFiles(usernames, accountManager, dateOfRemoval, supportEmail)
131+
await sendEmails(config, usernames, accountManager, dateOfRemoval, supportEmail)
132+
}
133+
134+
async function sendEmails (config, usernames, accountManager, dateOfRemoval, supportEmail) {
135+
if (config.email && config.email.host) {
136+
const configPath = initConfigDir(config)
137+
const templates = initTemplateDirs(configPath)
138+
const users = await Promise.all(await usernames.map(async username => {
139+
const emailAddress = await accountManager.loadAccountRecoveryEmail({ username })
140+
const accountUri = accountManager.accountUriFor(username)
141+
return { username, emailAddress, accountUri }
142+
}))
143+
const emailService = new EmailService(templates.email, config.email)
144+
const sendingEmails = users
145+
.filter(user => !!user.emailAddress)
146+
.map(user => emailService.sendWithTemplate('invalid-username', {
147+
to: user.emailAddress,
148+
accountUri: user.accountUri,
149+
dateOfRemoval,
150+
supportEmail
151+
}))
152+
const emailsSent = await Promise.all(sendingEmails)
153+
console.info(`${emailsSent.length} emails sent to users with invalid usernames`)
154+
return
155+
}
156+
console.info('You have not configured an email service.')
157+
console.info('Please set it up to send users email about their accounts')
158+
}
159+
160+
function updateIndexFiles (usernames, accountManager, dateOfRemoval, supportEmail) {
161+
const invalidUsernameFilePath = path.join(process.cwd(), 'default-views', 'account', 'invalid-username.hbs')
162+
const source = fs.readFileSync(invalidUsernameFilePath, 'utf-8')
163+
const invalidUsernameTemplate = Handlebars.compile(source)
164+
usernames.forEach(username => backupIndexFile(username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail))
165+
}

bin/lib/options.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const fs = require('fs')
22
const path = require('path')
33
const validUrl = require('valid-url')
44
const { URL } = require('url')
5+
const { isEmail } = require('validator')
56

67
module.exports = [
78
// {
@@ -349,6 +350,18 @@ module.exports = [
349350
prompt: true,
350351
validate: validUri,
351352
when: answers => answers.enforceToc
353+
},
354+
{
355+
name: 'support-email',
356+
help: 'The support email you provide for your users (not required)',
357+
prompt: true,
358+
validate: (value) => {
359+
if (value && !isEmail(value)) {
360+
return 'Must be a valid email'
361+
}
362+
return true
363+
},
364+
when: answers => answers.multiuser
352365
}
353366
]
354367

bin/lib/start.js

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
const options = require('./options')
44
const fs = require('fs')
5-
const extend = require('extend')
6-
const { cyan, red, bold } = require('colorette')
5+
const { loadConfig } = require('./common')
6+
const { red, bold } = require('colorette')
77

88
module.exports = function (program, server) {
99
const start = program
@@ -34,24 +34,9 @@ module.exports = function (program, server) {
3434

3535
start.option('-v, --verbose', 'Print the logs to console')
3636

37-
start.action((opts) => {
38-
let argv = extend({}, opts, { version: program.version() })
39-
let configFile = argv['configFile'] || './config.json'
40-
41-
fs.readFile(configFile, (err, file) => {
42-
// No file exists, not a problem
43-
if (err) {
44-
console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`')
45-
} else {
46-
// Use flags with priority over config file
47-
const config = JSON.parse(file)
48-
Object.keys(config).forEach((option) => {
49-
argv[option] = argv[option] || config[option]
50-
})
51-
}
52-
53-
bin(argv, server)
54-
})
37+
start.action(async (options) => {
38+
const config = loadConfig(program, options)
39+
bin(config, server)
5540
})
5641
}
5742

common/css/solid.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@
4848
.progress .level-4{
4949
width: 100%;
5050
}
51+
52+
.login-up-form .form-group {
53+
margin-bottom: 5px;
54+
}
55+
56+
.xs-header {
57+
margin-top: 0px;
58+
}

common/js/auth-buttons.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* Provide functionality for authentication buttons */
2+
3+
(({ auth }) => {
4+
// Wire up DOM elements
5+
const [loginButton, logoutButton, registerButton, accountSettings] =
6+
['login', 'logout', 'register', 'accountSettings'].map(id =>
7+
document.getElementById(id) || document.createElement('a'))
8+
loginButton.addEventListener('click', login)
9+
logoutButton.addEventListener('click', logout)
10+
registerButton.addEventListener('click', register)
11+
12+
// Track authentication status and update UI
13+
auth.trackSession(session => {
14+
const loggedIn = !!session
15+
const isOwner = loggedIn && new URL(session.webId).origin === location.origin
16+
loginButton.classList.toggle('hidden', loggedIn)
17+
logoutButton.classList.toggle('hidden', !loggedIn)
18+
accountSettings.classList.toggle('hidden', !isOwner)
19+
})
20+
21+
// Log the user in on the client and the server
22+
async function login () {
23+
const session = await auth.popupLogin()
24+
if (session) {
25+
// Make authenticated request to the server to establish a session cookie
26+
const {status} = await auth.fetch(location, { method: 'HEAD' })
27+
if (status === 401) {
28+
alert(`Invalid login.\n\nDid you set ${session.idp} as your OIDC provider in your profile ${session.webId}?`)
29+
await auth.logout()
30+
}
31+
// Now that we have a cookie, reload to display the authenticated page
32+
location.reload()
33+
}
34+
}
35+
36+
// Log the user out from the client and the server
37+
async function logout () {
38+
await auth.logout()
39+
location.reload()
40+
}
41+
42+
// Redirect to the registration page
43+
function register () {
44+
const registration = new URL('/register', location)
45+
registration.searchParams.set('returnToUrl', location)
46+
location.href = registration
47+
}
48+
})(solid)

config.json-default

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
"logo": ""
1717
},
1818
"enforceToc": true,
19-
"tocUri": "https://your-toc"
19+
"tocUri": "https://your-toc",
20+
"supportEmail": "Your support email address"
2021
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module.exports.render = render
2+
3+
function render (data) {
4+
return {
5+
subject: `Invalid username for account ${data.accountUri}`,
6+
7+
/**
8+
* Text version
9+
*/
10+
text: `Hi,
11+
12+
We're sorry to inform you that the username for account ${data.accountUri} is not allowed after changes to username policy.
13+
14+
This account has been set to be deleted at ${data.dateOfRemoval}.
15+
16+
${data.supportEmail ? `Please contact ${data.supportEmail} if you want to move your account.` : ''}`,
17+
18+
/**
19+
* HTML version
20+
*/
21+
html: `<p>Hi,</p>
22+
23+
<p>We're sorry to inform you that the username for account ${data.accountUri} is not allowed after changes to username policy.</p>
24+
25+
<p>This account has been set to be deleted at ${data.dateOfRemoval}.</p>
26+
27+
${data.supportEmail ? `<p>Please contact ${data.supportEmail} if you want to move your account.</p>` : ''}
28+
`
29+
}
30+
}

0 commit comments

Comments
 (0)