Start working on Rust impl of identity-api
This commit is contained in:
parent
864521787f
commit
1d131e790c
13 changed files with 520 additions and 0 deletions
0
identity-api-rs/.database/.gitkeep
Normal file
0
identity-api-rs/.database/.gitkeep
Normal file
9
identity-api-rs/.gitignore
vendored
Normal file
9
identity-api-rs/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
|
||||
.database/*
|
||||
!.database/.gitkeep
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
11
identity-api-rs/Cargo.toml
Normal file
11
identity-api-rs/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "identity-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7" }
|
||||
chrono = "0.4"
|
||||
diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono"] }
|
||||
dotenvy = "0.15"
|
||||
tokio = { version = "1", features = ["full"] }
|
9
identity-api-rs/diesel.toml
Normal file
9
identity-api-rs/diesel.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 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"
|
0
identity-api-rs/migrations/.gitkeep
Normal file
0
identity-api-rs/migrations/.gitkeep
Normal file
|
@ -0,0 +1,8 @@
|
|||
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;
|
|
@ -0,0 +1,69 @@
|
|||
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
|
||||
);
|
28
identity-api-rs/src/database/mod.rs
Normal file
28
identity-api-rs/src/database/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// 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::{Connection, SqliteConnection};
|
||||
|
||||
use crate::env::database_url;
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod types;
|
||||
|
||||
pub fn establish_connection() -> SqliteConnection {
|
||||
let url = database_url();
|
||||
SqliteConnection::establish(url).unwrap_or_else(|_| panic!("failed to connect to {}", url))
|
||||
}
|
46
identity-api-rs/src/database/models.rs
Normal file
46
identity-api-rs/src/database/models.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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 crate::database::schema;
|
||||
use crate::database::types::List;
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[diesel(table_name = schema::date_entries)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct DateEntry {
|
||||
id: String,
|
||||
referenced_date: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[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>,
|
||||
}
|
99
identity-api-rs/src/database/schema.rs
Normal file
99
identity-api-rs/src/database/schema.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
// @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,
|
||||
);
|
83
identity-api-rs/src/database/types.rs
Normal file
83
identity-api-rs/src/database/types.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use diesel::{
|
||||
backend::Backend, deserialize::{FromSql, FromSqlRow}, serialize::ToSql, sql_types::Text, sqlite::Sqlite
|
||||
};
|
||||
|
||||
#[derive(FromSqlRow, Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct List<V: std::fmt::Debug>(pub Vec<V>);
|
||||
|
||||
impl ToString for List<String> {
|
||||
fn to_string(&self) -> String {
|
||||
"[".to_owned()
|
||||
+ &self
|
||||
.0
|
||||
.iter()
|
||||
.map(|v| "\"".to_owned() + &v.replace("\"", "\\\"") + "\"")
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
+ "]"
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for List<String> {
|
||||
fn from(mut value: String) -> Self {
|
||||
if value.starts_with('[') {
|
||||
value.remove(0);
|
||||
}
|
||||
|
||||
if value.ends_with(']') {
|
||||
value.pop();
|
||||
}
|
||||
|
||||
let char_len = value.chars().count();
|
||||
let mut values = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut writing_str = false;
|
||||
let mut escape_next = false;
|
||||
|
||||
for (i, ch) in value.chars().enumerate() {
|
||||
if i == char_len - 1 {
|
||||
values.push(current);
|
||||
current = String::new();
|
||||
} else if escape_next {
|
||||
current.push(ch);
|
||||
escape_next = false;
|
||||
} else if ch == '\\' {
|
||||
escape_next = true;
|
||||
} else if ch == '\"' {
|
||||
writing_str = !writing_str;
|
||||
} else if ch == ',' && !writing_str {
|
||||
values.push(current);
|
||||
current = String::new();
|
||||
} else if writing_str {
|
||||
current.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
List(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Text, Sqlite> for List<String>
|
||||
where
|
||||
String: FromSql<Text, Sqlite>,
|
||||
{
|
||||
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 ToSql<Text, Sqlite> for List<String>
|
||||
where
|
||||
String: ToSql<Text, Sqlite>,
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
112
identity-api-rs/src/env.rs
Normal file
112
identity-api-rs/src/env.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
// 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;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
const REQUIRED_ENV_VARIABLES: [&'static 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(|e| LoadEnvError::DotenvyError(e))?;
|
||||
|
||||
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 str {
|
||||
static IDENTITY_API_JWT_SECRET: OnceLock<String> = OnceLock::new();
|
||||
IDENTITY_API_JWT_SECRET.get_or_init(|| {
|
||||
env::var("IDENTITY_API_JWT_SECRET")
|
||||
.expect("environment variables were not loaded correctly")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jwt_alg() -> &'static str {
|
||||
static IDENTITY_API_JWT_ALG: OnceLock<String> = OnceLock::new();
|
||||
IDENTITY_API_JWT_ALG.get_or_init(|| {
|
||||
env::var("IDENTITY_API_JWT_ALG").expect("environment variables were not loaded correctly")
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
46
identity-api-rs/src/main.rs
Normal file
46
identity-api-rs/src/main.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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::{routing::get, Router};
|
||||
use env::LoadEnvError;
|
||||
|
||||
mod database;
|
||||
mod env;
|
||||
|
||||
#[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}`")
|
||||
}
|
||||
});
|
||||
|
||||
let app = Router::new().route("/", get(landing));
|
||||
|
||||
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