Compare commits
No commits in common. "fe62b28a0363d6f378a8ea1cebd80fc448e52790" and "864521787ff15d3cd2bf70a9c8df25b6b1dd88e6" have entirely different histories.
fe62b28a03
...
864521787f
20 changed files with 0 additions and 909 deletions
9
identity-api-rs/.gitignore
vendored
9
identity-api-rs/.gitignore
vendored
|
@ -1,9 +0,0 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
|
||||
.database/*
|
||||
!.database/.gitkeep
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "identity-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
axum = { version = "0.7", features = ["macros", "tracing"] }
|
||||
tower-http = { version = "0.6", features = ["trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "r2d2"] }
|
||||
dotenvy = "0.15"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
r2d2 = "0.8"
|
||||
jsonwebtoken = "9"
|
||||
uuid = { version = "1.10", features = ["v4", "fast-rng"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
|
@ -1,9 +0,0 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/database/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
|
@ -1,8 +0,0 @@
|
|||
DROP TABLE IF EXISTS date_entries;
|
||||
DROP TABLE IF EXISTS location_entries;
|
||||
DROP TABLE IF EXISTS music_entries;
|
||||
DROP TABLE IF EXISTS entries;
|
||||
DROP TABLE IF EXISTS heirs;
|
||||
DROP TABLE IF EXISTS session_keys;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS limits;
|
|
@ -1,69 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS limits (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
current_asset_count integer NOT NULL,
|
||||
max_asset_count integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
last_connected_at timestamp NOT NULL,
|
||||
email varchar NOT NULL,
|
||||
password varchar NOT NULL,
|
||||
name varchar NOT NULL,
|
||||
limits varchar NOT NULL,
|
||||
assets varchar NOT NULL,
|
||||
FOREIGN KEY (limits) REFERENCES limits (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_keys (
|
||||
key varchar PRIMARY KEY NOT NULL,
|
||||
user_id varchar NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS heirs (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
user_id varchar NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
name varchar NOT NULL,
|
||||
email varchar,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
user_id varchar NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
feelings text NOT NULL,
|
||||
assets text NOT NULL,
|
||||
title text,
|
||||
description text,
|
||||
kind varchar NOT NULL,
|
||||
music_entry varchar,
|
||||
location_entry varchar,
|
||||
date_entry varchar,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id),
|
||||
FOREIGN KEY (music_entry) REFERENCES music_entries (id),
|
||||
FOREIGN KEY (location_entry) REFERENCES location_entries (id),
|
||||
FOREIGN KEY (date_entry) REFERENCES date_entries (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS music_entries (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
artist varchar NOT NULL,
|
||||
title varchar NOT NULL,
|
||||
links text NOT NULL,
|
||||
universal_ids text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS location_entries (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
location_text text,
|
||||
location_coordinates varchar
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS date_entries (
|
||||
id varchar PRIMARY KEY NOT NULL,
|
||||
referenced_date timestamp NOT NULL
|
||||
);
|
|
@ -1,19 +0,0 @@
|
|||
use crate::env;
|
||||
use jsonwebtoken::{TokenData, Header, Validation};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JwtUser {
|
||||
pub uid: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub exp: u64,
|
||||
}
|
||||
|
||||
pub fn encode_jwt(claims: &JwtUser) -> jsonwebtoken::errors::Result<String> {
|
||||
jsonwebtoken::encode(&Header::new(*env::jwt_alg()), claims, &env::jwt_secret().0)
|
||||
}
|
||||
|
||||
pub fn decode_jwt(jwt: &str) -> jsonwebtoken::errors::Result<TokenData<JwtUser>> {
|
||||
jsonwebtoken::decode::<JwtUser>(jwt, &env::jwt_secret().1, &Validation::new(*env::jwt_alg()))
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
use diesel::{SqliteConnection, r2d2::{ConnectionManager, PooledConnection}, RunQueryDsl, QueryDsl, SelectableHelper, ExpressionMethods, OptionalExtension};
|
||||
use crate::database::models::User;
|
||||
|
||||
pub fn user(user_id: &str, conn: &mut PooledConnection<ConnectionManager<SqliteConnection>>) -> diesel::result::QueryResult<User> {
|
||||
use crate::database::schema::users::dsl::users;
|
||||
users
|
||||
.find(user_id)
|
||||
.select(User::as_select())
|
||||
.first(conn)
|
||||
}
|
||||
|
||||
pub fn user_by_email(email: &str, conn: &mut PooledConnection<ConnectionManager<SqliteConnection>>) -> diesel::result::QueryResult<Option<User>> {
|
||||
use crate::database::schema::users::dsl as users;
|
||||
users::users
|
||||
.filter(users::email.eq(email))
|
||||
.limit(1)
|
||||
.select(User::as_select())
|
||||
.first(conn)
|
||||
.optional()
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// Identity. Store your memories and mental belongings
|
||||
// Copyright (C) 2024 Sofía Aritz
|
||||
//
|
||||
// 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::fmt::Display;
|
||||
use diesel::{
|
||||
backend::Backend, deserialize::{FromSql, FromSqlRow}, serialize::ToSql, sql_types::Text, sqlite::Sqlite
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
#[derive(FromSqlRow, Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub struct List<V: std::fmt::Debug + std::clone::Clone>(pub Vec<V>);
|
||||
|
||||
impl<V: std::fmt::Debug + std::clone::Clone> List<V> {
|
||||
fn from_vec(vec: Vec<V>) -> Self {
|
||||
Self(vec)
|
||||
}
|
||||
|
||||
fn into_vec(self) -> Vec<V> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Serialize + std::fmt::Debug + std::clone::Clone> Display for List<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&serde_json::to_string(&self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> From<String> for List<A>
|
||||
where
|
||||
A: DeserializeOwned + std::fmt::Debug + std::clone::Clone,
|
||||
{
|
||||
fn from(value: String) -> Self {
|
||||
serde_json::from_str(&value).expect("failed to deserialize list")
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: std::fmt::Debug + std::clone::Clone + DeserializeOwned> FromSql<Text, Sqlite> for List<V>
|
||||
{
|
||||
fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||
let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
|
||||
Ok(List::from(str))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: std::fmt::Debug + std::clone::Clone + Serialize> ToSql<Text, Sqlite> for List<V>
|
||||
{
|
||||
fn to_sql<'b>(
|
||||
&'b self,
|
||||
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
|
||||
) -> diesel::serialize::Result {
|
||||
let val = self.to_string();
|
||||
out.set_value(val);
|
||||
Ok(diesel::serialize::IsNull::No)
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// Identity. Store your memories and mental belongings
|
||||
// Copyright (C) 2024 Sofía Aritz
|
||||
//
|
||||
// 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 diesel::prelude::*;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use diesel::r2d2::Pool;
|
||||
|
||||
use crate::env;
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod list;
|
||||
pub mod actions;
|
||||
|
||||
|
||||
pub fn create_connection_pool() -> Result<Pool<ConnectionManager<SqliteConnection>>, r2d2::Error> {
|
||||
let url = env::database_url();
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(url);
|
||||
Pool::builder().build(manager)
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
// Identity. Store your memories and mental belongings
|
||||
// Copyright (C) 2024 Sofía Aritz
|
||||
//
|
||||
// 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 chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::schema;
|
||||
use crate::database::list::List;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UniversalId {
|
||||
provider: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocationCoordinates {
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::date_entries)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct DateEntry {
|
||||
id: String,
|
||||
referenced_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::entries)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Entry {
|
||||
id: String,
|
||||
user_id: String,
|
||||
created_at: NaiveDateTime,
|
||||
feelings: List<String>,
|
||||
assets: List<String>,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
kind: String,
|
||||
music_entry: Option<String>,
|
||||
location_entry: Option<String>,
|
||||
date_entry: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::heirs)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Heir {
|
||||
id: String,
|
||||
user_id: String,
|
||||
created_at: NaiveDateTime,
|
||||
name: String,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::limits)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Limit {
|
||||
id: String,
|
||||
current_asset_count: i32,
|
||||
max_asset_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::location_entries)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct LocationEntry {
|
||||
id: String,
|
||||
location_text: Option<String>,
|
||||
/// JSON value: { latitude: number, longitude: number }
|
||||
location_coordinates: Option<String>,
|
||||
}
|
||||
|
||||
impl LocationEntry {
|
||||
pub fn location_coordinates(&self) -> Option<LocationCoordinates> {
|
||||
self.location_coordinates.as_ref().map(|v| serde_json::from_str(v).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::music_entries)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct MusicEntry {
|
||||
id: String,
|
||||
artist: String,
|
||||
title: String,
|
||||
links: List<String>,
|
||||
universal_ids: List<UniversalId>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::session_keys)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct SessionKey {
|
||||
key: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = schema::users)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub last_connected_at: NaiveDateTime,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
pub limits: String,
|
||||
pub assets: String,
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
date_entries (id) {
|
||||
id -> Text,
|
||||
referenced_date -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
entries (id) {
|
||||
id -> Text,
|
||||
user_id -> Text,
|
||||
created_at -> Timestamp,
|
||||
feelings -> Text,
|
||||
assets -> Text,
|
||||
title -> Nullable<Text>,
|
||||
description -> Nullable<Text>,
|
||||
kind -> Text,
|
||||
music_entry -> Nullable<Text>,
|
||||
location_entry -> Nullable<Text>,
|
||||
date_entry -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
heirs (id) {
|
||||
id -> Text,
|
||||
user_id -> Text,
|
||||
created_at -> Timestamp,
|
||||
name -> Text,
|
||||
email -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
limits (id) {
|
||||
id -> Text,
|
||||
current_asset_count -> Integer,
|
||||
max_asset_count -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
location_entries (id) {
|
||||
id -> Text,
|
||||
location_text -> Nullable<Text>,
|
||||
location_coordinates -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
music_entries (id) {
|
||||
id -> Text,
|
||||
artist -> Text,
|
||||
title -> Text,
|
||||
links -> Text,
|
||||
universal_ids -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
session_keys (key) {
|
||||
key -> Text,
|
||||
user_id -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Text,
|
||||
created_at -> Timestamp,
|
||||
last_connected_at -> Timestamp,
|
||||
email -> Text,
|
||||
password -> Text,
|
||||
name -> Text,
|
||||
limits -> Text,
|
||||
assets -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(entries -> date_entries (date_entry));
|
||||
diesel::joinable!(entries -> location_entries (location_entry));
|
||||
diesel::joinable!(entries -> music_entries (music_entry));
|
||||
diesel::joinable!(entries -> users (user_id));
|
||||
diesel::joinable!(heirs -> users (user_id));
|
||||
diesel::joinable!(session_keys -> users (user_id));
|
||||
diesel::joinable!(users -> limits (limits));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
date_entries,
|
||||
entries,
|
||||
heirs,
|
||||
limits,
|
||||
location_entries,
|
||||
music_entries,
|
||||
session_keys,
|
||||
users,
|
||||
);
|
|
@ -1,117 +0,0 @@
|
|||
// Identity. Store your memories and mental belongings
|
||||
// Copyright (C) 2024 Sofía Aritz
|
||||
//
|
||||
// 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::{env, str::FromStr};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
|
||||
|
||||
const REQUIRED_ENV_VARIABLES: [&str; 4] = [
|
||||
"IDENTITY_API_JWT_SECRET",
|
||||
"IDENTITY_API_ASSET_API_ENDPOINT",
|
||||
"IDENTITY_API_JWT_ALG",
|
||||
"DATABASE_URL",
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadEnvError {
|
||||
DotenvyError(dotenvy::Error),
|
||||
MissingVariable(String),
|
||||
}
|
||||
|
||||
pub fn load_env() -> Result<(), LoadEnvError> {
|
||||
dotenvy::dotenv().map_err(LoadEnvError::DotenvyError)?;
|
||||
|
||||
for key in REQUIRED_ENV_VARIABLES {
|
||||
if env::var(key).is_err() {
|
||||
return Err(LoadEnvError::MissingVariable(key.into()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn landing_message() -> &'static str {
|
||||
static IDENTITY_API_LANDING_MESSAGE: OnceLock<String> = OnceLock::new();
|
||||
IDENTITY_API_LANDING_MESSAGE.get_or_init(|| {
|
||||
env::var("IDENTITY_API_LANDING_MESSAGE").unwrap_or(format!(
|
||||
"{} v{}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn asset_api_m2m_refresh_interval() -> &'static Duration {
|
||||
static IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL: OnceLock<Duration> = OnceLock::new();
|
||||
IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL.get_or_init(|| {
|
||||
let millis: u64 = env::var("IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_ms")
|
||||
.map(|v|
|
||||
v
|
||||
.parse()
|
||||
.expect("invalid environment variable value: `IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_ms`")
|
||||
)
|
||||
.unwrap_or(60 * 1000);
|
||||
|
||||
Duration::from_millis(millis)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn listen_port() -> &'static u16 {
|
||||
static IDENTITY_API_LISTEN_PORT: OnceLock<u16> = OnceLock::new();
|
||||
IDENTITY_API_LISTEN_PORT.get_or_init(|| {
|
||||
env::var("IDENTITY_API_LISTEN_PORT")
|
||||
.map(|v| {
|
||||
v.parse()
|
||||
.expect("invalid environment variable value: `IDENTITY_API_LISTEN_PORT`")
|
||||
})
|
||||
.unwrap_or(3000)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jwt_secret() -> &'static (EncodingKey, DecodingKey) {
|
||||
static IDENTITY_API_JWT_SECRET: OnceLock<(EncodingKey, DecodingKey)> = OnceLock::new();
|
||||
IDENTITY_API_JWT_SECRET.get_or_init(|| {
|
||||
let secret = env::var("IDENTITY_API_JWT_SECRET")
|
||||
.expect("environment variables were not loaded correctly");
|
||||
|
||||
(EncodingKey::from_secret(secret.as_bytes()), DecodingKey::from_secret(secret.as_bytes()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jwt_alg() -> &'static Algorithm {
|
||||
static IDENTITY_API_JWT_ALG: OnceLock<Algorithm> = OnceLock::new();
|
||||
IDENTITY_API_JWT_ALG.get_or_init(|| {
|
||||
let algo = env::var("IDENTITY_API_JWT_ALG").expect("environment variables were not loaded correctly");
|
||||
Algorithm::from_str(&algo).expect("invalid JWT algorithm")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn asset_api_endpoint() -> &'static str {
|
||||
static IDENTITY_API_ASSET_API_ENDPOINT: OnceLock<String> = OnceLock::new();
|
||||
IDENTITY_API_ASSET_API_ENDPOINT.get_or_init(|| {
|
||||
env::var("IDENTITY_API_ASSET_API_ENDPOINT")
|
||||
.expect("environment variables were not loaded correctly")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn database_url() -> &'static str {
|
||||
static DATABASE_URL: OnceLock<String> = OnceLock::new();
|
||||
DATABASE_URL.get_or_init(|| {
|
||||
env::var("DATABASE_URL").expect("environment variables were not loaded correctly")
|
||||
})
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use axum::{async_trait, extract::FromRequestParts, http::{header::AUTHORIZATION, request::Parts, StatusCode}};
|
||||
use tracing::{warn, error};
|
||||
use crate::database::{actions, models::User};
|
||||
use crate::AppState;
|
||||
use crate::auth::JwtUser;
|
||||
pub struct ExtractJwtUser(pub JwtUser);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for ExtractJwtUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
if let Some(authorization) = parts.headers.get(AUTHORIZATION) {
|
||||
if let Ok(authorization) = authorization.to_str() {
|
||||
let token = authorization.replacen("Bearer ", "", 1);
|
||||
match crate::auth::decode_jwt(&token) {
|
||||
Ok(claims) => Ok(Self(claims.claims)),
|
||||
Err(err) => {
|
||||
warn!("token couldn't be decoded: {:?}", err);
|
||||
Err((StatusCode::UNAUTHORIZED, "Invalid token"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("invalid authorization header: {:?}", authorization);
|
||||
Err((StatusCode::BAD_REQUEST, "Invalid `AUTHORIZATION` header"))
|
||||
}
|
||||
} else {
|
||||
warn!("missing authorization header");
|
||||
Err((StatusCode::BAD_REQUEST, "Missing `AUTHORIZATION` header"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtractUser(pub User);
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<AppState> for ExtractUser
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||
let jwt_user = ExtractJwtUser::from_request_parts(parts, state).await?;
|
||||
|
||||
if let Ok(mut conn) = state.pool.get() {
|
||||
if let Ok(user) = actions::user(&jwt_user.0.uid, &mut conn) {
|
||||
Ok(Self(user))
|
||||
} else {
|
||||
error!("JWT user does not exist in database");
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
|
||||
}
|
||||
} else {
|
||||
error!("failed to obtain pooled connection");
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod auth;
|
|
@ -1,2 +0,0 @@
|
|||
pub mod extractors;
|
||||
pub mod routes;
|
|
@ -1,128 +0,0 @@
|
|||
use std::time::SystemTime;
|
||||
use argon2::{Argon2, PasswordHasher, password_hash::{rand_core::OsRng, SaltString}};
|
||||
use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router};
|
||||
use chrono::{Utc, NaiveDateTime};
|
||||
use diesel::{RunQueryDsl, ExpressionMethods};
|
||||
use tracing::{error, info};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use crate::{database::actions, http::extractors::auth::ExtractUser, auth::JwtUser, AppState};
|
||||
|
||||
pub fn auth_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/auth/account", get(account))
|
||||
.route("/auth/register", post(register))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AccountResponse {
|
||||
id: String,
|
||||
created_at: NaiveDateTime,
|
||||
last_connected_at: NaiveDateTime,
|
||||
email: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
async fn account(ExtractUser(user): ExtractUser) -> Result<Json<AccountResponse>, StatusCode> {
|
||||
Ok(Json(AccountResponse {
|
||||
id: user.id,
|
||||
created_at: user.created_at,
|
||||
last_connected_at: user.last_connected_at,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RegisterRequest {
|
||||
email: String,
|
||||
password: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RegisterResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
async fn register(State(state): State<AppState>, Json(req): Json<RegisterRequest>) -> Result<Json<RegisterResponse>, StatusCode> {
|
||||
use crate::database::schema::users::dsl as users;
|
||||
use crate::database::schema::limits::dsl as limits;
|
||||
|
||||
if let Ok(mut conn) = state.pool.get() {
|
||||
let user = actions::user_by_email(&req.email, &mut conn);
|
||||
|
||||
if user.is_err() {
|
||||
error!("failed to retrieve potential existing user from database: {}, error: {:?}", &req.email, user.err());
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
if user.is_ok_and(|v| v.is_some()) {
|
||||
info!("tried to register existing user: {}", &req.email);
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
let limit_id = Uuid::new_v4().to_string();
|
||||
let result = diesel::insert_into(limits::limits)
|
||||
.values((
|
||||
limits::id.eq(&limit_id),
|
||||
limits::current_asset_count.eq(0),
|
||||
limits::max_asset_count.eq(10),
|
||||
))
|
||||
.execute(&mut conn);
|
||||
|
||||
if result.is_err() {
|
||||
error!("failed to insert into limits: {}, error: {:?}", &req.email, result.err());
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2.hash_password(req.password.as_bytes(), &salt);
|
||||
|
||||
if let Ok(password_hash) = password_hash {
|
||||
let user_id = Uuid::new_v4().to_string();
|
||||
let result = diesel::insert_into(users::users)
|
||||
.values((
|
||||
users::id.eq(&user_id),
|
||||
users::created_at.eq(Utc::now().naive_utc()),
|
||||
users::last_connected_at.eq(Utc::now().naive_utc()),
|
||||
users::email.eq(&req.email),
|
||||
users::password.eq(password_hash.to_string()),
|
||||
users::name.eq(&req.name),
|
||||
users::limits.eq(&limit_id),
|
||||
// TODO: Implement diesel::Expression for List
|
||||
users::assets.eq("[]"),
|
||||
))
|
||||
.execute(&mut conn);
|
||||
|
||||
if result.is_err() {
|
||||
error!("failed to insert into users: {}, error: {:?}", req.email, result.err());
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
match crate::auth::encode_jwt(&JwtUser {
|
||||
uid: user_id,
|
||||
email: req.email,
|
||||
name: req.name,
|
||||
exp: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_secs() + 180 * 24 * 3600,
|
||||
}) {
|
||||
Ok(token) => Ok(Json(RegisterResponse { token })),
|
||||
Err(err) => {
|
||||
error!("token couldn't be encoded: {:?}", err);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("failed to hash password: {:?}", password_hash.err());
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
} else {
|
||||
error!("failed to obtain pooled connection");
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod auth;
|
|
@ -1,116 +0,0 @@
|
|||
// Identity. Store your memories and mental belongings
|
||||
// Copyright (C) 2024 Sofía Aritz
|
||||
//
|
||||
// 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 axum::{extract::{MatchedPath, Request}, response::Response, routing::get, Router};
|
||||
use database::create_connection_pool;
|
||||
use diesel::{r2d2::ConnectionManager, SqliteConnection};
|
||||
use env::LoadEnvError;
|
||||
use http::routes::auth::auth_router;
|
||||
use r2d2::Pool;
|
||||
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
|
||||
use tracing::{info, info_span, warn, error, Span};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use tokio::time::Duration;
|
||||
|
||||
mod database;
|
||||
mod env;
|
||||
mod http;
|
||||
mod auth;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
pool: Pool<ConnectionManager<SqliteConnection>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let _ = env::load_env().inspect_err(|e| match e {
|
||||
LoadEnvError::DotenvyError(error) => panic!("failed to load .env file: {:?}", error),
|
||||
LoadEnvError::MissingVariable(variable) => {
|
||||
panic!("missing environment variable: `{variable}`")
|
||||
}
|
||||
});
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
// axum logs rejections from built-in extractors with the `axum::rejection`
|
||||
// target, at `TRACE` level. `axum::rejection=trace` enables showing those events
|
||||
format!(
|
||||
"{}=debug,tower_http=debug,axum::rejection=trace",
|
||||
env!("CARGO_CRATE_NAME")
|
||||
)
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let state = AppState {
|
||||
pool: create_connection_pool().expect("failed to create database connection pool"),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(landing))
|
||||
.merge(auth_router())
|
||||
.with_state(state)
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request<_>| {
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
|
||||
info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
)
|
||||
})
|
||||
.on_response(|response: &Response, _latency: Duration, _span: &Span| {
|
||||
if response.status().is_client_error() {
|
||||
warn!(
|
||||
"client error: {}",
|
||||
response.status().to_string()
|
||||
);
|
||||
} else {
|
||||
info!("finished processing request");
|
||||
}
|
||||
})
|
||||
.on_failure(
|
||||
|error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
|
||||
error!(
|
||||
"internal server error: {}",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||
.await
|
||||
.expect("failed to bind");
|
||||
|
||||
println!("listening on http://{}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("failed to serve app");
|
||||
}
|
||||
|
||||
async fn landing() -> &'static str {
|
||||
env::landing_message()
|
||||
}
|
Loading…
Reference in a new issue