diff --git a/identity-api-rs/.database/.gitkeep b/identity-api-rs/.database/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/identity-api-rs/.gitignore b/identity-api-rs/.gitignore
new file mode 100644
index 0000000..f518836
--- /dev/null
+++ b/identity-api-rs/.gitignore
@@ -0,0 +1,9 @@
+/target
+Cargo.lock
+
+.database/*
+!.database/.gitkeep
+
+.env
+.env.*
+!.env.example
\ No newline at end of file
diff --git a/identity-api-rs/Cargo.toml b/identity-api-rs/Cargo.toml
new file mode 100644
index 0000000..4bee58b
--- /dev/null
+++ b/identity-api-rs/Cargo.toml
@@ -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"] }
\ No newline at end of file
diff --git a/identity-api-rs/diesel.toml b/identity-api-rs/diesel.toml
new file mode 100644
index 0000000..fd7a1cd
--- /dev/null
+++ b/identity-api-rs/diesel.toml
@@ -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"
diff --git a/identity-api-rs/migrations/.gitkeep b/identity-api-rs/migrations/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/down.sql b/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/down.sql
new file mode 100644
index 0000000..1528d22
--- /dev/null
+++ b/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/down.sql
@@ -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;
\ No newline at end of file
diff --git a/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/up.sql b/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/up.sql
new file mode 100644
index 0000000..07a7209
--- /dev/null
+++ b/identity-api-rs/migrations/2024-10-13-184137_users_and_entries/up.sql
@@ -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
+);
\ No newline at end of file
diff --git a/identity-api-rs/src/database/mod.rs b/identity-api-rs/src/database/mod.rs
new file mode 100644
index 0000000..f689afe
--- /dev/null
+++ b/identity-api-rs/src/database/mod.rs
@@ -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 .
+
+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))
+}
diff --git a/identity-api-rs/src/database/models.rs b/identity-api-rs/src/database/models.rs
new file mode 100644
index 0000000..26249a8
--- /dev/null
+++ b/identity-api-rs/src/database/models.rs
@@ -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 .
+
+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,
+ assets: List,
+ title: Option,
+ description: Option,
+ kind: String,
+ music_entry: Option,
+ location_entry: Option,
+ date_entry: Option,
+}
diff --git a/identity-api-rs/src/database/schema.rs b/identity-api-rs/src/database/schema.rs
new file mode 100644
index 0000000..17e27cd
--- /dev/null
+++ b/identity-api-rs/src/database/schema.rs
@@ -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,
+ description -> Nullable,
+ kind -> Text,
+ music_entry -> Nullable,
+ location_entry -> Nullable,
+ date_entry -> Nullable,
+ }
+}
+
+diesel::table! {
+ heirs (id) {
+ id -> Text,
+ user_id -> Text,
+ created_at -> Timestamp,
+ name -> Text,
+ email -> Nullable,
+ }
+}
+
+diesel::table! {
+ limits (id) {
+ id -> Text,
+ current_asset_count -> Integer,
+ max_asset_count -> Integer,
+ }
+}
+
+diesel::table! {
+ location_entries (id) {
+ id -> Text,
+ location_text -> Nullable,
+ location_coordinates -> Nullable,
+ }
+}
+
+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,
+);
diff --git a/identity-api-rs/src/database/types.rs b/identity-api-rs/src/database/types.rs
new file mode 100644
index 0000000..dc14bbf
--- /dev/null
+++ b/identity-api-rs/src/database/types.rs
@@ -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(pub Vec);
+
+impl ToString for List {
+ fn to_string(&self) -> String {
+ "[".to_owned()
+ + &self
+ .0
+ .iter()
+ .map(|v| "\"".to_owned() + &v.replace("\"", "\\\"") + "\"")
+ .collect::>()
+ .join(",")
+ + "]"
+ }
+}
+
+impl From for List {
+ 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 for List
+where
+ String: FromSql,
+{
+ fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result {
+ let str = >::from_sql(bytes)?;
+ Ok(List::from(str))
+ }
+}
+
+impl ToSql for List
+where
+ String: ToSql,
+{
+ 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)
+ }
+}
diff --git a/identity-api-rs/src/env.rs b/identity-api-rs/src/env.rs
new file mode 100644
index 0000000..8b0e325
--- /dev/null
+++ b/identity-api-rs/src/env.rs
@@ -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 .
+
+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 = 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 = 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 = 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 = 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 = 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 = 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 = OnceLock::new();
+ DATABASE_URL.get_or_init(|| {
+ env::var("DATABASE_URL").expect("environment variables were not loaded correctly")
+ })
+}
diff --git a/identity-api-rs/src/main.rs b/identity-api-rs/src/main.rs
new file mode 100644
index 0000000..d50c277
--- /dev/null
+++ b/identity-api-rs/src/main.rs
@@ -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 .
+
+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()
+}