New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Invalidusernames command #919
Conversation
Why is there both a invalid-username.hbs and .js? |
|
const { cyan, bold } = require('colorette') | ||
const util = require('util') | ||
|
||
module.exports = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could be wrong, but isn't this the default behaviour in CommonJS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you're right, it's unnecessary. I'll remove it.
}) | ||
} catch (err) { | ||
// No file exists, not a problem | ||
console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely a new issue, the required changes would be server-wide.
That said, it /should/ eventually be done, since one of the most frequently requested features about logging is timestamps, which our current logger does not support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging is great for long-running processes (and the server has logging), but this is just a short one-time job.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, actually, I should probably reuse debug
that's laid out via lib/debug.js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might consider a logger too, but yeah, that should be its own issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let configFile = argv['configFile'] || './config.json' | ||
|
||
try { | ||
const file = await util.promisify(fs.readFile)(configFile) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that the only occurence of fs
?
If so, would const { readFile } = require('fs')
work also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plus const { promisify } = require('util')
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After Node 8, I'm pretty sure all fs
methods have a Promise interface natively, so the promisify is not even needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://nodejs.org/api/fs.html#fs_fs_promises_api currently has stability: experimental.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently has stability: experimental.
Ohhh right, I was thinking about fs-extra
, never mind :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll write away promisify.
.option('--notify', 'Will notify users with usernames that are invalid') | ||
.option('--delete', 'Will delete users with usernames that are invalid') | ||
.description('Manage usernames that are invalid') | ||
.action(async (options) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My personal view: I move the implementation of the action into another file / function.
This way I can have different interfaces for it (CLI, Web UI etc).
Plus, it is easier to test.
Plus, it is a separation of concerns (CLI vs. using the lib).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do it 100% if I thought this code was reusable, which I'm leaning toward it isn't =/
As I mentioned earlier, I found it difficult to write tests for this. It relies on a lot of file-based functionality, which makes it unsuited for unit tests. I could write integration tests, but didn't see any for the other commands, so didn't know how to attack it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From an Angular point of view I'd argue with Dependency Injection.
That is, providing the fs
as an argument to the function.
But it feels meh somehow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not that familiar with dependency injection in Node. I've tried MockRequire earlier, but my tests always turned a nightmare to maintain.
} | ||
|
||
const invalidUsernames = await getInvalidUsernames(config) | ||
const host = SolidHost.from({ port: config.port, serverUri: config.serverUri }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I wonder, whether const host = SolidHost.from({ config })
would work.
Like in Unpacking fields from objects passed as function parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reckon you mean const host = SolidHost.from(config)
? (As const host = SolidHost.from({ config })
is a shortcut for const host = SolidHost.from({ config: config })
.)
Could work, but I wanted to stay specific reg this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, more specific is better in this case (since it's clearer which args are actually used)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
}) | ||
} | ||
|
||
async function createNewIndexFile (username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail, fileOptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I don't like functions with so many arguments. I consider them a code smell.
Like the function trying to do too much.
It helps to have smaller functions, which are then called in order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like long signatures either, but in this case it's not really reusable functions, but I wanted more readable code by splitting it up into "logical" chunks of code (in what I hope is good named functions).
But yeah, if we wanted to reuse this functionality, I should probably restructure it a bit to decrease the length of the signatures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if you are struggling with writing tests, this should be argument enough to break the function up, shouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I knew how to break up this code into reusable, testable functions, I would do it ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I can write the high-level steps I see. Implementation goes into the functions then …
async function createNewIndexFile (/* arguments */) {
if (isBackupNeeded(/* arguments */)) {
await Promise.all([
backupIndexHtml(/* arguments */)
backupAcl(/* arguments */)
])
await Promise.all([
generateNewIndexHtml(/* arguments */)
generateNewAcl(/* arguments */)
])
logCompletion(/* arguments */)
}
}
or maybe group the backup + creation into a single function (like an transaction).
You may want to timestamp the index.backup.html
also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plus, looking at the function name, createNewIndexFile
is misleading, since it does more than it says …
so its caller would need to call more functions.
.map(async username => { | ||
try { | ||
const user = accountManager.userAccountFrom({ username }) | ||
await oidcManager.users.deleteUser(user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC I've read in the docs, that other auth managers (than oidc) would be supported as well.
So I am wondering here, why it has to be oidc.
Maybe add an comment? Could be that it is a „stupid question” for contributors familiar with the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not a stupid question; We support WebID-TLS and WebID-OIDC now, and WebID-TLS don't store user-data like WebID-OIDC does. (If I'm wrong, pleasepleaseplease correct me =P )
I could try to abstract away the OIDC-part of this, to accommodate future auth managers better, but I think that's going to might take a long time (for me, at least - I'm not that into auth managers yet). So I was thinking this can be done when we implement support for other auth managers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you could leave a comment here, so the next person touching the code knows your thoughts (since I'd not look up the associated PR immediately, but rather git blame
…).
I don't know the conventions in this project, though.
const userDirectory = accountManager.accountDirFor(username) | ||
const currentIndex = path.join(userDirectory, 'index.html') | ||
const currentIndexExists = await fileExists(currentIndex) | ||
const backupIndex = path.join(userDirectory, 'index.backup.html') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still in documentRoot, i.e. accessible from the Internet (for everybody?)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, should probably copy the acl-file as well O_O
const isUserDirectory = new RegExp(`.${hostname}$`) | ||
return files | ||
.filter(file => isUserDirectory.test(file)) | ||
.map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exactly userDirectory.length - hostname.length - 1
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Userdirectory can be read as [username].[hostname]
, so it's to remove the dot between username and hostname.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it fixed that there must be a dot?
If so, can't you split
the hostname
(and take the second-to-last value)?
if (usernames.length === 0) { | ||
console.info('No invalid usernames was found') | ||
} | ||
console.info(`${usernames.length} invalid usernames were found:${usernames.map(username => `\n- ${username}`)}`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't that go into an else
branch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, forgot a return in the if-block (that's at least how I like to write that logic).
} | ||
|
||
async function notifyUsers (usernames, accountManager, config) { | ||
const twoWeeksFromNow = Date.now() + 14 * 24 * 60 * 60 * 1000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I prefer the former, but didn't want to include it just for this.
})) | ||
const emailService = new EmailService(templates.email, config.email) | ||
const sendingEmails = await users | ||
.filter(user => !!user.emailAddress) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe more readable as .filter(user => Boolean(user.emailAddress))
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree, !!
is more standard, and is used throughout our libs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!!
reminds me on the output of uglify
:-)
But being used throughout is a valid argument.
} | ||
|
||
async function updateIndexFiles (usernames, accountManager, dateOfRemoval, supportEmail) { | ||
const invalidUsernameFilePath = path.join(process.cwd(), 'default-views/account/invalid-username.hbs') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use path.join
when the second argument is not OS-agnostic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was planning to see if I could find another a way to reference default-views/account
, but forgot about it. I'll make another try, or at least fix this to be OS-agnostic.
*/ | ||
text: `Hi, | ||
|
||
We're sorry to inform you that the username for account ${data.accountUri} is not allowed after changes to username policy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe refer to username policy
somehow? I'd be wondering, what exactly is in there if I would receive such an e-mail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary, but open to be convinced otherwise.
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<title>Invalid username</title> | ||
<link rel="stylesheet" href="/common/css/bootstrap.min.css"> | ||
<script></script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh? An empty tag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will remove.
</head> | ||
<body> | ||
<div class="container"> | ||
<h4>Invalid username</h4> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not an h1
? Semantic Web and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of legacy; the other views use h4 (I reckon they did it like this initially because they thought h1 was to big; We (mostly) use bootstrap (3.3.4) for the presentation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@megoth hehe, that's exactly why, h1 was too big on bootstrap :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some comments while walking through the files. |
Great with a lot of comments, I just wanted to note that this code will most likely run only once, so it is not worth spending too much time on it. |
This is my attempt at fixing #824.
A lot of things happening here, but it boils down to a command with two options:
[solid] invalidusernames
: Will list all invalid usernames that exists on the server today[solid] invalidusernames --notify
: Will go through all invalid accounts with invalid usernames anddefault-views/account/invalid-username.hbs
)default-templates/emails/invalid-username.js
)[solid] invalidusernames --delete
: Will delete all accounts with invalid usernamesI've also added a new option in the
init
script that prompts for a support-email for POD-providers (this will only show if the multiuser-property is set to true). As part of this I made use of theisEmail
-method in https://www.npmjs.com/package/validator, which I understand from #916 might not've been the most well-thought through decision.I've tried writing some tests, but find it difficult to write tests for a lot of this =/
Any and all feedback are (as always) appreciated, and I hope I'm not too far off with this solution.