feat(ui,backend): implemented login - UNTESTED!

This commit is contained in:
antifallobst 2023-09-12 22:00:38 +02:00
parent 6c855b24f7
commit ba25f98d58
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
5 changed files with 1072 additions and 35 deletions

884
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,3 +11,8 @@ tokio = { version = "1.32", features = ["macros", "rt-multi-thread"] }
tui = {version = "0.19"} tui = {version = "0.19"}
tui-textarea = { version = "0.2", features = ["crossterm"] } tui-textarea = { version = "0.2", features = ["crossterm"] }
crossterm = { version = "0.25" } crossterm = { version = "0.25" }
reqwest = { version = "0.11.20", features = ["json"] }
clap = { version = "4.4.2", features = ["derive"] }
serde_json = "1.0"
serde = { version = "1.0.183", features = ["derive"] }

122
src/api/mod.rs Normal file
View File

@ -0,0 +1,122 @@
use anyhow::{Error, Ok, Result};
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize)]
struct AuthResponse {
token: String,
}
#[derive(Debug, Deserialize)]
struct UserIdResponse {
name: String,
id: i64,
}
#[derive(Debug, Deserialize)]
struct UserInfoResponse {
id: i64,
name: String,
joined: i64,
is_admin: bool,
}
#[derive(Debug)]
pub struct Api {
token: String,
base_url: String,
}
impl Api {
pub async fn from_creds(
base_url: &str,
username: String,
password: String,
) -> Result<Result<Self>> {
let token = {
let body = json!({
"username": username,
"password": password,
});
let request = format!("{base_url}/account/authenticate");
let response = Client::new().post(request).json(&body).send().await?;
match response.status() {
StatusCode::OK => (),
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
StatusCode::UNAUTHORIZED => return Ok(Err(Error::msg("Wrong password!"))),
StatusCode::FORBIDDEN => {
return Ok(Err(Error::msg("Blocked for security reasons!")))
}
StatusCode::NOT_FOUND => return Ok(Err(Error::msg("Unknown username!"))),
StatusCode::FAILED_DEPENDENCY => {
return Ok(Err(Error::msg("Account not verified!")))
}
_ => return Ok(Err(Error::msg("Unknown error"))),
}
let response_body: AuthResponse = response.json().await?;
response_body.token
};
Api::from_token(base_url, token).await
}
pub async fn from_token(base_url: &str, token: String) -> Result<Result<Self>> {
let id = {
let request = format!("{base_url}/account/id");
let response = Client::new()
.post(request)
.bearer_auth(&token)
.send()
.await?;
match response.status() {
StatusCode::OK => (),
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
StatusCode::UNAUTHORIZED => return Ok(Err(Error::msg("Invalid token!"))),
StatusCode::FORBIDDEN => {
return Ok(Err(Error::msg("Blocked for security reasons!")))
}
_ => return Ok(Err(Error::msg("Unknown error"))),
}
let response_body: UserIdResponse = response.json().await?;
response_body.id
};
{
let request = format!("{base_url}/user/info");
let response = Client::new()
.get(request)
.query(&[("id", id)])
.send()
.await?;
match response.status() {
StatusCode::OK => (),
StatusCode::BAD_REQUEST => return Ok(Err(Error::msg("400 - Bad Request"))),
StatusCode::FORBIDDEN => {
return Ok(Err(Error::msg("Blocked for security reasons!")))
}
StatusCode::NOT_FOUND => return Ok(Err(Error::msg("User not found"))),
_ => return Ok(Err(Error::msg("Unknown error"))),
}
let response_body: UserInfoResponse = response.json().await?;
if !response_body.is_admin {
return Ok(Err(Error::msg("You're not an admin!")));
}
}
Ok(Ok(Self {
token,
base_url: base_url.to_string(),
}))
}
}

View File

@ -1,15 +1,32 @@
mod ui; mod api;
mod state; mod state;
mod ui;
use anyhow::Result; use anyhow::Result;
use clap::Parser;
/// A tui for administrating nerdcult.net
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The base URL of the API
#[arg(short, long, default_value = "https://api.nerdcult.net")]
base_url: Option<String>,
/// An access token for the API
#[arg(short, long)]
token: Option<String>,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let args = Args::parse();
let stdout = ui::prepare()?; let stdout = ui::prepare()?;
let base_url = args.base_url.unwrap();
let mut login = ui::login::UI::new(stdout)?; let mut login = ui::login::UI::new(stdout, base_url)?;
login.run().await?; let api = login.run().await?;
Ok(()) Ok(())
} }

View File

@ -1,19 +1,20 @@
use std::io::Stdout; use crate::api::Api;
use anyhow::Result; use anyhow::Result;
use tui::{
backend::CrosstermBackend,
widgets::{Block, Borders, Paragraph},
style::{Color, Modifier, Style},
text::Span,
layout::{Constraint, Direction, Layout, Alignment},
Terminal,
};
use tui_textarea::TextArea;
use crossterm::{ use crossterm::{
event::{Event, KeyEvent, KeyCode, DisableMouseCapture}, event::{DisableMouseCapture, Event, KeyCode, KeyEvent},
execute, execute,
terminal::{disable_raw_mode, LeaveAlternateScreen}, terminal::{disable_raw_mode, LeaveAlternateScreen},
}; };
use std::io::Stdout;
use tui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
Terminal,
};
use tui_textarea::TextArea;
trait TextAreaExt { trait TextAreaExt {
fn disable(&mut self); fn disable(&mut self);
@ -52,6 +53,8 @@ pub struct UI<'a> {
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
position: InputPosition, position: InputPosition,
base_url: String,
status: String,
username: TextArea<'a>, username: TextArea<'a>,
password: TextArea<'a>, password: TextArea<'a>,
@ -73,7 +76,7 @@ impl Drop for UI<'_> {
} }
impl UI<'_> { impl UI<'_> {
pub fn new(stdout: Stdout) -> Result<Self> { pub fn new(stdout: Stdout, base_url: String) -> Result<Self> {
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?; let terminal = Terminal::new(backend)?;
@ -90,14 +93,17 @@ impl UI<'_> {
Ok(Self { Ok(Self {
terminal, terminal,
position: InputPosition::Username, position: InputPosition::Username,
base_url,
status: "".to_string(),
username, username,
password, password,
password_data password_data,
}) })
} }
pub async fn run(&mut self) -> Result<String> { pub async fn run(&mut self) -> Result<Api> {
loop { loop {
self.draw().await?;
let event = crossterm::event::read()?; let event = crossterm::event::read()?;
match event { match event {
@ -114,15 +120,18 @@ impl UI<'_> {
Event::Key(KeyEvent { Event::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
}) => { }) => match self.position {
match self.position {
InputPosition::Ok => { InputPosition::Ok => {
let username = self.username.lines()[0].clone(); let username = self.username.lines()[0].clone();
let password = self.password_data.lines()[0].clone(); let password = self.password_data.lines()[0].clone();
let api = Api::from_creds(&self.base_url, username, password).await?;
match api {
Ok(a) => return Ok(a),
Err(e) => self.status = e.to_string(),
}
} }
_ => self.cycle(), _ => self.cycle(),
} },
}
input => match self.position { input => match self.position {
InputPosition::Username => { InputPosition::Username => {
self.username.input(input.to_owned()); self.username.input(input.to_owned());
@ -143,12 +152,10 @@ impl UI<'_> {
} }
} }
} }
_ => () _ => (),
},
} }
} }
self.draw().await?;
}
} }
fn cycle(&mut self) { fn cycle(&mut self) {
@ -174,7 +181,7 @@ impl UI<'_> {
async fn draw(&mut self) -> Result<()> { async fn draw(&mut self) -> Result<()> {
// define a 32 * 6 chunk in the middle of the screen // define a 32 * 6 chunk in the middle of the screen
let mut chunk = self.terminal.size()?; let mut chunk = self.terminal.size()?;
chunk.x = (chunk.width / 2) - 24; chunk.x = (chunk.width / 2) - 14;
chunk.y = (chunk.height / 2) - 5; chunk.y = (chunk.height / 2) - 5;
chunk.height = 9; chunk.height = 9;
chunk.width = 28; chunk.width = 28;
@ -185,6 +192,10 @@ impl UI<'_> {
inner_chunk.height -= 1; inner_chunk.height -= 1;
inner_chunk.width -= 2; inner_chunk.width -= 2;
let mut status_chunk = self.terminal.size()?;
status_chunk.y = chunk.y - 1;
status_chunk.height = 1;
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
@ -204,11 +215,15 @@ impl UI<'_> {
_ => Span::styled("OK", Style::default().fg(Color::DarkGray)), _ => Span::styled("OK", Style::default().fg(Color::DarkGray)),
}; };
let content_status = Span::styled(self.status.clone(), Style::default().fg(Color::Red));
let block = Block::default().title("Login").borders(Borders::ALL); let block = Block::default().title("Login").borders(Borders::ALL);
let ok = Paragraph::new(content_ok).alignment(Alignment::Center); let ok = Paragraph::new(content_ok).alignment(Alignment::Center);
let status = Paragraph::new(content_status).alignment(Alignment::Center);
self.terminal.draw(|f| { self.terminal.draw(|f| {
f.render_widget(status, status_chunk);
f.render_widget(block, chunk); f.render_widget(block, chunk);
f.render_widget(self.username.widget(), chunks[0]); f.render_widget(self.username.widget(), chunks[0]);
f.render_widget(self.password.widget(), chunks[1]); f.render_widget(self.password.widget(), chunks[1]);