forked from CringeStudios/gamja
Add support for draft/account-registration
A new UI to register and verify accounts is added.
This commit is contained in:
parent
b1d5f1436e
commit
be08302c1f
@ -9,6 +9,8 @@ import JoinForm from "./join-form.js";
|
|||||||
import Help from "./help.js";
|
import Help from "./help.js";
|
||||||
import NetworkForm from "./network-form.js";
|
import NetworkForm from "./network-form.js";
|
||||||
import AuthForm from "./auth-form.js";
|
import AuthForm from "./auth-form.js";
|
||||||
|
import RegisterForm from "./register-form.js";
|
||||||
|
import VerifyForm from "./verify-form.js";
|
||||||
import Composer from "./composer.js";
|
import Composer from "./composer.js";
|
||||||
import ScrollManager from "./scroll-manager.js";
|
import ScrollManager from "./scroll-manager.js";
|
||||||
import Dialog from "./dialog.js";
|
import Dialog from "./dialog.js";
|
||||||
@ -193,6 +195,8 @@ export default class App extends Component {
|
|||||||
this.handleNetworkRemove = this.handleNetworkRemove.bind(this);
|
this.handleNetworkRemove = this.handleNetworkRemove.bind(this);
|
||||||
this.handleDismissError = this.handleDismissError.bind(this);
|
this.handleDismissError = this.handleDismissError.bind(this);
|
||||||
this.handleAuthSubmit = this.handleAuthSubmit.bind(this);
|
this.handleAuthSubmit = this.handleAuthSubmit.bind(this);
|
||||||
|
this.handleRegisterSubmit = this.handleRegisterSubmit.bind(this);
|
||||||
|
this.handleVerifySubmit = this.handleVerifySubmit.bind(this);
|
||||||
|
|
||||||
this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
|
this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
|
||||||
|
|
||||||
@ -1403,6 +1407,7 @@ export default class App extends Component {
|
|||||||
handleAuthSubmit(username, password) {
|
handleAuthSubmit(username, password) {
|
||||||
let serverID = State.getActiveServerID(this.state);
|
let serverID = State.getActiveServerID(this.state);
|
||||||
let client = this.clients.get(serverID);
|
let client = this.clients.get(serverID);
|
||||||
|
// TODO: show auth status (pending/error) in dialog
|
||||||
client.authenticate("PLAIN", { username, password }).then(() => {
|
client.authenticate("PLAIN", { username, password }).then(() => {
|
||||||
let firstClient = this.clients.values().next().value;
|
let firstClient = this.clients.values().next().value;
|
||||||
if (client !== firstClient) {
|
if (client !== firstClient) {
|
||||||
@ -1424,6 +1429,51 @@ export default class App extends Component {
|
|||||||
this.dismissDialog();
|
this.dismissDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRegisterClick(serverID) {
|
||||||
|
let client = this.clients.get(serverID);
|
||||||
|
let emailRequired = client.checkAccountRegistrationCap("email-required");
|
||||||
|
this.openDialog("register", { emailRequired });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRegisterSubmit(email, password) {
|
||||||
|
let serverID = State.getActiveServerID(this.state);
|
||||||
|
let client = this.clients.get(serverID);
|
||||||
|
// TODO: show registration status (pending/error) in dialog
|
||||||
|
client.registerAccount(email, password).then((data) => {
|
||||||
|
this.dismissDialog();
|
||||||
|
|
||||||
|
if (data.verificationRequired) {
|
||||||
|
this.openDialog("verify", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstClient = this.clients.values().next().value;
|
||||||
|
if (client !== firstClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let autoconnect = store.autoconnect.load();
|
||||||
|
if (!autoconnect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Saving account registration credentials");
|
||||||
|
autoconnect = {
|
||||||
|
...autoconnect,
|
||||||
|
saslPlain: { username: data.account, password },
|
||||||
|
};
|
||||||
|
store.autoconnect.put(autoconnect);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVerifySubmit(code) {
|
||||||
|
let serverID = State.getActiveServerID(this.state);
|
||||||
|
let client = this.clients.get(serverID);
|
||||||
|
// TODO: display verification status (pending/error) in dialog
|
||||||
|
client.verifyAccount(this.state.dialogData.account, code).then(() => {
|
||||||
|
this.dismissDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleAddNetworkClick() {
|
handleAddNetworkClick() {
|
||||||
this.openDialog("network");
|
this.openDialog("network");
|
||||||
}
|
}
|
||||||
@ -1614,6 +1664,19 @@ export default class App extends Component {
|
|||||||
</>
|
</>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
|
case "register":
|
||||||
|
dialog = html`
|
||||||
|
<${Dialog} title="Register a new ${getServerName(activeServer, activeBouncerNetwork, isBouncer)} account" onDismiss=${this.dismissDialog}>
|
||||||
|
<${RegisterForm} emailRequired=${dialogData.emailRequired} onSubmit=${this.handleRegisterSubmit}/>
|
||||||
|
</>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case "verify":
|
||||||
|
dialog = html`
|
||||||
|
<${Dialog} title="Verify ${getServerName(activeServer, activeBouncerNetwork, isBouncer)} account" onDismiss=${this.dismissDialog}>
|
||||||
|
<${VerifyForm} account=${dialogData.account} message=${dialogData.message} onSubmit=${this.handleVerifySubmit}/>
|
||||||
|
</>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -1669,9 +1732,11 @@ export default class App extends Component {
|
|||||||
buffer=${activeBuffer}
|
buffer=${activeBuffer}
|
||||||
server=${activeServer}
|
server=${activeServer}
|
||||||
isBouncer=${isBouncer}
|
isBouncer=${isBouncer}
|
||||||
|
bouncerNetwork=${activeBouncerNetwork}
|
||||||
onChannelClick=${this.handleChannelClick}
|
onChannelClick=${this.handleChannelClick}
|
||||||
onNickClick=${this.handleNickClick}
|
onNickClick=${this.handleNickClick}
|
||||||
onAuthClick=${() => this.handleAuthClick(activeBuffer.server)}
|
onAuthClick=${() => this.handleAuthClick(activeBuffer.server)}
|
||||||
|
onRegisterClick=${() => this.handleRegisterClick(activeBuffer.server)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
@ -96,7 +96,7 @@ class LogLine extends Component {
|
|||||||
|
|
||||||
let lineClass = "";
|
let lineClass = "";
|
||||||
let content;
|
let content;
|
||||||
let invitee, target;
|
let invitee, target, account;
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case "NOTICE":
|
case "NOTICE":
|
||||||
case "PRIVMSG":
|
case "PRIVMSG":
|
||||||
@ -205,12 +205,28 @@ class LogLine extends Component {
|
|||||||
content = linkify(stripANSI(msg.params[1]), onChannelClick);
|
content = linkify(stripANSI(msg.params[1]), onChannelClick);
|
||||||
break;
|
break;
|
||||||
case irc.RPL_LOGGEDIN:
|
case irc.RPL_LOGGEDIN:
|
||||||
let account = msg.params[2];
|
account = msg.params[2];
|
||||||
content = html`You are now authenticated as ${account}`;
|
content = html`You are now authenticated as ${account}`;
|
||||||
break;
|
break;
|
||||||
case irc.RPL_LOGGEDOUT:
|
case irc.RPL_LOGGEDOUT:
|
||||||
content = html`You are now unauthenticated`;
|
content = html`You are now unauthenticated`;
|
||||||
break;
|
break;
|
||||||
|
case "REGISTER":
|
||||||
|
account = msg.params[1];
|
||||||
|
let reason = linkify(msg.params[2]);
|
||||||
|
switch (msg.params[0]) {
|
||||||
|
case "SUCCESS":
|
||||||
|
content = html`A new account has been created, you are now authenticated as ${account}`;
|
||||||
|
break;
|
||||||
|
case "VERIFICATION_REQUIRED":
|
||||||
|
content = html`A new account has been created, but further action is required to complete registration: ${reason}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "VERIFY":
|
||||||
|
account = msg.params[1];
|
||||||
|
content = html`The new account has been verified, you are now authenticated as ${account}`;
|
||||||
|
break;
|
||||||
case irc.RPL_UMODEIS:
|
case irc.RPL_UMODEIS:
|
||||||
let mode = msg.params[1];
|
let mode = msg.params[1];
|
||||||
if (mode) {
|
if (mode) {
|
||||||
@ -233,6 +249,10 @@ class LogLine extends Component {
|
|||||||
content = html`${msg.command} ${linkify(msg.params.join(" "))}`;
|
content = html`${msg.command} ${linkify(msg.params.join(" "))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline ${lineClass}" data-key=${msg.key}>
|
<div class="logline ${lineClass}" data-key=${msg.key}>
|
||||||
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
|
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
|
||||||
@ -457,22 +477,37 @@ class ProtocolHandlerNagger extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthNagger({ server, onClick }) {
|
function AccountNagger({ server, onAuthClick, onRegisterClick }) {
|
||||||
let accDesc = "an account on this server";
|
let accDesc = "an account on this server";
|
||||||
if (server.isupport.has("NETWORK")) {
|
if (server.isupport.has("NETWORK")) {
|
||||||
accDesc = "a " + server.isupport.get("NETWORK") + " account";
|
accDesc = "a " + server.isupport.get("NETWORK") + " account";
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(event) {
|
function handleAuthClick(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onClick();
|
onAuthClick();
|
||||||
|
}
|
||||||
|
function handleRegisterClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
onRegisterClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = [html`
|
||||||
|
You are unauthenticated on this server,
|
||||||
|
${" "}
|
||||||
|
<a href="#" onClick=${handleAuthClick}>login</a>
|
||||||
|
${" "}
|
||||||
|
`];
|
||||||
|
|
||||||
|
if (server.supportsAccountRegistration) {
|
||||||
|
msg.push(html`or <a href="#" onClick=${handleRegisterClick}>register</a> ${accDesc}`);
|
||||||
|
} else {
|
||||||
|
msg.push(html`if you have ${accDesc}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline">
|
<div class="logline">
|
||||||
<${Timestamp}/>
|
<${Timestamp}/> ${msg}
|
||||||
${" "}
|
|
||||||
You are unauthenticated on this server, <a href="#" onClick=${handleClick}>login</a> if you have ${accDesc}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -515,21 +550,29 @@ export default class Buffer extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let buf = this.props.buffer;
|
let buf = this.props.buffer;
|
||||||
let server = this.props.server;
|
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let server = this.props.server;
|
||||||
|
let bouncerNetwork = this.props.bouncerNetwork;
|
||||||
|
let serverName = server.isupport.get("NETWORK");
|
||||||
|
|
||||||
let children = [];
|
let children = [];
|
||||||
if (buf.type == BufferType.SERVER) {
|
if (buf.type == BufferType.SERVER) {
|
||||||
children.push(html`<${NotificationNagger}/>`);
|
children.push(html`<${NotificationNagger}/>`);
|
||||||
}
|
}
|
||||||
if (buf.type == BufferType.SERVER && this.props.isBouncer && !server.isupport.has("BOUNCER_NETID")) {
|
if (buf.type == BufferType.SERVER && this.props.isBouncer && !server.isupport.has("BOUNCER_NETID")) {
|
||||||
let name = server.isupport.get("NETWORK");
|
children.push(html`<${ProtocolHandlerNagger} bouncerName=${serverName}/>`);
|
||||||
children.push(html`<${ProtocolHandlerNagger} bouncerName=${name}/>`);
|
|
||||||
}
|
}
|
||||||
if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) {
|
if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) {
|
||||||
children.push(html`<${AuthNagger} server=${server} onClick=${this.props.onAuthClick}/>`);
|
children.push(html`
|
||||||
|
<${AccountNagger}
|
||||||
|
server=${server}
|
||||||
|
onAuthClick=${this.props.onAuthClick}
|
||||||
|
onRegisterClick=${this.props.onRegisterClick}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let onChannelClick = this.props.onChannelClick;
|
let onChannelClick = this.props.onChannelClick;
|
||||||
|
54
components/register-form.js
Normal file
54
components/register-form.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { html, Component } from "../lib/index.js";
|
||||||
|
|
||||||
|
export default class RegisterForm extends Component {
|
||||||
|
state = {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event) {
|
||||||
|
let target = event.target;
|
||||||
|
let value = target.type == "checkbox" ? target.checked : target.value;
|
||||||
|
this.setState({ [target.name]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.props.onSubmit(this.state.email, this.state.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<form onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
|
||||||
|
<label>
|
||||||
|
E-mail:<br/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value=${this.state.email}
|
||||||
|
required=${this.props.emailRequired}
|
||||||
|
placeholder=${this.props.emailRequired ? null : "(optional)"}
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password:<br/>
|
||||||
|
<input type="password" name="password" value=${this.state.password} required/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<button>Register</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
43
components/verify-form.js
Normal file
43
components/verify-form.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { html, Component } from "../lib/index.js";
|
||||||
|
import linkify from "../lib/linkify.js";
|
||||||
|
|
||||||
|
export default class RegisterForm extends Component {
|
||||||
|
state = {
|
||||||
|
code: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event) {
|
||||||
|
let target = event.target;
|
||||||
|
let value = target.type == "checkbox" ? target.checked : target.value;
|
||||||
|
this.setState({ [target.name]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.props.onSubmit(this.state.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<form onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
|
||||||
|
<p>Your account <strong>${this.props.account}</strong> has been created, but a verification code is required to complete the registration:<br/>${linkify(this.props.message)}</p>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Verification code:<br/>
|
||||||
|
<input type="text" name="code" value=${this.state.code} required autofocus autocomplete="off"/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<button>Verify account</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ const permanentCaps = [
|
|||||||
"server-time",
|
"server-time",
|
||||||
"setname",
|
"setname",
|
||||||
|
|
||||||
|
"draft/account-registration",
|
||||||
"draft/chathistory",
|
"draft/chathistory",
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
"draft/extended-monitor",
|
"draft/extended-monitor",
|
||||||
@ -552,6 +553,14 @@ export default class Client extends EventTarget {
|
|||||||
return saslCap.split(",").includes(mech);
|
return saslCap.split(",").includes(mech);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkAccountRegistrationCap(k) {
|
||||||
|
let v = this.availableCaps["draft/account-registration"];
|
||||||
|
if (v === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return v.split(",").includes(k);
|
||||||
|
}
|
||||||
|
|
||||||
requestCaps() {
|
requestCaps() {
|
||||||
let wantCaps = [].concat(permanentCaps);
|
let wantCaps = [].concat(permanentCaps);
|
||||||
if (!this.params.bouncerNetwork) {
|
if (!this.params.bouncerNetwork) {
|
||||||
@ -915,4 +924,45 @@ export default class Client extends EventTarget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerAccount(email, password) {
|
||||||
|
let msg = {
|
||||||
|
command: "REGISTER",
|
||||||
|
params: ["*", email || "*", password],
|
||||||
|
};
|
||||||
|
return this.roundtrip(msg, (msg) => {
|
||||||
|
switch (msg.command) {
|
||||||
|
case "REGISTER":
|
||||||
|
let result = msg.params[0];
|
||||||
|
return {
|
||||||
|
verificationRequired: result === "VERIFICATION_REQUIRED",
|
||||||
|
account: msg.params[1],
|
||||||
|
message: msg.params[2],
|
||||||
|
};
|
||||||
|
case "FAIL":
|
||||||
|
if (msg.params[0] === "REGISTER") {
|
||||||
|
throw msg;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAccount(account, code) {
|
||||||
|
let msg = {
|
||||||
|
command: "VERIFY",
|
||||||
|
params: [account, code],
|
||||||
|
};
|
||||||
|
return this.roundtrip(msg, (msg) => {
|
||||||
|
switch (msg.command) {
|
||||||
|
case "VERIFY":
|
||||||
|
return { message: msg.params[2] };
|
||||||
|
case "FAIL":
|
||||||
|
if (msg.params[0] === "VERIFY") {
|
||||||
|
throw msg;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
state.js
12
state.js
@ -257,6 +257,7 @@ export const State = {
|
|||||||
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
|
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
|
||||||
account: null,
|
account: null,
|
||||||
supportsSASLPlain: false,
|
supportsSASLPlain: false,
|
||||||
|
supportsAccountRegistration: false,
|
||||||
});
|
});
|
||||||
return [id, { servers }];
|
return [id, { servers }];
|
||||||
},
|
},
|
||||||
@ -348,11 +349,20 @@ export const State = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
case "CAP":
|
case "CAP":
|
||||||
return updateServer({ supportsSASLPlain: client.supportsSASL("PLAIN") });
|
return updateServer({
|
||||||
|
supportsSASLPlain: client.supportsSASL("PLAIN"),
|
||||||
|
supportsAccountRegistration: !!client.enabledCaps["draft/account-registration"],
|
||||||
|
});
|
||||||
case irc.RPL_LOGGEDIN:
|
case irc.RPL_LOGGEDIN:
|
||||||
return updateServer({ account: msg.params[2] });
|
return updateServer({ account: msg.params[2] });
|
||||||
case irc.RPL_LOGGEDOUT:
|
case irc.RPL_LOGGEDOUT:
|
||||||
return updateServer({ account: null });
|
return updateServer({ account: null });
|
||||||
|
case "REGISTER":
|
||||||
|
case "VERIFY":
|
||||||
|
if (msg.params[0] === "SUCCESS") {
|
||||||
|
return updateServer({ account: msg.params[1] });
|
||||||
|
}
|
||||||
|
break;
|
||||||
case irc.RPL_NOTOPIC:
|
case irc.RPL_NOTOPIC:
|
||||||
channel = msg.params[1];
|
channel = msg.params[1];
|
||||||
return updateBuffer(channel, { topic: null });
|
return updateBuffer(channel, { topic: null });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user