Start working on Rust impl of identity-api

This commit is contained in:
Sofía Aritz 2024-10-14 17:34:10 +02:00
parent 864521787f
commit 1d131e790c
Signed by: sofia
GPG key ID: 90B5116E3542B28F
13 changed files with 520 additions and 0 deletions

View file

9
identity-api-rs/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
/target
Cargo.lock
.database/*
!.database/.gitkeep
.env
.env.*
!.env.example

View 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"] }

View 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"

View file

View 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;

View file

@ -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
);

View 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))
}

View 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>,
}

View 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,
);

View 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
View 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")
})
}

View 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()
}