mirror of https://github.com/realaravinth/gitpad
feat: bootstarp gists
AUTHENTICATION - Sign Up - Sign IN ACCOUNT - Username Exists - Email Exists - Account delete - Password update - Email update - Username update - Get account secret - Update secret All routes are implemented with proper error handling and testing CONFIGURATION See ./config/default.toml for full listmaster
parent
3447a3a45c
commit
34a67a5535
@ -0,0 +1,36 @@
|
||||
log = "info" # possible values: "info", "warn", "trace", "error", "debug"
|
||||
source_code = "https://github.com/realaravinth/gists"
|
||||
allow_registration = true # allow registration on server
|
||||
allow_demo = true # allow demo on server
|
||||
|
||||
[server]
|
||||
# The port at which you want authentication to listen to
|
||||
# takes a number, choose from 1000-10000 if you dont know what you are doing
|
||||
port = 7000
|
||||
#IP address. Enter 0.0.0.0 to listen on all availale addresses
|
||||
ip= "0.0.0.0"
|
||||
# enter your hostname, eg: example.com
|
||||
domain = "localhost"
|
||||
proxy_has_tls = false
|
||||
cookie_secret = "k&y8G#J&2gesW&N6hNauy63vgRzq9ZLPb39"
|
||||
#workers = 2
|
||||
|
||||
[database]
|
||||
# This section deals with the database location and how to access it
|
||||
# Please note that at the moment, we have support for only postgresqa.
|
||||
# Example, if you are Batman, your config would be:
|
||||
# hostname = "batcave.org"
|
||||
# port = "5432"
|
||||
# username = "batman"
|
||||
# password = "somereallycomplicatedBatmanpassword"
|
||||
hostname = "localhost"
|
||||
port = "5432"
|
||||
username = "postgres"
|
||||
password = "password"
|
||||
name = "postgres"
|
||||
pool = 4
|
||||
database_type = "postgres"
|
||||
|
||||
|
||||
[repository]
|
||||
root = "/tmp/gists.batsense.net"
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
pub mod v1;
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::data::api::v1::account::*;
|
||||
use crate::data::api::v1::auth::Password;
|
||||
use crate::errors::*;
|
||||
use crate::AppData;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
pub use super::auth;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AccountCheckPayload {
|
||||
pub val: String,
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(username_exists);
|
||||
cfg.service(set_username);
|
||||
cfg.service(email_exists);
|
||||
cfg.service(set_email);
|
||||
cfg.service(delete_account);
|
||||
cfg.service(update_user_password);
|
||||
cfg.service(get_secret);
|
||||
cfg.service(update_user_secret);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Email {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Username {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// update username
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_username",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn set_username(
|
||||
id: Identity,
|
||||
payload: web::Json<Username>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
|
||||
let new_name = data
|
||||
.update_username(&(**db), &username, &payload.username)
|
||||
.await?;
|
||||
|
||||
id.forget();
|
||||
id.remember(new_name);
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.username_exists")]
|
||||
async fn username_exists(
|
||||
payload: web::Json<AccountCheckPayload>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
Ok(HttpResponse::Ok().json(data.username_exists(&(**db), &payload.val).await?))
|
||||
}
|
||||
|
||||
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.email_exists")]
|
||||
pub async fn email_exists(
|
||||
payload: web::Json<AccountCheckPayload>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
Ok(HttpResponse::Ok().json(data.email_exists(&(**db), &payload.val).await?))
|
||||
}
|
||||
|
||||
/// update email
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_email",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn set_email(
|
||||
id: Identity,
|
||||
payload: web::Json<Email>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
data.set_email(&(**db), &username, &payload.email).await?;
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.account.delete",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn delete_account(
|
||||
id: Identity,
|
||||
payload: web::Json<Password>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
|
||||
data.delete_user(&(**db), &username, &payload.password)
|
||||
.await?;
|
||||
id.forget();
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_password",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn update_user_password(
|
||||
id: Identity,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
payload: web::Json<ChangePasswordReqest>,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
let payload = payload.into_inner();
|
||||
data.change_password(&(**db), &username, &payload).await?;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[my_codegen::get(
|
||||
path = "crate::V1_API_ROUTES.account.get_secret",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn get_secret(id: Identity, data: AppData, db: crate::DB) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
let secret = data.get_secret(&(**db), &username).await?;
|
||||
Ok(HttpResponse::Ok().json(secret))
|
||||
}
|
||||
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_secret",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn update_user_secret(
|
||||
id: Identity,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
let _secret = data.update_user_secret(&(**db), &username).await?;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use super::*;
|
||||
use crate::api::v1::ROUTES;
|
||||
use crate::data::api::v1::account::*;
|
||||
use crate::data::api::v1::auth::Password;
|
||||
use crate::data::Data;
|
||||
use crate::errors::*;
|
||||
use crate::*;
|
||||
|
||||
use crate::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_account_works() {
|
||||
let (db, data) = sqlx_postgres::get_data().await;
|
||||
uname_email_exists_works(data.clone(), db.clone()).await;
|
||||
email_udpate_password_validation_del_userworks(data.clone(), db.clone()).await;
|
||||
username_update_works(data.clone(), db.clone()).await;
|
||||
update_password_works(data.clone(), db.clone()).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn sqlite_account_works() {
|
||||
let (db, data) = sqlx_sqlite::get_data().await;
|
||||
uname_email_exists_works(data.clone(), db.clone()).await;
|
||||
email_udpate_password_validation_del_userworks(data.clone(), db.clone()).await;
|
||||
username_update_works(data.clone(), db.clone()).await;
|
||||
update_password_works(data.clone(), db.clone()).await;
|
||||
}
|
||||
|
||||
async fn uname_email_exists_works(data: Arc<Data>, db: BoxDB) {
|
||||
const NAME: &str = "testuserexists";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "testuserexists@a.com2";
|
||||
let db = &db;
|
||||
|
||||
let _ = data.delete_user(db, NAME, PASSWORD).await;
|
||||
|
||||
let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(data, db).await;
|
||||
|
||||
// chech if get user secret works
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.cookie(cookies.clone())
|
||||
.uri(ROUTES.account.get_secret)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// chech if get user secret works
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::post()
|
||||
.cookie(cookies.clone())
|
||||
.uri(ROUTES.account.update_secret)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let mut payload = AccountCheckPayload { val: NAME.into() };
|
||||
|
||||
let user_exists_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.username_exists)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(user_exists_resp.status(), StatusCode::OK);
|
||||
let mut resp: AccountCheckResp = test::read_body_json(user_exists_resp).await;
|
||||
assert!(resp.exists);
|
||||
|
||||
payload.val = PASSWORD.into();
|
||||
|
||||
let user_doesnt_exist = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.username_exists)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(user_doesnt_exist.status(), StatusCode::OK);
|
||||
resp = test::read_body_json(user_doesnt_exist).await;
|
||||
assert!(!resp.exists);
|
||||
|
||||
let email_doesnt_exist = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.email_exists)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(email_doesnt_exist.status(), StatusCode::OK);
|
||||
resp = test::read_body_json(email_doesnt_exist).await;
|
||||
assert!(!resp.exists);
|
||||
|
||||
payload.val = EMAIL.into();
|
||||
|
||||
let email_exist = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.email_exists)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(email_exist.status(), StatusCode::OK);
|
||||
resp = test::read_body_json(email_exist).await;
|
||||
assert!(resp.exists);
|
||||
}
|
||||
|
||||
async fn email_udpate_password_validation_del_userworks(data: Arc<Data>, db: BoxDB) {
|
||||
const NAME: &str = "testuser2";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "testuser1@a.com2";
|
||||
const NAME2: &str = "eupdauser";
|
||||
const EMAIL2: &str = "eupdauser@a.com";
|
||||
|
||||
let _ = data.delete_user(&db, NAME, PASSWORD).await;
|
||||
let _ = data.delete_user(&db, NAME2, PASSWORD).await;
|
||||
|
||||
let _ = data.register_and_signin(&db, NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = data.register_and_signin(&db, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(data, db).await;
|
||||
|
||||
// update email
|
||||
let mut email_payload = Email {
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
let email_update_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&email_payload, ROUTES.account.update_email)
|
||||
//post_request!(&email_payload, EMAIL_UPDATE)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(email_update_resp.status(), StatusCode::OK);
|
||||
|
||||
// check duplicate email while duplicate email
|
||||
email_payload.email = EMAIL2.into();
|
||||
data.bad_post_req_test(
|
||||
&db,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.account.update_email,
|
||||
&email_payload,
|
||||
ServiceError::EmailTaken,
|
||||
)
|
||||
.await;
|
||||
|
||||
// wrong password while deleteing account
|
||||
let mut payload = Password {
|
||||
password: NAME.into(),
|
||||
};
|
||||
data.bad_post_req_test(
|
||||
&db,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.account.delete,
|
||||
&payload,
|
||||
ServiceError::WrongPassword,
|
||||
)
|
||||
.await;
|
||||
|
||||
// delete account
|
||||
payload.password = PASSWORD.into();
|
||||
let delete_user_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.delete)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(delete_user_resp.status(), StatusCode::OK);
|
||||
|
||||
// try to delete an account that doesn't exist
|
||||
let account_not_found_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, ROUTES.account.delete)
|
||||
.cookie(cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND);
|
||||
let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await;
|
||||
assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound));
|
||||
}
|
||||
|
||||
async fn username_update_works(data: Arc<Data>, db: BoxDB) {
|
||||
const NAME: &str = "testuserupda";
|
||||
const EMAIL: &str = "testuserupda@sss.com";
|
||||
const EMAIL2: &str = "testuserupda2@sss.com";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const NAME2: &str = "terstusrtds";
|
||||
const NAME_CHANGE: &str = "terstusrtdsxx";
|
||||
|
||||
let db = &db;
|
||||
|
||||
let _ = futures::join!(
|
||||
data.delete_user(db, NAME, PASSWORD),
|
||||
data.delete_user(db, NAME2, PASSWORD),
|
||||
data.delete_user(db, NAME_CHANGE, PASSWORD)
|
||||
);
|
||||
|
||||
let _ = data.register_and_signin(db, NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(data, db).await;
|
||||
|
||||
// update username
|
||||
let mut username_udpate = Username {
|
||||
username: NAME_CHANGE.into(),
|
||||
};
|
||||
let username_update_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&username_udpate, ROUTES.account.update_username)
|
||||
.cookie(cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(username_update_resp.status(), StatusCode::OK);
|
||||
|
||||
// check duplicate username with duplicate username
|
||||
username_udpate.username = NAME2.into();
|
||||
data.bad_post_req_test(
|
||||
db,
|
||||
NAME_CHANGE,
|
||||
PASSWORD,
|
||||
ROUTES.account.update_username,
|
||||
&username_udpate,
|
||||
ServiceError::UsernameTaken,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn update_password_works(data: Arc<Data>, db: BoxDB) {
|
||||
const NAME: &str = "updatepassuser";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "updatepassuser@a.com";
|
||||
|
||||
let db = &db;
|
||||
|
||||
let _ = data.delete_user(db, NAME, PASSWORD).await;
|
||||
|
||||
let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(data, db).await;
|
||||
|
||||
let new_password = "newpassword";
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: PASSWORD.into(),
|
||||
new_password: new_password.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
let res = data.change_password(db, NAME, &update_password).await;
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: PASSWORD.into(),
|
||||
new_password: new_password.into(),
|
||||
confirm_new_password: new_password.into(),
|
||||
};
|
||||
|
||||
assert!(data
|
||||
.change_password(db, NAME, &update_password)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: new_password.into(),
|
||||
new_password: new_password.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
data.bad_post_req_test(
|
||||
db,
|
||||
NAME,
|
||||
new_password,
|
||||
ROUTES.account.update_password,
|
||||
&update_password,
|
||||
ServiceError::PasswordsDontMatch,
|
||||
)
|
||||
.await;
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: PASSWORD.into(),
|
||||
new_password: PASSWORD.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
data.bad_post_req_test(
|
||||
db,
|
||||
NAME,
|
||||
new_password,
|
||||
ROUTES.account.update_password,
|
||||
&update_password,
|
||||
ServiceError::WrongPassword,
|
||||
)
|
||||
.await;
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: new_password.into(),
|
||||
new_password: PASSWORD.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
let update_password_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&update_password, ROUTES.account.update_password)
|
||||
.cookie(cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(update_password_resp.status(), StatusCode::OK);
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::data::api::v1::auth::{Login, Register};
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
|
||||
use super::RedirectQuery;
|
||||
use crate::errors::*;
|
||||
use crate::AppData;
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(register);
|
||||
cfg.service(login);
|
||||
cfg.service(signout);
|
||||
}
|
||||
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
|
||||
async fn register(
|
||||
payload: web::Json<Register>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
data.register(&(**db), &payload).await?;
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")]
|
||||
async fn login(
|
||||
id: Identity,
|
||||
payload: web::Json<Login>,
|
||||
query: web::Query<RedirectQuery>,
|
||||
data: AppData,
|
||||
db: crate::DB,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let payload = payload.into_inner();
|
||||
let username = data.login(&(**db), &payload).await?;
|
||||
id.remember(username);
|
||||
let query = query.into_inner();
|
||||
if let Some(redirect_to) = query.redirect_to {
|
||||
Ok(HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[my_codegen::get(
|
||||
path = "crate::V1_API_ROUTES.auth.logout",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
async fn signout(id: Identity) -> impl Responder {
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
if id.identity().is_some() {
|
||||
id.forget();
|
||||
}
|
||||
HttpResponse::Found()
|
||||
.append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None)))
|
||||
.finish()
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{DB, GIT_COMMIT_HASH, VERSION};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BuildDetails {
|
||||
pub version: &'static str,
|
||||
pub git_commit_hash: &'static str,
|
||||
}
|
||||
|
||||
pub mod routes {
|
||||
pub struct Meta {
|
||||
pub build_details: &'static str,
|
||||
pub health: &'static str,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
build_details: "/api/v1/meta/build",
|
||||
health: "/api/v1/meta/health",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// emmits build details of the bninary
|
||||
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")]
|
||||
async fn build_details() -> impl Responder {
|
||||
let build = BuildDetails {
|
||||
version: VERSION,
|
||||
git_commit_hash: GIT_COMMIT_HASH,
|
||||
};
|
||||
HttpResponse::Ok().json(build)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
/// Health check return datatype
|
||||
pub struct Health {
|
||||
db: bool,
|
||||
}
|
||||
|
||||
/// emmits build details of the bninary
|
||||
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
|
||||
async fn health(db: DB) -> impl Responder {
|
||||
let health = Health {
|
||||
db: db.ping().await,
|
||||
};
|
||||
HttpResponse::Ok().json(health)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(build_details);
|
||||
cfg.service(health);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
|
||||
use crate::routes::services;
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn build_details_works() {
|
||||
let app = test::init_service(App::new().configure(services)).await;
|
||||
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.uri(V1_API_ROUTES.meta.build_details)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use actix_auth_middleware::Authentication;
|
||||
use actix_web::web::ServiceConfig;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod account;
|
||||
pub mod auth;
|
||||
pub mod meta;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::ROUTES;
|
||||
|
||||
pub fn services(cfg: &mut ServiceConfig) {
|
||||
auth::services(cfg);
|
||||
account::services(cfg);
|
||||
meta::services(cfg);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RedirectQuery {
|
||||
pub redirect_to: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_auth_middleware() -> Authentication<routes::Routes> {
|
||||
Authentication::with_identity(ROUTES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
//! V1 API Routes
|
||||
use actix_auth_middleware::{Authentication, GetLoginRoute};
|
||||
|
||||
use super::meta::routes::Meta;
|
||||
|
||||
/// constant [Routes](Routes) instance
|
||||
pub const ROUTES: Routes = Routes::new();
|
||||
|
||||
/// Authentication routes
|
||||
pub struct Auth {
|
||||
/// logout route
|
||||
pub logout: &'static str,
|
||||
/// login route
|
||||
pub login: &'static str,
|
||||
/// registration route
|
||||
pub register: &'static str,
|
||||
}
|
||||
impl Auth {
|
||||
/// create new instance of Authentication route
|
||||
pub const fn new() -> Auth {
|
||||
let login = "/api/v1/signin";
|
||||
let logout = "/logout";
|
||||
let register = "/api/v1/signup";
|
||||
Auth {
|
||||
logout,
|
||||
login,
|
||||
register,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Account management routes
|
||||
pub struct Account {
|
||||
/// delete account route
|
||||
pub delete: &'static str,
|
||||
/// route to check if an email exists
|
||||
pub email_exists: &'static str,
|
||||
/// route to fetch account secret
|
||||
pub get_secret: &'static str,
|
||||
/// route to update a user's email
|
||||
pub update_email: &'static str,
|
||||
/// route to update password
|
||||
pub update_password: &'static str,
|
||||
/// route to update secret
|
||||
pub update_secret: &'static str,
|
||||
/// route to check if a username is already registered
|
||||
pub username_exists: &'static str,
|
||||
/// route to change username
|
||||
pub update_username: &'static str,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// create a new instance of [Account][Account] routes
|
||||
pub const fn new() -> Account {
|
||||
let get_secret = "/api/v1/account/secret/get";
|
||||
let update_secret = "/api/v1/account/secret/update";
|
||||
let delete = "/api/v1/account/delete";
|
||||
let email_exists = "/api/v1/account/email/exists";
|
||||
let username_exists = "/api/v1/account/username/exists";
|
||||
let update_username = "/api/v1/account/username/update";
|
||||
let update_email = "/api/v1/account/email/update";
|
||||
let update_password = "/api/v1/account/password/update";
|
||||
Account {
|
||||
delete,
|
||||
email_exists,
|
||||
get_secret,
|
||||
update_email,
|
||||
update_password,
|
||||
update_secret,
|
||||
username_exists,
|
||||
update_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level routes data structure for V1 AP1
|
||||
pub struct Routes {
|
||||
/// Authentication routes
|
||||
pub auth: Auth,
|
||||
/// Account routes
|
||||
pub account: Account,
|
||||
|
||||
/// Meta routes
|
||||
pub meta: Meta,
|
||||
}
|
||||
|
||||
impl Routes {
|
||||
/// create new instance of Routes
|
||||
const fn new() -> Routes {
|
||||
Routes {
|
||||
auth: Auth::new(),
|
||||
account: Account::new(),
|
||||
meta: Meta::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_auth_middleware() -> Authentication<Routes> {
|
||||
Authentication::with_identity(ROUTES)
|
||||
}
|
||||
|
||||
impl GetLoginRoute for Routes {
|
||||
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||
if let Some(redirect_to) = src {
|
||||
format!(
|
||||
"{}?redirect_to={}",
|
||||
self.auth.login,
|
||||
urlencoding::encode(redirect_to)
|
||||
)
|
||||
} else {
|
||||
self.auth.register.to_string()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
use actix_web::http::{header, StatusCode};
|
||||
use actix_web::test;
|
||||
|
||||
use crate::api::v1::ROUTES;
|
||||
use crate::data::api::v1::auth::{Login, Register};
|
||||
use crate::data::Data;
|
||||
use crate::db::BoxDB;
|
||||
use crate::errors::*;
|
||||
use crate::*;
|
||||
|
||||
use crate::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_auth_works() {
|
||||
let (db, data) = sqlx_postgres::get_data().await;
|
||||
auth_works(data.clone(), db.clone()).await;
|
||||
serverside_password_validation_works(data, db).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn sqlite_auth_works() {
|
||||
let (db, data) = sqlx_sqlite::get_data().await;
|
||||
auth_works(data.clone(), db.clone()).await;
|
||||
serverside_password_validation_works(data, db).await;
|
||||
}
|
||||
|
||||
async fn auth_works(data: Arc<Data>, db: BoxDB) {
|
||||
const NAME: &str = "testuserfoo";
|
||||
const PASSWORD: &str = "longpassword";
|
||||
const EMAIL: &str = "testuser1foo@a.com";
|
||||
|
||||
let _ = data.delete_user(&db, NAME, PASSWORD).await;
|
||||
let app = get_app!(data, db).await;
|
||||
|
||||
// 1. Register with email == None
|
||||
let msg = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: None,
|
||||
};
|
||||
let resp =
|
||||
test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
// delete user TODO
|
||||
let _ = data.delete_user(&db, NAME, PASSWORD).await;
|
||||
|
||||
// 1. Register and signin
|
||||
let (_, signin_resp) = data.register_and_signin(&db, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
|
||||
// Sign in with email
|
||||
data.signin_test(&db, EMAIL, PASSWORD).await;
|
||||
|
||||
// 2. check if duplicate username is allowed
|
||||
let mut msg = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: Some(EMAIL.into()),
|
||||
};
|
||||
|
||||
msg.username = format!("asdfasd{}", msg.username);
|
||||
data.bad_post_req_test(
|
||||
&db,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.register,
|
||||
&msg,
|
||||
ServiceError::EmailTaken,
|
||||
)
|
||||
.await;
|
||||
|
||||
msg.email = Some(format!("asdfasd{}", msg.email.unwrap()));
|
||||
msg.username = NAME.into();
|
||||
data.bad_post_req_test(
|
||||
&db,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.register,
|
||||
&msg,
|
||||
ServiceError::UsernameTaken,
|
||||
)
|
||||
.await;
|
||||
|
||||
// 3. sigining in with non-existent user
|
||||
let mut creds = Login {
|
||||
login: "nonexistantuser".into(),
|
||||
password: msg.password.clone(),
|
||||
};
|
||||
data.bad_post_req_test(
|
||||
&db,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.login,
|
||||