summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/error.rs48
-rw-r--r--src/lib.rs26
-rw-r--r--src/main.rs371
-rw-r--r--src/notification.rs20
-rw-r--r--src/routes/error.rs40
-rw-r--r--src/routes/home.rs21
-rw-r--r--src/routes/login.rs82
-rw-r--r--src/routes/logout.rs11
-rw-r--r--src/routes/mod.rs7
-rw-r--r--src/routes/signup.rs79
-rw-r--r--src/routes/static.rs22
-rw-r--r--src/routes/users.rs16
-rw-r--r--src/users.rs13
13 files changed, 402 insertions, 354 deletions
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..e4999c8
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,48 @@
+use actix_session::{SessionGetError, SessionInsertError};
+use actix_web::{body::BoxBody, HttpResponse, ResponseError};
+
+#[derive(Debug)]
+pub enum PinussyError {
+ Database(sqlx::Error),
+ SessionInsertError,
+ SessionGetError,
+ Bcrypt,
+}
+
+impl From<SessionInsertError> for PinussyError {
+ fn from(_: SessionInsertError) -> Self {
+ Self::SessionInsertError
+ }
+}
+
+impl From<SessionGetError> for PinussyError {
+ fn from(_: SessionGetError) -> Self {
+ Self::SessionGetError
+ }
+}
+
+impl From<sqlx::Error> for PinussyError {
+ fn from(e: sqlx::Error) -> Self {
+ Self::Database(e)
+ }
+}
+
+impl From<bcrypt::BcryptError> for PinussyError {
+ fn from(_: bcrypt::BcryptError) -> Self {
+ Self::Bcrypt
+ }
+}
+
+impl std::fmt::Display for PinussyError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for PinussyError {}
+
+impl ResponseError for PinussyError {
+ fn error_response(&self) -> HttpResponse<BoxBody> {
+ HttpResponse::new(self.status_code())
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..591d0c6
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,26 @@
+#[macro_use]
+mod actix_ructe;
+
+mod error;
+mod notification;
+pub mod routes;
+mod users;
+
+use sqlx::{Pool, Postgres};
+
+type Result<T> = std::result::Result<T, crate::error::PinussyError>;
+
+#[derive(Clone)]
+pub struct Pinussy {
+ pub db: Pool<Postgres>,
+}
+
+#[derive(sqlx::Type)]
+#[sqlx(type_name = "privacy", rename_all = "lowercase")]
+pub enum Privacy {
+ Private,
+ Unlisted,
+ Public,
+}
+
+include!(concat!(env!("OUT_DIR"), "/templates.rs"));
diff --git a/src/main.rs b/src/main.rs
index b10d0cf..6a439ec 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,33 +1,13 @@
-#[macro_use]
-mod actix_ructe;
-
-use std::time::{Duration, SystemTime};
-
use actix_session::storage::CookieSessionStore;
-use actix_session::{Session, SessionGetError, SessionInsertError, SessionMiddleware};
-use actix_web::body::{BoxBody, EitherBody, MessageBody};
-use actix_web::cookie::time::Error;
+use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
-use actix_web::dev::ServiceResponse;
-use actix_web::http::header::LOCATION;
-use actix_web::http::{header, StatusCode};
-use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
-use actix_web::web::Redirect;
-use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, ResponseError};
-use bcrypt::{hash, verify, DEFAULT_COST};
-use serde::Deserialize;
-use sqlx::postgres::PgDatabaseError;
-use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
-use templates::statics::StaticFile;
-
-static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60);
-
-type Result<T> = std::result::Result<T, PinussyError>;
+use actix_web::http::StatusCode;
+use actix_web::middleware::ErrorHandlers;
+use actix_web::{web, App, HttpServer};
+use sqlx::postgres::PgPoolOptions;
-#[derive(Clone)]
-struct Pinussy {
- db: Pool<Postgres>,
-}
+use pinussy::routes;
+use pinussy::Pinussy;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@@ -50,19 +30,19 @@ async fn main() -> std::io::Result<()> {
))
.wrap(
ErrorHandlers::new()
- .handler(StatusCode::NOT_FOUND, render_404)
- .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
+ .handler(StatusCode::NOT_FOUND, routes::error::render_404)
+ .handler(StatusCode::INTERNAL_SERVER_ERROR, routes::error::render_500),
)
.app_data(web::Data::new(pinussy.clone()))
- .service(web::resource("/static/{filename}").to(static_file))
- .service(home)
+ .service(web::resource("/static/{filename}").to(routes::r#static::file))
+ .service(routes::home::get)
// .service(home_auth)
- .service(get_login)
- .service(post_login)
- .service(post_logout)
- .service(get_signup)
- .service(post_signup)
- .service(get_users)
+ .service(routes::login::get)
+ .service(routes::login::post)
+ .service(routes::logout::post)
+ .service(routes::signup::get)
+ .service(routes::signup::post)
+ .service(routes::users::get)
// .service(get_pins)
// .service(post_pin)
// .service(get_pin)
@@ -73,320 +53,3 @@ async fn main() -> std::io::Result<()> {
.run()
.await
}
-
-#[get("/")]
-async fn home(session: Session, state: web::Data<Pinussy>) -> Result<HttpResponse> {
- let username: Option<String>;
- if let Some(user_id) = session.get::<i32>("user_id")? {
- username = Some(
- sqlx::query!("select username from users where id = $1", user_id)
- .fetch_one(&state.db)
- .await?
- .username,
- )
- } else {
- username = None
- }
- return Ok(HttpResponse::Ok().body(render!(templates::home_html, username).unwrap()));
-}
-
-#[get("/signup")]
-async fn get_signup() -> HttpResponse {
- HttpResponse::Ok().body(render!(templates::signup_html, None).unwrap())
-}
-
-#[derive(Deserialize)]
-struct SignupForm {
- username: String,
- password: String,
-}
-
-#[post("/signup")]
-async fn post_signup(
- state: web::Data<Pinussy>,
- form: web::Form<SignupForm>,
-) -> Result<HttpResponse> {
- let password_hash = hash(&form.password, DEFAULT_COST)?;
- match sqlx::query!(
- "insert into users(username, password) values ($1, $2)",
- &form.username,
- password_hash
- )
- .execute(&state.db)
- .await
- {
- Ok(_) => {
- return Ok(HttpResponse::Ok().body(
- render!(
- templates::signup_html,
- Some(Notification {
- kind: NotificationKind::Info,
- message: format!("you have successfully registered as {}", &form.username)
- })
- )
- .unwrap(),
- ))
- }
- Err(e) => {
- match e {
- sqlx::Error::Database(e) => {
- if e.is_unique_violation() {
- return Ok(HttpResponse::Conflict().body(
- render!(
- templates::signup_html,
- Some(Notification {
- kind: NotificationKind::Error,
- message: format!(
- "error: the username \"{}\" already exists",
- &form.username
- )
- })
- )
- .unwrap(),
- ));
- }
- }
- // TODO: log error
- _ => {}
- }
- return Ok(HttpResponse::InternalServerError().body(
- render!(
- templates::signup_html,
- Some(Notification {
- kind: NotificationKind::Error,
- message: "there was an internal server error. please try again later."
- .to_owned()
- })
- )
- .unwrap(),
- ));
- }
- };
-}
-
-#[get("/login")]
-async fn get_login() -> HttpResponse {
- HttpResponse::Ok().body(render!(templates::login_html, None).unwrap())
-}
-
-#[derive(Deserialize)]
-struct LoginForm {
- username: String,
- password: String,
- rememberme: Option<String>,
-}
-
-#[post("/login")]
-async fn post_login(
- state: web::Data<Pinussy>,
- session: Session,
- form: web::Form<SignupForm>,
-) -> Result<HttpResponse> {
- match sqlx::query!(
- "select id, password from users where username = $1",
- &form.username
- )
- .fetch_one(&state.db)
- .await
- {
- Ok(user) => {
- let password_hash: String = user.password;
- if verify(&form.password, &password_hash)? {
- session.insert("user_id", user.id)?;
- return Ok(HttpResponse::SeeOther()
- .insert_header((LOCATION, "/"))
- .finish());
- } else {
- return Ok(HttpResponse::Unauthorized().body(
- render!(
- templates::login_html,
- Some(Notification {
- kind: NotificationKind::Error,
- message: "that password is incorrect".to_owned()
- })
- )
- .unwrap(),
- ));
- }
- }
- Err(sqlx::Error::RowNotFound) => {
- return Ok(HttpResponse::NotFound().body(
- render!(
- templates::login_html,
- Some(Notification {
- kind: NotificationKind::Error,
- message: format!("the user \"{}\" does not exist", &form.username)
- })
- )
- .unwrap(),
- ));
- }
- Err(_) => {
- return Ok(HttpResponse::InternalServerError().body(
- render!(
- templates::login_html,
- Some(Notification {
- kind: NotificationKind::Error,
- message: "internal server error. please try again later".to_owned()
- })
- )
- .unwrap(),
- ));
- }
- }
-}
-
-#[post("/logout")]
-async fn post_logout(session: Session) -> HttpResponse {
- session.purge();
- HttpResponse::SeeOther()
- .insert_header((LOCATION, "/"))
- .finish()
-}
-
-#[derive(sqlx::Type)]
-#[sqlx(type_name = "privacy", rename_all = "lowercase")]
-enum Privacy {
- Private,
- Unlisted,
- Public,
-}
-
-#[derive(sqlx::FromRow)]
-pub struct User {
- id: i32,
- username: String,
- password: String,
- email: Option<String>,
- bio: Option<String>,
- site: Option<String>,
- privacy: Privacy,
- admin: bool,
-}
-
-pub enum NotificationKind {
- Info,
- Warning,
- Error,
-}
-
-impl std::fmt::Display for NotificationKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- NotificationKind::Info => f.write_str("info"),
- NotificationKind::Warning => f.write_str("warning"),
- NotificationKind::Error => f.write_str("error"),
- }
- }
-}
-
-pub struct Notification {
- kind: NotificationKind,
- message: String,
-}
-
-#[get("/users")]
-async fn get_users(state: web::Data<Pinussy>) -> Result<HttpResponse> {
- let users: Vec<User> = sqlx::query_as("select * from users")
- .fetch_all(&state.db)
- .await
- // TODO: no unwrap
- .unwrap();
- println!("lol");
- Ok(HttpResponse::Ok().body(render!(templates::users_html, users).unwrap()))
-}
-
-async fn static_file(path: web::Path<String>) -> HttpResponse {
- let name = &path.into_inner();
- if let Some(data) = StaticFile::get(name) {
- let far_expires = SystemTime::now() + FAR;
- HttpResponse::Ok()
- .insert_header(header::Expires(far_expires.into()))
- .insert_header(header::ContentType(data.mime.clone()))
- .body(data.content)
- } else {
- HttpResponse::NotFound()
- .reason("No such static file.")
- .finish()
- }
-}
-
-fn render_404(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
- Ok(error_response(
- res,
- StatusCode::NOT_FOUND,
- "The resource you requested can't be found.",
- ))
-}
-
-fn render_500(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
- Ok(error_response(
- res,
- StatusCode::INTERNAL_SERVER_ERROR,
- "Sorry, Something went wrong. This is probably not your fault.",
- ))
-}
-
-fn error_response(
- mut res: ServiceResponse,
- status_code: StatusCode,
- message: &str,
-) -> ErrorHandlerResponse<BoxBody> {
- res.headers_mut().insert(
- header::CONTENT_TYPE,
- header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
- );
- ErrorHandlerResponse::Response(res.map_body(|_head, _body| {
- EitherBody::right(MessageBody::boxed(
- render!(templates::error_html, status_code, message).unwrap(),
- ))
- }))
-}
-
-#[derive(Debug)]
-enum PinussyError {
- Database(sqlx::Error),
- SessionInsertError,
- SessionGetError,
- Bcrypt,
-}
-
-impl From<SessionInsertError> for PinussyError {
- fn from(_: SessionInsertError) -> Self {
- Self::SessionInsertError
- }
-}
-
-impl From<SessionGetError> for PinussyError {
- fn from(_: SessionGetError) -> Self {
- Self::SessionGetError
- }
-}
-
-impl From<sqlx::Error> for PinussyError {
- fn from(e: sqlx::Error) -> Self {
- Self::Database(e)
- }
-}
-
-impl From<bcrypt::BcryptError> for PinussyError {
- fn from(_: bcrypt::BcryptError) -> Self {
- Self::Bcrypt
- }
-}
-
-impl std::fmt::Display for PinussyError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{:?}", self)
- }
-}
-
-impl std::error::Error for PinussyError {}
-
-impl ResponseError for PinussyError {
- fn error_response(&self) -> HttpResponse<BoxBody> {
- HttpResponse::new(self.status_code())
- }
-}
-
-include!(concat!(env!("OUT_DIR"), "/templates.rs"));
diff --git a/src/notification.rs b/src/notification.rs
new file mode 100644
index 0000000..2283761
--- /dev/null
+++ b/src/notification.rs
@@ -0,0 +1,20 @@
+pub enum Kind {
+ Info,
+ Warning,
+ Error,
+}
+
+impl std::fmt::Display for Kind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Kind::Info => f.write_str("info"),
+ Kind::Warning => f.write_str("warning"),
+ Kind::Error => f.write_str("error"),
+ }
+ }
+}
+
+pub struct Notification {
+ pub kind: Kind,
+ pub message: String,
+}
diff --git a/src/routes/error.rs b/src/routes/error.rs
new file mode 100644
index 0000000..543b8a8
--- /dev/null
+++ b/src/routes/error.rs
@@ -0,0 +1,40 @@
+use actix_web::{
+ body::{BoxBody, EitherBody, MessageBody},
+ dev::ServiceResponse,
+ http::{header, StatusCode},
+ middleware::ErrorHandlerResponse,
+};
+
+use crate::templates;
+
+pub fn render_404(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
+ Ok(error_response(
+ res,
+ StatusCode::NOT_FOUND,
+ "The resource you requested can't be found.",
+ ))
+}
+
+pub fn render_500(res: ServiceResponse) -> actix_web::Result<ErrorHandlerResponse<BoxBody>> {
+ Ok(error_response(
+ res,
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "Sorry, Something went wrong. This is probably not your fault.",
+ ))
+}
+
+fn error_response(
+ mut res: ServiceResponse,
+ status_code: StatusCode,
+ message: &str,
+) -> ErrorHandlerResponse<BoxBody> {
+ res.headers_mut().insert(
+ header::CONTENT_TYPE,
+ header::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
+ );
+ ErrorHandlerResponse::Response(res.map_body(|_head, _body| {
+ EitherBody::right(MessageBody::boxed(
+ render!(templates::error_html, status_code, message).unwrap(),
+ ))
+ }))
+}
diff --git a/src/routes/home.rs b/src/routes/home.rs
new file mode 100644
index 0000000..11d3a72
--- /dev/null
+++ b/src/routes/home.rs
@@ -0,0 +1,21 @@
+use actix_session::Session;
+use actix_web::{get, web, HttpResponse};
+
+use crate::templates;
+use crate::{Pinussy, Result};
+
+#[get("/")]
+async fn get(session: Session, state: web::Data<Pinussy>) -> Result<HttpResponse> {
+ let username: Option<String>;
+ if let Some(user_id) = session.get::<i32>("user_id")? {
+ username = Some(
+ sqlx::query!("select username from users where id = $1", user_id)
+ .fetch_one(&state.db)
+ .await?
+ .username,
+ )
+ } else {
+ username = None
+ }
+ return Ok(HttpResponse::Ok().body(render!(templates::home_html, username).unwrap()));
+}
diff --git a/src/routes/login.rs b/src/routes/login.rs
new file mode 100644
index 0000000..33f7f69
--- /dev/null
+++ b/src/routes/login.rs
@@ -0,0 +1,82 @@
+use actix_session::Session;
+use actix_web::http::header::LOCATION;
+use actix_web::{get, post, web, HttpResponse};
+use bcrypt::verify;
+use serde::Deserialize;
+
+use crate::notification::{Kind, Notification};
+use crate::templates;
+use crate::Pinussy;
+use crate::Result;
+
+#[get("/login")]
+async fn get() -> HttpResponse {
+ HttpResponse::Ok().body(render!(templates::login_html, None).unwrap())
+}
+
+#[derive(Deserialize)]
+struct LoginForm {
+ username: String,
+ password: String,
+ rememberme: Option<String>,
+}
+
+#[post("/login")]
+async fn post(
+ state: web::Data<Pinussy>,
+ session: Session,
+ form: web::Form<LoginForm>,
+) -> Result<HttpResponse> {
+ match sqlx::query!(
+ "select id, password from users where username = $1",
+ &form.username
+ )
+ .fetch_one(&state.db)
+ .await
+ {
+ Ok(user) => {
+ let password_hash: String = user.password;
+ if verify(&form.password, &password_hash)? {
+ session.insert("user_id", user.id)?;
+ return Ok(HttpResponse::SeeOther()
+ .insert_header((LOCATION, "/"))
+ .finish());
+ } else {
+ return Ok(HttpResponse::Unauthorized().body(
+ render!(
+ templates::login_html,
+ Some(Notification {
+ kind: Kind::Error,
+ message: "that password is incorrect".to_owned()
+ })
+ )
+ .unwrap(),
+ ));
+ }
+ }
+ Err(sqlx::Error::RowNotFound) => {
+ return Ok(HttpResponse::NotFound().body(
+ render!(
+ templates::login_html,
+ Some(Notification {
+ kind: Kind::Error,
+ message: format!("the user \"{}\" does not exist", &form.username)
+ })
+ )
+ .unwrap(),
+ ));
+ }
+ Err(_) => {
+ return Ok(HttpResponse::InternalServerError().body(
+ render!(
+ templates::login_html,
+ Some(Notification {
+ kind: Kind::Error,
+ message: "internal server error. please try again later".to_owned()
+ })
+ )
+ .unwrap(),
+ ));
+ }
+ }
+}
diff --git a/src/routes/logout.rs b/src/routes/logout.rs
new file mode 100644
index 0000000..712b74c
--- /dev/null
+++ b/src/routes/logout.rs
@@ -0,0 +1,11 @@
+use actix_session::Session;
+use actix_web::http::header::LOCATION;
+use actix_web::{post, HttpResponse};
+
+#[post("/logout")]
+async fn post(session: Session) -> HttpResponse {
+ session.purge();
+ HttpResponse::SeeOther()
+ .insert_header((LOCATION, "/"))
+ .finish()
+}
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
new file mode 100644
index 0000000..3b73f38
--- /dev/null
+++ b/src/routes/mod.rs
@@ -0,0 +1,7 @@
+pub mod error;
+pub mod home;
+pub mod login;
+pub mod logout;
+pub mod signup;
+pub mod r#static;
+pub mod users;
diff --git a/src/routes/signup.rs b/src/routes/signup.rs
new file mode 100644
index 0000000..ae10201
--- /dev/null
+++ b/src/routes/signup.rs
@@ -0,0 +1,79 @@
+use actix_web::{get, post, web, HttpResponse};
+use serde::Deserialize;
+
+use crate::notification::{Kind as NotificationKind, Notification};
+use crate::templates;
+use crate::Pinussy;
+use crate::Result;
+use bcrypt::{hash, DEFAULT_COST};
+
+#[get("/signup")]
+async fn get() -> HttpResponse {
+ HttpResponse::Ok().body(render!(templates::signup_html, None).unwrap())
+}
+
+#[derive(Deserialize)]
+struct SignupForm {
+ username: String,
+ password: String,
+}
+
+#[post("/signup")]
+async fn post(state: web::Data<Pinussy>, form: web::Form<SignupForm>) -> Result<HttpResponse> {
+ let password_hash = hash(&form.password, DEFAULT_COST)?;
+ match sqlx::query!(
+ "insert into users(username, password) values ($1, $2)",
+ &form.username,
+ password_hash
+ )
+ .execute(&state.db)
+ .await
+ {
+ Ok(_) => {
+ return Ok(HttpResponse::Ok().body(
+ render!(
+ templates::signup_html,
+ Some(Notification {
+ kind: NotificationKind::Info,
+ message: format!("you have successfully registered as {}", &form.username)
+ })
+ )
+ .unwrap(),
+ ))
+ }
+ Err(e) => {
+ match e {
+ sqlx::Error::Database(e) => {
+ if e.is_unique_violation() {
+ return Ok(HttpResponse::Conflict().body(
+ render!(
+ templates::signup_html,
+ Some(Notification {
+ kind: NotificationKind::Error,
+ message: format!(
+ "error: the username \"{}\" already exists",
+ &form.username
+ )
+ })
+ )
+ .unwrap(),
+ ));
+ }
+ }
+ // TODO: log error
+ _ => {}
+ }
+ return Ok(HttpResponse::InternalServerError().body(
+ render!(
+ templates::signup_html,
+ Some(Notification {
+ kind: NotificationKind::Error,
+ message: "there was an internal server error. please try again later."
+ .to_owned()
+ })
+ )
+ .unwrap(),
+ ));
+ }
+ };
+}
diff --git a/src/routes/static.rs b/src/routes/static.rs
new file mode 100644
index 0000000..cabc6f8
--- /dev/null
+++ b/src/routes/static.rs
@@ -0,0 +1,22 @@
+use std::time::{Duration, SystemTime};
+
+use actix_web::{http::header, web, HttpResponse};
+
+use crate::templates::statics::StaticFile;
+
+static FAR: Duration = Duration::from_secs(180 * 24 * 60 * 60);
+
+pub async fn file(path: web::Path<String>) -> HttpResponse {
+ let name = &path.into_inner();
+ if let Some(data) = StaticFile::get(name) {
+ let far_expires = SystemTime::now() + FAR;
+ HttpResponse::Ok()
+ .insert_header(header::Expires(far_expires.into()))
+ .insert_header(header::ContentType(data.mime.clone()))
+ .body(data.content)
+ } else {
+ HttpResponse::NotFound()
+ .reason("No such static file.")
+ .finish()
+ }
+}
diff --git a/src/routes/users.rs b/src/routes/users.rs
new file mode 100644
index 0000000..2ad9ede
--- /dev/null
+++ b/src/routes/users.rs
@@ -0,0 +1,16 @@
+use actix_web::{get, web, HttpResponse};
+
+use crate::templates;
+use crate::users::User;
+use crate::{Pinussy, Result};
+
+#[get("/users")]
+async fn get(state: web::Data<Pinussy>) -> Result<HttpResponse> {
+ let users: Vec<User> = sqlx::query_as("select * from users")
+ .fetch_all(&state.db)
+ .await
+ // TODO: no unwrap
+ .unwrap();
+ println!("lol");
+ Ok(HttpResponse::Ok().body(render!(templates::users_html, users).unwrap()))
+}
diff --git a/src/users.rs b/src/users.rs
new file mode 100644
index 0000000..bf41fd5
--- /dev/null
+++ b/src/users.rs
@@ -0,0 +1,13 @@
+use crate::Privacy;
+
+#[derive(sqlx::FromRow)]
+pub struct User {
+ pub id: i32,
+ pub username: String,
+ pub password: String,
+ pub email: Option<String>,
+ pub bio: Option<String>,
+ pub site: Option<String>,
+ pub privacy: Privacy,
+ pub admin: bool,
+}