Add post-connect UI to login via SASL

If the server supports SASL and if we aren't logged in with any
account, add a UI to authenticate via SASL. This allows users to
login anonymously then login via SASL.

This will also ease the draft/account-registration implementation.
This commit is contained in:
Simon Ser 2021-11-21 16:40:46 +01:00
parent 24b50a332c
commit 3e2ac307f6
4 changed files with 120 additions and 3 deletions

View file

@ -8,12 +8,13 @@ import ConnectForm from "./connect-form.js";
import JoinForm from "./join-form.js"; 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 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";
import { html, Component, createRef } from "../lib/index.js"; import { html, Component, createRef } from "../lib/index.js";
import { strip as stripANSI } from "../lib/ansi.js"; import { strip as stripANSI } from "../lib/ansi.js";
import { SERVER_BUFFER, BufferType, ReceiptType, ServerStatus, Unread, State } from "../state.js"; import { SERVER_BUFFER, BufferType, ReceiptType, ServerStatus, Unread, State, getServerName } from "../state.js";
import commands from "../commands.js"; import commands from "../commands.js";
import { setup as setupKeybindings } from "../keybindings.js"; import { setup as setupKeybindings } from "../keybindings.js";
import * as store from "../store.js"; import * as store from "../store.js";
@ -191,6 +192,7 @@ export default class App extends Component {
this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this); this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this);
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.saveReceipts = debounce(this.saveReceipts.bind(this), 500); this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
@ -1377,6 +1379,35 @@ export default class App extends Component {
this.setState({ dialog: null, dialogData: null }); this.setState({ dialog: null, dialogData: null });
} }
handleAuthClick(serverID) {
let client = this.clients.get(serverID);
this.openDialog("auth", { username: client.nick });
}
handleAuthSubmit(username, password) {
let serverID = State.getActiveServerID(this.state);
let client = this.clients.get(serverID);
client.authenticate("PLAIN", { username, password }).then(() => {
let firstClient = this.clients.values().next().value;
if (client !== firstClient) {
return;
}
let autoconnect = store.autoconnect.load();
if (!autoconnect) {
return;
}
console.log("Saving SASL PLAIN credentials");
autoconnect = {
...autoconnect,
saslPlain: { username, password },
};
store.autoconnect.put(autoconnect);
});
this.dismissDialog();
}
handleAddNetworkClick() { handleAddNetworkClick() {
this.openDialog("network"); this.openDialog("network");
} }
@ -1560,6 +1591,13 @@ export default class App extends Component {
</> </>
`; `;
break; break;
case "auth":
dialog = html`
<${Dialog} title="Login to ${getServerName(activeServer, activeBouncerNetwork, isBouncer)}" onDismiss=${this.dismissDialog}>
<${AuthForm} username=${dialogData.username} onSubmit=${this.handleAuthSubmit}/>
</>
`;
break;
} }
let error = null; let error = null;
@ -1616,7 +1654,9 @@ export default class App extends Component {
server=${activeServer} server=${activeServer}
isBouncer=${isBouncer} isBouncer=${isBouncer}
onChannelClick=${this.handleChannelClick} onChannelClick=${this.handleChannelClick}
onNickClick=${this.handleNickClick}/> onNickClick=${this.handleNickClick}
onAuthClick=${() => this.handleAuthClick(activeBuffer.server)}
/>
</section> </section>
</> </>
${memberList} ${memberList}

51
components/auth-form.js Normal file
View file

@ -0,0 +1,51 @@
import { html, Component } from "../lib/index.js";
export default class NetworkForm extends Component {
state = {
username: "",
password: "",
};
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
if (props.username) {
this.state.username = props.username;
}
}
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.username, this.state.password);
}
render() {
return html`
<form onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
<label>
Username:<br/>
<input type="username" name="username" value=${this.state.username} required/>
</label>
<br/><br/>
<label>
Password:<br/>
<input type="password" name="password" value=${this.state.password} required autofocus/>
</label>
<br/><br/>
<button>Login</button>
</form>
`;
}
}

View file

@ -2,7 +2,7 @@ import { html, Component } from "../lib/index.js";
import linkify from "../lib/linkify.js"; import linkify from "../lib/linkify.js";
import * as irc from "../lib/irc.js"; import * as irc from "../lib/irc.js";
import { strip as stripANSI } from "../lib/ansi.js"; import { strip as stripANSI } from "../lib/ansi.js";
import { BufferType, getNickURL, getChannelURL, getMessageURL } from "../state.js"; import { BufferType, ServerStatus, getNickURL, getChannelURL, getMessageURL } from "../state.js";
import * as store from "../store.js"; import * as store from "../store.js";
import Membership from "./membership.js"; import Membership from "./membership.js";
@ -457,6 +457,26 @@ class ProtocolHandlerNagger extends Component {
} }
} }
function AuthNagger({ server, onClick }) {
let accDesc = "an account on this server";
if (server.isupport.has("NETWORK")) {
accDesc = "a " + server.isupport.get("NETWORK") + " account";
}
function handleClick(event) {
event.preventDefault();
onClick();
}
return html`
<div class="logline">
<${Timestamp}/>
${" "}
You are unauthenticated on this server, <a href="#" onClick=${handleClick}>login</a> if you have ${accDesc}
</div>
`;
}
class DateSeparator extends Component { class DateSeparator extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -508,6 +528,9 @@ export default class Buffer extends Component {
let name = server.isupport.get("NETWORK"); let name = server.isupport.get("NETWORK");
children.push(html`<${ProtocolHandlerNagger} bouncerName=${name}/>`); children.push(html`<${ProtocolHandlerNagger} bouncerName=${name}/>`);
} }
if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) {
children.push(html`<${AuthNagger} server=${server} onClick=${this.props.onAuthClick}/>`);
}
let onChannelClick = this.props.onChannelClick; let onChannelClick = this.props.onChannelClick;
let onNickClick = this.props.onNickClick; let onNickClick = this.props.onNickClick;

View file

@ -256,6 +256,7 @@ export const State = {
isupport: new Map(), isupport: new Map(),
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459), users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
account: null, account: null,
supportsSASLPlain: false,
}); });
return [id, { servers }]; return [id, { servers }];
}, },
@ -346,6 +347,8 @@ export const State = {
}; };
}), }),
}; };
case "CAP":
return updateServer({ supportsSASLPlain: client.supportsSASL("PLAIN") });
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: