feat(ui,backend): implemented login - UNTESTED!
This commit is contained in:
parent
6c855b24f7
commit
ba25f98d58
File diff suppressed because it is too large
Load Diff
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
23
src/main.rs
23
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
||||||
|
@ -87,17 +90,20 @@ impl UI<'_> {
|
||||||
username.enable();
|
username.enable();
|
||||||
password.disable();
|
password.disable();
|
||||||
|
|
||||||
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,11 +152,9 @@ impl UI<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => ()
|
_ => (),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
self.draw().await?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -193,7 +204,7 @@ impl UI<'_> {
|
||||||
Constraint::Length(3), // 2. Password:
|
Constraint::Length(3), // 2. Password:
|
||||||
Constraint::Length(1), // 3. OK
|
Constraint::Length(1), // 3. OK
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(inner_chunk);
|
.split(inner_chunk);
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
|
|
Loading…
Reference in New Issue