}
const REDIRECT_MAGIC_STRING: &str = "urn:ietf:wg:oauth:2.0:oob";
+const WEBSITE: &str =
+ "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/";
+const WEBSITE_REGISTERED: &str = "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/registered.html";
pub enum AppTokenType<'a> {
- ClientCredentials,
+ ClientCredentialsLogin,
+ ClientCredentialsRegister,
AuthorizationCode(&'a str),
}
Ok(())
}
- pub fn register_client(&mut self) -> Result<Application, ClientError> {
+ pub fn register_client(
+ &mut self,
+ creating_account: bool,
+ ) -> Result<Application, ClientError> {
+ // If we're logging in to an existing account, we set
+ // redirect_uris to the magic string that causes the Mastodon
+ // server's emailed link to display the code visibly to the
+ // user, so that they can paste it back into our TUI to let it
+ // retrieve its user token.
+ //
+ // If we're _creating_ an account, we instead set it to
+ // redirect to a Mastodonochrome web page, because in that
+ // situation, the browser only needs to say 'ok' - and what
+ // happens if a browser tries to redirect to that urn:... URI
+ // is confusing and looks more like an error than a success.
+ //
+ // The fact that "redirect_uris" is plural in this request and
+ // singular in the followup ones suggests that we _ought_ to
+ // be able to permit both possibilities here, and in the next
+ // request, pick one of them. But I don't know how to do that,
+ // so instead, we specify only the right one of them here.
+ let redirect_uri = if creating_account {
+ WEBSITE_REGISTERED
+ } else {
+ REDIRECT_MAGIC_STRING
+ };
let req = Req::post("/api/v1/apps")
- .param("redirect_uris", REDIRECT_MAGIC_STRING)
+ .param("redirect_uris", redirect_uri)
.param("client_name", "Mastodonochrome")
.param("scopes", "read write push")
- .param("website", "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/");
+ .param("website", WEBSITE);
let (url, rsp) = self.api_request(req)?;
let rspstatus = rsp.status();
if !rspstatus.is_success() {
}?;
let req = Req::post("/oauth/token")
- .param("redirect_uri", REDIRECT_MAGIC_STRING)
.param("client_id", client_id)
.param("client_secret", client_secret);
let req = match toktype {
- AppTokenType::ClientCredentials => {
- req.param("grant_type", "client_credentials")
- }
+ AppTokenType::ClientCredentialsLogin => req
+ .param("grant_type", "client_credentials")
+ .param("redirect_uri", REDIRECT_MAGIC_STRING),
+ AppTokenType::ClientCredentialsRegister => req
+ .param("grant_type", "client_credentials")
+ .param("redirect_uri", WEBSITE_REGISTERED)
+ .param("scope", "read write push"),
AppTokenType::AuthorizationCode(code) => req
.param("grant_type", "authorization_code")
+ .param("redirect_uri", REDIRECT_MAGIC_STRING)
.param("code", code)
.param("scope", "read write push"),
};
.url(self.auth.instance_url.as_deref().expect("should have set up an instance URL before calling get_auth_url"))?;
Ok(url.to_string())
}
+
+ pub fn register_account(
+ &mut self,
+ username: &str,
+ email: &str,
+ password: &str,
+ language: &str,
+ ) -> Result<Token, ClientError> {
+ let req = Req::post("api/v1/accounts")
+ .param("username", username)
+ .param("email", email)
+ .param("password", password)
+ .param("agreement", true) // the calling UI should have confirmed
+ .param("locale", language);
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ let tok: Token = match serde_json::from_str(&rsp.text()?) {
+ Ok(tok) => Ok(tok),
+ Err(e) => Err(ClientError::UrlError(url, e.to_string())),
+ }?;
+ Ok(tok)
+ }
+ }
+
+ pub fn register_confirmation(
+ &mut self,
+ urlstr: &str,
+ ) -> Result<(), ClientError> {
+ let url = match Url::parse(urlstr) {
+ Ok(url) => Ok(url),
+ Err(e) => Err(ClientError::UrlParseError(
+ urlstr.to_owned(),
+ e.to_string(),
+ )),
+ }?;
+ let req = self.client.request(reqwest::Method::GET, url).build()?;
+ let (rsp, log) = execute_and_log_request(&self.client, req)?;
+ self.consume_transaction_log(log);
+ let rspstatus = rsp.status();
+ if rspstatus.is_redirection() || rspstatus.is_success() {
+ Ok(())
+ } else {
+ Err(ClientError::UrlError(
+ urlstr.to_owned(),
+ rspstatus.to_string(),
+ ))
+ }
+ }
}
use reqwest::Url;
+use std::cell::RefCell;
+use std::rc::Rc;
+use sys_locale::get_locale;
+use super::activity_stack::UtilityActivity;
use super::auth::AuthConfig;
use super::client::{AppTokenType, Client, ClientError};
use super::coloured_string::*;
use super::config::ConfigLocation;
-use super::editor::EditableMenuLine;
+use super::editor::{
+ count_edit_chars, EditableMenuLine, EditableMenuLineData,
+};
use super::text::*;
use super::tui::{
- ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*,
+ ActivityState, CursorPosition, LogicalAction, OurKey, OurKey::*, TuiError,
};
use super::types::{Account, Application, Instance};
use super::TopLevelError;
+#[derive(Default)]
+struct MandatoryString(String);
+
+impl MandatoryString {
+ fn ok(&self) -> bool {
+ !self.0.is_empty()
+ }
+}
+
+impl EditableMenuLineData for MandatoryString {
+ fn display(&self) -> ColouredString {
+ if self.0.is_empty() {
+ ColouredString::uniform("none", 'r')
+ } else {
+ ColouredString::plain(&self.0)
+ }
+ }
+
+ fn to_text(&self) -> String {
+ self.0.clone()
+ }
+
+ fn update(&mut self, text: &str) {
+ *self = Self(text.to_owned());
+ }
+}
+
+#[derive(Debug)]
+struct Password {
+ this: Rc<RefCell<String>>, // the password we're editing
+ other: Rc<RefCell<String>>, // the one in the other field
+ confirmation: bool, // are we the confirmation field?
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum PasswordDiagnostic {
+ OrigGood,
+ ConfirmGood,
+ Empty,
+ TooShort,
+ TooLong,
+ Mismatch,
+ SilentMismatch,
+}
+
+impl Password {
+ fn diagnostic(&self) -> PasswordDiagnostic {
+ let thisstr = self.this.borrow();
+ let nchars = count_edit_chars(&thisstr);
+ if thisstr.is_empty() {
+ PasswordDiagnostic::Empty
+ } else if self.confirmation {
+ let otherstr = self.other.borrow();
+ // No need to diagnose a mismatch if the other password
+ // field is empty, because we're already displaying an
+ // error message in the other edit line.
+ if otherstr.is_empty() {
+ PasswordDiagnostic::SilentMismatch
+ } else if *thisstr != *otherstr {
+ PasswordDiagnostic::Mismatch
+ } else {
+ PasswordDiagnostic::ConfirmGood
+ }
+ } else {
+ // Diagnose bad password lengths. I'm not 100% sure
+ // whether this can be reconfigured per Mastodon
+ // instance, but as of
+ // https://github.com/mastodon/mastodon commit
+ // bd415af9a11fe7057c9f428b7bdaeb8d4fb3a77b,
+ // config/initializers/devise.rb line 281 sets
+ // 'config.password_length = 8..72'.
+ if nchars < 8 {
+ PasswordDiagnostic::TooShort
+ } else if nchars > 72 {
+ PasswordDiagnostic::TooLong
+ } else {
+ PasswordDiagnostic::OrigGood
+ }
+ }
+ }
+
+ fn ok(&self) -> bool {
+ match self.diagnostic() {
+ PasswordDiagnostic::OrigGood | PasswordDiagnostic::ConfirmGood => {
+ true
+ }
+ _ => false,
+ }
+ }
+}
+impl EditableMenuLineData for Password {
+ const SECRET: bool = true;
+
+ fn display(&self) -> ColouredString {
+ let nchars = count_edit_chars(&self.this.borrow());
+ let masked = ColouredString::plain("*").repeat(nchars);
+ match self.diagnostic() {
+ PasswordDiagnostic::OrigGood => {
+ masked + ColouredString::uniform(" length ok", 'f')
+ }
+ PasswordDiagnostic::ConfirmGood => {
+ masked + ColouredString::uniform(" match", 'f')
+ }
+ PasswordDiagnostic::Empty => ColouredString::uniform("none", 'r'),
+ PasswordDiagnostic::SilentMismatch => masked,
+ PasswordDiagnostic::TooShort => {
+ masked + ColouredString::uniform(" too short!", 'r')
+ }
+ PasswordDiagnostic::TooLong => {
+ masked + ColouredString::uniform(" too long!", 'r')
+ }
+ PasswordDiagnostic::Mismatch => {
+ masked + ColouredString::uniform(" mismatch!", 'r')
+ }
+ }
+ }
+
+ fn to_text(&self) -> String {
+ self.this.borrow().clone()
+ }
+
+ fn update(&mut self, text: &str) {
+ *self.this.borrow_mut() = text.to_owned();
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LoginState {
Initial,
LoginTokenError,
LoginSuccess,
LoginFailure,
+ RegisterInput,
+ RegisterFinal,
+ RegisterFailure,
}
struct LoginMenu {
para_server_id: Paragraph,
el_server: EditableMenuLine<String>,
ml_login: MenuKeypressLine,
+ ml_register: MenuKeypressLine,
para_login_instructions: Paragraph,
para_login_url: Paragraph,
el_logincode: EditableMenuLine<String>,
para_login_outcome: Paragraph,
+ el_username: EditableMenuLine<MandatoryString>,
+ el_email: EditableMenuLine<MandatoryString>,
+ el_password: EditableMenuLine<Password>,
+ el_password_confirm: EditableMenuLine<Password>,
+ ml_rules: MenuKeypressLine,
+ cl_accept: CyclingMenuLine<bool>,
+ el_register_confirm: EditableMenuLine<String>,
+ ml_register_done: MenuKeypressLine,
+ para_regconfirm_outcome: Paragraph,
state: LoginState,
application: Option<Application>,
}
'H',
),
);
+ let ml_register = MenuKeypressLine::new(
+ Pr('R'),
+ ColouredString::uniform(
+ "Register on this server as a new user",
+ 'H',
+ ),
+ );
let para_server_id =
Paragraph::new().set_centred(true).set_indent(10, 10);
"".to_owned(),
);
+ let el_username = EditableMenuLine::new(
+ Pr('N'),
+ ColouredString::plain("Name of your new account: "),
+ MandatoryString::default(),
+ );
+ let el_email = EditableMenuLine::new(
+ Pr('E'),
+ ColouredString::plain("Email address to associate with account: "),
+ MandatoryString::default(),
+ );
+ let password1 = Rc::new(RefCell::new("".to_owned()));
+ let password2 = Rc::new(RefCell::new("".to_owned()));
+ let password1c = Rc::clone(&password1);
+ let el_password = EditableMenuLine::new(
+ Pr('P'),
+ ColouredString::plain("Password for the instance website: "),
+ Password {
+ this: password1c,
+ other: Rc::clone(&password2),
+ confirmation: false,
+ },
+ );
+ let el_password_confirm = EditableMenuLine::new(
+ Ctrl('P'),
+ ColouredString::plain("Password again for confirmation: "),
+ Password {
+ this: password2,
+ other: password1,
+ confirmation: true,
+ },
+ );
+ let ml_rules = MenuKeypressLine::new(
+ Pr('R'),
+ ColouredString::uniform("Read the server rules", 'H'),
+ );
+ let cl_accept = CyclingMenuLine::new(
+ Pr('A'),
+ ColouredString::plain("Do you accept the server rules? "),
+ &[
+ (false, ColouredString::uniform("no", 'r')),
+ (true, ColouredString::uniform("yes", 'f')),
+ ],
+ false,
+ );
+
let para_login_outcome =
Paragraph::new().set_centred(true).set_indent(10, 10);
+ let el_register_confirm = EditableMenuLine::new(
+ Pr('U'),
+ ColouredString::plain("Confirmation URL: "),
+ "".to_owned(),
+ );
+ let ml_register_done = MenuKeypressLine::new(
+ Pr('C'),
+ ColouredString::plain(
+ "Continue to Main Menu (I followed the link in a browser)",
+ ),
+ );
+
+ let para_regconfirm_outcome =
+ Paragraph::new().set_centred(true).set_indent(10, 10);
+
let mut menu = LoginMenu {
cfgloc,
title,
para_intro,
el_server,
ml_login,
+ ml_register,
para_server_id,
para_login_instructions,
para_login_url,
el_logincode,
para_login_outcome,
+ el_username,
+ el_email,
+ el_password,
+ el_password_confirm,
+ ml_rules,
+ cl_accept,
+ el_register_confirm,
+ ml_register_done,
+ para_regconfirm_outcome,
state: LoginState::Initial,
application: None,
};
self.alignment_line.check_widths(&mut lmaxwid, &mut rmaxwid);
self.el_server.check_widths(&mut lmaxwid, &mut rmaxwid);
self.ml_login.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.ml_register.check_widths(&mut lmaxwid, &mut rmaxwid);
self.el_logincode.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_username.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_email.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_password.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_password_confirm
+ .check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.ml_rules.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.cl_accept.check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.el_register_confirm
+ .check_widths(&mut lmaxwid, &mut rmaxwid);
+ self.ml_register_done
+ .check_widths(&mut lmaxwid, &mut rmaxwid);
self.el_server.reset_widths();
self.ml_login.reset_widths();
+ self.ml_register.reset_widths();
self.el_logincode.reset_widths();
+ self.el_username.reset_widths();
+ self.el_email.reset_widths();
+ self.el_password.reset_widths();
+ self.el_password_confirm.reset_widths();
+ self.ml_rules.reset_widths();
+ self.cl_accept.reset_widths();
+ self.el_register_confirm.reset_widths();
+ self.ml_register_done.reset_widths();
self.el_server.ensure_widths(lmaxwid, rmaxwid);
self.ml_login.ensure_widths(lmaxwid, rmaxwid);
+ self.ml_register.ensure_widths(lmaxwid, rmaxwid);
self.el_logincode.ensure_widths(lmaxwid, rmaxwid);
+ self.el_username.ensure_widths(lmaxwid, rmaxwid);
+ self.el_email.ensure_widths(lmaxwid, rmaxwid);
+ self.el_password.ensure_widths(lmaxwid, rmaxwid);
+ self.el_password_confirm.ensure_widths(lmaxwid, rmaxwid);
+ self.ml_rules.ensure_widths(lmaxwid, rmaxwid);
+ self.cl_accept.ensure_widths(lmaxwid, rmaxwid);
+ self.el_register_confirm.ensure_widths(lmaxwid, rmaxwid);
+ self.ml_register_done.ensure_widths(lmaxwid, rmaxwid);
(lmaxwid, rmaxwid)
}
}
};
let instance_url = url.as_str().trim_end_matches('/');
- self.el_server.set_data(instance_url.to_owned());
+ self.el_server.set_text(instance_url);
client.auth = AuthConfig::default();
client.auth.instance_url = Some(instance_url.to_owned());
client: &mut Client,
) -> Result<String, ClientError> {
// Register the client and get its details
- let app = client.register_client()?;
- let app_token =
- client.get_app_token(&app, AppTokenType::ClientCredentials)?;
+ let app = client.register_client(false)?;
+ let app_token = client
+ .get_app_token(&app, AppTokenType::ClientCredentialsLogin)?;
client.auth.user_token = Some(app_token.access_token);
let _app: Application = client.verify_app_credentials()?;
let code = self.el_logincode.get_data().trim();
let app = self
.application
- .as_ref()
+ .clone()
.expect("Can't get here without registering an application");
// Use our code to get the final user access token and all the
// account details
let token = client
- .get_app_token(app, AppTokenType::AuthorizationCode(code))?;
+ .get_app_token(&app, AppTokenType::AuthorizationCode(code))?;
client.auth.user_token = Some(token.access_token.clone());
- let account: Account = client.verify_account_credentials()?;
- let instance: Instance = client.instance()?;
+ client.auth.client_id = Some(app.client_id.clone().unwrap());
+ client.auth.client_secret = Some(app.client_secret.clone().unwrap());
+ Ok(finish_account_setup(client, &self.cfgloc)?)
+ }
+
+ fn verify_login_code(&mut self, client: &mut Client) -> LogicalAction {
+ match self.verify_login_code_fallible(client) {
+ Ok(_) => {
+ self.para_login_outcome.clear();
+ self.para_login_outcome.push_text(
+ ColouredString::uniform("Success! Logged in as ", 'H'),
+ false,
+ );
+ self.para_login_outcome.push_text(
+ ColouredString::uniform(&client.our_account_fq(), 'K'),
+ false,
+ );
+ self.para_login_outcome.push_text(
+ ColouredString::general(
+ ". Now press [SPACE] to continue to the Main Menu.",
+ "HHHHHHHHHHHHHKKKKKHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH",
+ ),
+ false,
+ );
+ self.state = LoginState::LoginSuccess;
+ LogicalAction::Nothing
+ }
+ Err(e) => {
+ self.para_login_outcome.clear();
+ self.para_login_outcome.push_text(
+ ColouredString::uniform(
+ &format!("Error logging in: {e}"),
+ 'r',
+ ),
+ false,
+ );
+ self.state = LoginState::LoginFailure;
+ LogicalAction::Beep
+ }
+ }
+ }
+
+ fn register_account_fallible(
+ &mut self,
+ client: &mut Client,
+ ) -> Result<(), TopLevelError> {
+ // Register the client and get its details
+ let app = client.register_client(true)?;
+ let app_token = client
+ .get_app_token(&app, AppTokenType::ClientCredentialsRegister)?;
+ client.auth.user_token = Some(app_token.access_token);
+
+ assert!(
+ self.cl_accept.get_value(),
+ "This keystroke should have been locked until you accepted"
+ );
+
+ // FIXME: ability to configure this?
+ let language = get_locale()
+ .as_deref()
+ .and_then(|s| s.split('-').next())
+ .map(|s| if s.is_empty() { "en" } else { s })
+ .unwrap_or("en")
+ .to_owned();
+ // Send the account registration request
+ let token = client.register_account(
+ &self.el_username.get_data().0,
+ &self.el_email.get_data().0,
+ &self.el_password.get_data().this.borrow(),
+ &language,
+ )?;
+
+ // Now we have a user token. Save what we have of it.
+ let instance_url = client.auth.instance_url.take();
client.auth = AuthConfig {
- account_id: Some(account.id),
- username: Some(account.username),
- instance_url: client.auth.instance_url.clone(),
- instance_domain: Some(instance.domain),
+ account_id: None, // we don't have this yet
+ username: None, // this isn't confirmed either
+ instance_url,
+ instance_domain: None, // we'd have to request an Instance again
client_id: Some(app.client_id.clone().unwrap()),
client_secret: Some(app.client_secret.clone().unwrap()),
- user_token: Some(token.access_token),
+ user_token: Some(token.access_token.clone()),
};
let mut json = serde_json::to_string_pretty(&client.auth).unwrap();
json.push('\n');
self.cfgloc.create_file("auth", &json)?;
+ // FIXME: in final step, self.finish_account_setup(client, &app, token.access_token.clone())
+ self.state = LoginState::RegisterFinal;
+
Ok(())
}
- fn verify_login_code(&mut self, client: &mut Client) -> LogicalAction {
- match self.verify_login_code_fallible(client) {
+ fn register_account(&mut self, client: &mut Client) -> LogicalAction {
+ match self.register_account_fallible(client) {
Ok(_) => {
self.para_login_outcome.clear();
self.para_login_outcome.push_text(
- ColouredString::uniform("Success! Logged in as ", 'H'),
+ ColouredString::uniform(
+ "Success! The server should have sent a confirmation to your email address ",
+ 'H',
+ ),
false,
);
self.para_login_outcome.push_text(
- ColouredString::uniform(&client.our_account_fq(), 'K'),
+ ColouredString::uniform(&self.el_email.get_data().0, 'K'),
false,
);
self.para_login_outcome.push_text(
- ColouredString::general(
- ". Now press [SPACE] to continue to the Main Menu.",
- "HHHHHHHHHHHHHKKKKKHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH",
+ ColouredString::uniform(
+ ". Either paste the confirmation URL from that email below, or visit it in a browser.",
+ 'H',
),
false,
);
- self.state = LoginState::LoginSuccess;
+ self.state = LoginState::RegisterFinal;
LogicalAction::Nothing
}
Err(e) => {
self.para_login_outcome.clear();
self.para_login_outcome.push_text(
ColouredString::uniform(
- &format!("Error logging in: {e}"),
+ &format!("Error registering account: {e}"),
+ 'r',
+ ),
+ false,
+ );
+ self.state = LoginState::RegisterFailure;
+ LogicalAction::Beep
+ }
+ }
+ }
+
+ fn confirm_registration_fallible(
+ &mut self,
+ client: &mut Client,
+ with_url: bool,
+ ) -> Result<(), TopLevelError> {
+ if with_url {
+ let url = self.el_register_confirm.get_data();
+ client.register_confirmation(url)?;
+ }
+ Ok(finish_account_setup(client, &self.cfgloc)?)
+ }
+
+ fn confirm_registration(
+ &mut self,
+ client: &mut Client,
+ with_url: bool,
+ ) -> LogicalAction {
+ match self.confirm_registration_fallible(client, with_url) {
+ Ok(_) => LogicalAction::FinishedLoggingIn,
+ Err(e) => {
+ self.para_regconfirm_outcome.clear();
+ self.para_regconfirm_outcome.push_text(
+ ColouredString::uniform(
+ &format!("Error confirming registration: {e}"),
'r',
),
false,
);
- self.state = LoginState::LoginFailure;
LogicalAction::Beep
}
}
if self.state == LoginState::ServerValid {
lines.extend_from_slice(&BlankLine::render_static());
lines.extend_from_slice(&self.ml_login.render(w));
+ lines.extend_from_slice(&self.ml_register.render(w));
}
if self.state == LoginState::LoginToken
|| self.state == LoginState::LoginTokenError
lines.extend_from_slice(&BlankLine::render_static());
push_split_lines(&mut lines, self.para_login_outcome.render(w));
}
+ if self.state == LoginState::RegisterInput
+ || self.state == LoginState::RegisterFailure
+ {
+ lines.extend_from_slice(&BlankLine::render_static());
+ lines.push(self.el_username.render(
+ w,
+ &mut cursorpos,
+ lines.len(),
+ ));
+ lines.push(self.el_email.render(w, &mut cursorpos, lines.len()));
+ lines.push(self.el_password.render(
+ w,
+ &mut cursorpos,
+ lines.len(),
+ ));
+ lines.push(self.el_password_confirm.render(
+ w,
+ &mut cursorpos,
+ lines.len(),
+ ));
+ lines.extend_from_slice(&self.ml_rules.render(w));
+ lines.extend_from_slice(&self.cl_accept.render(w));
+ }
+ if self.state == LoginState::RegisterFailure
+ || self.state == LoginState::RegisterFinal
+ {
+ lines.extend_from_slice(&BlankLine::render_static());
+ push_split_lines(&mut lines, self.para_login_outcome.render(w));
+ }
+ if self.state == LoginState::RegisterFinal {
+ lines.extend_from_slice(&BlankLine::render_static());
+ lines.push(self.el_register_confirm.render(
+ w,
+ &mut cursorpos,
+ lines.len(),
+ ));
+ lines.extend_from_slice(&self.ml_register_done.render(w));
+ lines.extend_from_slice(&BlankLine::render_static());
+ push_split_lines(
+ &mut lines,
+ self.para_regconfirm_outcome.render(w),
+ );
+ }
while lines.len() + 1 < h {
lines.extend_from_slice(&BlankLine::render_static());
}
}
LoginState::LoginSuccess => status.add(Space, "Main Menu", 100),
+ LoginState::RegisterInput => {
+ if self.el_username.get_data().ok()
+ && self.el_email.get_data().ok()
+ && self.el_password.get_data().ok()
+ && self.el_password_confirm.get_data().ok()
+ && self.cl_accept.get_value()
+ {
+ status.add(Space, "Register account", 100)
+ } else {
+ status.message(
+ "Fill in all fields and accept the server rules",
+ )
+ }
+ }
_ => status.add(Pr('Q'), "Quit", 100),
}
.finalise();
return self.verify_login_code(client);
}
return LogicalAction::Nothing;
+ } else if self.el_username.handle_keypress(key)
+ || self.el_email.handle_keypress(key)
+ || self.el_password_confirm.handle_keypress(key)
+ {
+ self.fix_widths();
+ return LogicalAction::Nothing;
+ } else if self.el_password.handle_keypress(key) {
+ self.fix_widths();
+ if !self.el_password.is_editing() {
+ self.el_password_confirm.set_text("");
+ self.el_password_confirm.start_editing();
+ }
+ return LogicalAction::Nothing;
+ } else if self.el_register_confirm.handle_keypress(key) {
+ self.fix_widths();
+ if !self.el_register_confirm.is_editing() {
+ return self.confirm_registration(client, true);
+ }
+ return LogicalAction::Nothing;
}
match key {
LogicalAction::Nothing
}
}
+ Pr('r') | Pr('R') => {
+ if self.state == LoginState::RegisterInput {
+ // R here means 'read rules'
+ LogicalAction::Goto(UtilityActivity::InstanceRules.into())
+ } else if self.state == LoginState::ServerValid {
+ // R here means 'register as new user'
+ self.state = LoginState::RegisterInput;
+ LogicalAction::Nothing
+ } else {
+ LogicalAction::Nothing
+ }
+ }
Pr('c') | Pr('C') => {
if self.state == LoginState::LoginToken
|| self.state == LoginState::LoginFailure
{
self.el_logincode.start_editing()
+ } else if self.state == LoginState::RegisterFinal {
+ self.confirm_registration(client, false)
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Pr('n') | Pr('N') => {
+ if self.state == LoginState::RegisterInput {
+ self.el_username.start_editing()
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Pr('e') | Pr('E') => {
+ if self.state == LoginState::RegisterInput {
+ self.el_email.start_editing()
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Pr('p') | Pr('P') => {
+ if self.state == LoginState::RegisterInput {
+ self.el_password.set_text("");
+ self.el_password.start_editing()
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Ctrl('P') => {
+ if self.state == LoginState::RegisterInput {
+ self.el_password_confirm.set_text("");
+ self.el_password_confirm.start_editing()
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Pr('a') | Pr('A') => {
+ if self.state == LoginState::RegisterInput {
+ self.cl_accept.cycle()
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+ Pr('u') | Pr('U') => {
+ if self.state == LoginState::RegisterFinal {
+ self.el_register_confirm.start_editing()
} else {
LogicalAction::Nothing
}
Space => {
if self.state == LoginState::LoginSuccess {
LogicalAction::FinishedLoggingIn
+ } else if self.state == LoginState::RegisterInput
+ && self.el_username.get_data().ok()
+ && self.el_email.get_data().ok()
+ && self.el_password.get_data().ok()
+ && self.el_password_confirm.get_data().ok()
+ && self.cl_accept.get_value()
+ {
+ self.register_account(client)
} else {
LogicalAction::Nothing
}
fn resize(&mut self, w: usize, _h: usize) {
self.el_server.resize(w);
self.el_logincode.resize(w);
+ self.el_username.resize(w);
+ self.el_email.resize(w);
+ self.el_password.resize(w);
+ self.el_password_confirm.resize(w);
+ self.el_register_confirm.resize(w);
}
}
+pub fn finish_account_setup(
+ client: &mut Client,
+ cfgloc: &ConfigLocation,
+) -> Result<(), TuiError> {
+ let account: Account = client.verify_account_credentials()?;
+ let instance: Instance = client.instance()?;
+
+ let token = client.auth.user_token.take();
+ let client_id = client.auth.client_id.take();
+ let client_secret = client.auth.client_secret.take();
+ client.auth = AuthConfig {
+ account_id: Some(account.id),
+ username: Some(account.username),
+ instance_url: client.auth.instance_url.clone(),
+ instance_domain: Some(instance.domain),
+ client_id,
+ client_secret,
+ user_token: token,
+ };
+
+ let mut json = serde_json::to_string_pretty(&client.auth).unwrap();
+ json.push('\n');
+ cfgloc.create_file("auth", &json)?;
+
+ Ok(())
+}
+
pub fn login_menu(cfgloc: ConfigLocation) -> Box<dyn ActivityState> {
Box::new(LoginMenu::new(cfgloc))
}