Skip to content

Commit 8f9f637

Browse files
authored
FEATURE: Add anonymous username generator plugin (#1)
* FEATURE: Add anonymous username generator plugin With this PR we add the core functionalities for this plugin: - Blocks usernames that contain parts of the user's real last name. - Provides a button to generate random usernames during sign-up (currently only in invites). * DEV: update * DEV: reduce random word list * DEV: make random word list optional * DEV: update name to invite-username-input * DEV: use username_validator modifier * DEV: rename modifier to username_validations * DEV: update based on core changes * DEV: update modifier naming + context.user * DEV: lint * DEV: revert name again! * dev: update spec to use model directly
1 parent 67d58cf commit 8f9f637

File tree

13 files changed

+279
-9
lines changed

13 files changed

+279
-9
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,13 @@
22

33
**Plugin Summary**
44

5-
For more information, please see: **url to meta topic**
5+
> [!WARNING]
6+
> Currently only works with `invite only` setups.
7+
8+
This plugin provides functionality to generate anonymous usernames for users during sign-up.
9+
10+
- [x] Generates random usernames based on a predefined list of words(`random words list` + random number).
11+
- [x] Option to restrict users to only use generated usernames (`only generated usernames` setting).
12+
13+
> [!IMPORTANT]
14+
> Ensure to have `full_name_requirement` setting as `"required_at_signup"`.

app/controllers/discourse_anon_usernames/examples_controller.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,5 @@
33
module ::DiscourseAnonUsernames
44
class ExamplesController < ::ApplicationController
55
requires_plugin PLUGIN_NAME
6-
7-
def index
8-
render json: { hello: "world" }
9-
end
106
end
117
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Component from "@glimmer/component";
2+
import { action } from "@ember/object";
3+
import { service } from "@ember/service";
4+
import DButton from "discourse/components/d-button";
5+
6+
const getFirstName = (fullName) => fullName.split(" ")[0];
7+
8+
export default class RandomizerButton extends Component {
9+
@service siteSettings;
10+
11+
get randomWordsList() {
12+
return this.siteSettings.random_words_list.split("|") || [];
13+
}
14+
15+
fetchRandomWord() {
16+
if (this.randomWordsList.length === 0) {
17+
return "";
18+
}
19+
return this.randomWordsList[
20+
Math.floor(Math.random() * this.randomWordsList.length)
21+
];
22+
}
23+
24+
@action
25+
async generate() {
26+
const randomizeFrom = getFirstName(this.args.randomizeFrom);
27+
const randomWord = this.fetchRandomWord();
28+
29+
this.args.onGenerate({
30+
target: {
31+
value: randomizeFrom + randomWord + Math.floor(Math.random() * 1000),
32+
},
33+
});
34+
}
35+
36+
get canGenerate() {
37+
return (
38+
this.args.randomizeFrom &&
39+
this.args.randomizeFrom.length !== 0 &&
40+
getFirstName(this.args.randomizeFrom).trim() !==
41+
this.args.randomizeFrom.trim()
42+
);
43+
}
44+
45+
<template>
46+
<DButton
47+
@icon="rotate"
48+
class="btn-primary randomizer-btn"
49+
@action={{this.generate}}
50+
disabled={{if this.canGenerate false true}}
51+
/>
52+
</template>
53+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { on } from "@ember/modifier";
2+
import InputTip from "discourse/components/input-tip";
3+
import valueEntered from "discourse/helpers/value-entered";
4+
import { apiInitializer } from "discourse/lib/api";
5+
import { i18n } from "discourse-i18n";
6+
import RandomizerButton from "../components/randomizer-button";
7+
8+
export default apiInitializer((api) => {
9+
const siteSettings = api.container.lookup("service:site-settings");
10+
11+
api.renderInOutlet(
12+
"invite-username-input",
13+
<template>
14+
<div class="input-with-randomizer-btn">
15+
<input
16+
{{on "focusin" @scrollInputIntoView}}
17+
{{on "input" @setAccountUsername}}
18+
type="text"
19+
value={{@accountUsername}}
20+
class={{valueEntered @accountUsername}}
21+
id="new-account-username"
22+
name="username"
23+
disabled={{siteSettings.only_generated_usernames}}
24+
maxlength={{@maxUsernameLength}}
25+
autocomplete="off"
26+
/>
27+
28+
<RandomizerButton
29+
@randomizeFrom={{@accountName}}
30+
@onGenerate={{@setAccountUsername}}
31+
/>
32+
33+
</div>
34+
{{#unless @accountUsername}}
35+
<label class="alt-placeholder" for="new-account-username">
36+
{{i18n "user.username.title"}}
37+
</label>
38+
{{/unless}}
39+
<InputTip @validation={{@usernameValidation}} id="username-validation" />
40+
</template>
41+
);
42+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@use "lib/viewport";
2+
3+
// change ordering of username input in invite sign up
4+
.invites-show {
5+
.invite-form form {
6+
.input-group {
7+
&.email-input {
8+
order: 1;
9+
}
10+
11+
&.name-input.name-required {
12+
order: 2;
13+
}
14+
15+
&.username-input {
16+
order: 3;
17+
}
18+
19+
&.password-input {
20+
order: 4;
21+
}
22+
}
23+
24+
.invitation-cta {
25+
order: 5;
26+
}
27+
}
28+
}
29+
30+
.input-with-randomizer-btn {
31+
display: flex;
32+
33+
.btn-primary {
34+
margin-left: 0.5rem;
35+
height: 2.5rem;
36+
}
37+
}

config/locales/client.en.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ en:
66
discourse_anon_usernames: "Discourse Anon Usernames"
77
js:
88
discourse_anon_usernames:
9-
placeholder: placeholder
9+
errors:
10+
full_name_required:
11+
title: "Full name required"
12+
message: "Please enter your full name (first and last) to generate an anonymous username."

config/locales/server.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
en:
2+
discourse_anon_usernames:
3+
errors:
4+
username_invalid: "The chosen username appears to contain your last name. Please choose a different username."

config/routes.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
DiscourseAnonUsernames::Engine.routes.draw do
4-
get "/examples" => "examples#index"
54
# define routes here
65
end
76

config/settings.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ discourse_anon_usernames:
22
discourse_anon_usernames_enabled:
33
default: false
44
client: true
5+
only_generated_usernames:
6+
default: false
7+
client: true
8+
random_words_list:
9+
type: list
10+
default: ""
11+
client: true

plugin.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# name: discourse-anon-usernames
4-
# about: TODO
4+
# about: Generate anonymous usernames for users and blocks them from using their real names in usernames
55
# meta_topic_id: TODO
66
# version: 0.0.1
77
# authors: Discourse
@@ -16,6 +16,17 @@ module ::DiscourseAnonUsernames
1616

1717
require_relative "lib/discourse_anon_usernames/engine"
1818

19+
register_asset "stylesheets/anon-usernames.scss"
20+
1921
after_initialize do
20-
# Code which should run after Rails has finished booting
22+
register_modifier(:username_validation) do |errors, context|
23+
next if context.object.nil? || context.object.name.blank?
24+
25+
username = context.username
26+
last_name = context.object.name.split(" ").last
27+
28+
is_leaking_last_name = username.downcase.include?(last_name.downcase)
29+
30+
errors << I18n.t("discourse_anon_usernames.errors.username_invalid") if is_leaking_last_name
31+
end
2132
end

0 commit comments

Comments
 (0)