diff --git a/components/app.js b/components/app.js index 094a5f5..856206f 100644 --- a/components/app.js +++ b/components/app.js @@ -8,12 +8,13 @@ import ConnectForm from "./connect-form.js"; import JoinForm from "./join-form.js"; import Help from "./help.js"; import NetworkForm from "./network-form.js"; +import AuthForm from "./auth-form.js"; import Composer from "./composer.js"; import ScrollManager from "./scroll-manager.js"; import Dialog from "./dialog.js"; import { html, Component, createRef } from "../lib/index.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 { setup as setupKeybindings } from "../keybindings.js"; import * as store from "../store.js"; @@ -191,6 +192,7 @@ export default class App extends Component { this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this); this.handleNetworkRemove = this.handleNetworkRemove.bind(this); this.handleDismissError = this.handleDismissError.bind(this); + this.handleAuthSubmit = this.handleAuthSubmit.bind(this); this.saveReceipts = debounce(this.saveReceipts.bind(this), 500); @@ -1377,6 +1379,35 @@ export default class App extends Component { 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() { this.openDialog("network"); } @@ -1560,6 +1591,13 @@ export default class App extends Component { `; 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; @@ -1616,7 +1654,9 @@ export default class App extends Component { server=${activeServer} isBouncer=${isBouncer} onChannelClick=${this.handleChannelClick} - onNickClick=${this.handleNickClick}/> + onNickClick=${this.handleNickClick} + onAuthClick=${() => this.handleAuthClick(activeBuffer.server)} + /> ${memberList} diff --git a/components/auth-form.js b/components/auth-form.js new file mode 100644 index 0000000..6eccdaa --- /dev/null +++ b/components/auth-form.js @@ -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` +
+ +

+ + +

+ + +
+ `; + } +} diff --git a/components/buffer.js b/components/buffer.js index e661a1b..8f2e30e 100644 --- a/components/buffer.js +++ b/components/buffer.js @@ -2,7 +2,7 @@ import { html, Component } from "../lib/index.js"; import linkify from "../lib/linkify.js"; import * as irc from "../lib/irc.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 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` +
+ <${Timestamp}/> + ${" "} + You are unauthenticated on this server, login if you have ${accDesc} +
+ `; +} + class DateSeparator extends Component { constructor(props) { super(props); @@ -508,6 +528,9 @@ export default class Buffer extends Component { let name = server.isupport.get("NETWORK"); 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 onNickClick = this.props.onNickClick; diff --git a/state.js b/state.js index e8fa354..33b3142 100644 --- a/state.js +++ b/state.js @@ -256,6 +256,7 @@ export const State = { isupport: new Map(), users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459), account: null, + supportsSASLPlain: false, }); return [id, { servers }]; }, @@ -346,6 +347,8 @@ export const State = { }; }), }; + case "CAP": + return updateServer({ supportsSASLPlain: client.supportsSASL("PLAIN") }); case irc.RPL_LOGGEDIN: return updateServer({ account: msg.params[2] }); case irc.RPL_LOGGEDOUT: