apps: sql + get valid one, get by id, insert, generate app id, generate secret

This commit is contained in:
Philippe Loctaux 2023-03-15 22:00:04 +01:00
parent b5c2be6c9f
commit 71b083895d
19 changed files with 490 additions and 0 deletions

14
crates/apps/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "apps"
version = "0.0.0"
edition = "2021"
[dependencies]
thiserror = { workspace = true }
chrono = { workspace = true }
url = { workspace = true }
# local crates
id = { path = "../id" }
database = { path = "../database" }
hash = { path = "../hash" }

View file

@ -0,0 +1,61 @@
use crate::error::Error;
use crate::App;
use database::sqlx::SqliteExecutor;
use database::Apps as DatabaseApps;
use hash::Secret;
use id::AppID;
use url::Url;
impl From<DatabaseApps> for App {
fn from(db: DatabaseApps) -> Self {
Self {
id: AppID(db.id),
created_at: db.created_at,
updated_at: db.updated_at,
is_archived: db.is_archived,
label: db.label,
redirect_uri: db.redirect_uri,
secret: db.secret,
is_confidential: db.is_confidential,
}
}
}
impl App {
pub async fn insert(
conn: impl SqliteExecutor<'_>,
id: &AppID,
label: &str,
redirect_uri: &Url,
secret: &Secret,
is_confidential: bool,
) -> Result<Option<()>, Error> {
Ok(DatabaseApps::insert(
conn,
id.as_ref(),
label,
redirect_uri.as_str(),
secret.hash(),
is_confidential,
)
.await?)
}
/// App needs to be not archived
pub async fn get_one(
conn: impl SqliteExecutor<'_>,
id: &str,
redirect: &str,
) -> Result<Option<Self>, Error> {
Ok(DatabaseApps::get_one(conn, id, redirect)
.await?
.map(Self::from))
}
pub async fn get_one_by_id(
conn: impl SqliteExecutor<'_>,
id: &str,
) -> Result<Option<Self>, Error> {
Ok(DatabaseApps::get_one_by_id(conn, id).await?.map(Self::from))
}
}

8
crates/apps/src/error.rs Normal file
View file

@ -0,0 +1,8 @@
// error
#[derive(thiserror::Error)]
// the rest
#[derive(Debug)]
pub enum Error {
#[error("Database: {0}")]
Database(#[from] database::Error),
}

31
crates/apps/src/lib.rs Normal file
View file

@ -0,0 +1,31 @@
mod database;
mod error;
use chrono::{DateTime, Utc};
use id::AppID;
pub use crate::error::Error;
#[derive(Debug)]
pub struct App {
id: AppID,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
label: String,
redirect_uri: String,
secret: String,
is_confidential: bool,
is_archived: bool,
}
impl App {
pub fn id(&self) -> &AppID {
&self.id
}
pub fn label(&self) -> &str {
&self.label
}
pub fn redirect_uri(&self) -> &str {
&self.redirect_uri
}
}

View file

@ -0,0 +1 @@
drop table if exists apps;

View file

@ -0,0 +1,22 @@
create table if not exists apps
(
id TEXT not null primary key,
created_at TEXT not null default CURRENT_TIMESTAMP,
updated_at TEXT not null default CURRENT_TIMESTAMP,
label TEXT not null,
redirect_uri TEXT not null,
secret TEXT not null,
is_confidential INTEGER not null,
is_archived INTEGER not null default 0
);
-- update "updated_at"
create trigger if not exists apps_updated_at
after update
on apps
for each row
begin
update apps
set updated_at = CURRENT_TIMESTAMP
where id is NEW.id;
end;

View file

@ -0,0 +1,13 @@
select id,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
label,
redirect_uri,
secret,
is_confidential as "is_confidential: bool",
is_archived as "is_archived: bool"
from apps
where id is (?)
and redirect_uri is (?)
and is_archived is 0

View file

@ -0,0 +1,11 @@
select id,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
label,
redirect_uri,
secret,
is_confidential as "is_confidential: bool",
is_archived as "is_archived: bool"
from apps
where id is (?)

View file

@ -0,0 +1,2 @@
insert into apps (id, label, redirect_uri, secret, is_confidential)
values (?, ?, ?, ?, ?)

View file

@ -510,6 +510,136 @@
},
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n revoked_at as \"revoked_at: DateTime<Utc>\",\n private_der,\n public_der\n\nfrom keys\nwhere revoked_at is null\norder by created_at desc\n"
},
"e22ba816faac0c17ca9f2c31fd1b4a5f13a09cece9ec78e0b6e018950c91facb": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "label",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "redirect_uri",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "secret",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "is_confidential: bool",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "is_archived: bool",
"ordinal": 7,
"type_info": "Int64"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n label,\n redirect_uri,\n secret,\n is_confidential as \"is_confidential: bool\",\n is_archived as \"is_archived: bool\"\nfrom apps\n\nwhere id is (?)\n"
},
"eb1a0153c88b0b2744ed1b71df04a91a129a0173fbbc3e2536f52d41e8dc99c4": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "label",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "redirect_uri",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "secret",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "is_confidential: bool",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "is_archived: bool",
"ordinal": 7,
"type_info": "Int64"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 2
}
},
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n label,\n redirect_uri,\n secret,\n is_confidential as \"is_confidential: bool\",\n is_archived as \"is_archived: bool\"\nfrom apps\n\nwhere id is (?)\n and redirect_uri is (?)\n and is_archived is 0\n"
},
"ed27954feb3e21b5c519ccd0312526e68fb3d88a1feb28bdafb414e990da55e8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 5
}
},
"query": "insert into apps (id, label, redirect_uri, secret, is_confidential)\nvalues (?, ?, ?, ?, ?)\n"
},
"f4edf4567542eaead2e0db14b0d4197c5d3c1bc02da1897b571bf63bfcb4526a": {
"describe": {
"columns": [

View file

@ -1,7 +1,9 @@
mod apps;
mod keys;
mod settings;
mod users;
pub use apps::Apps;
pub use keys::Keys;
pub use settings::Settings;
pub use users::Users;

View file

@ -0,0 +1,62 @@
use crate::error::{handle_error, Error};
use sqlx::sqlite::SqliteQueryResult;
use sqlx::types::chrono::{DateTime, Utc};
use sqlx::{FromRow, SqliteExecutor};
#[derive(FromRow)]
pub struct Apps {
pub id: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub label: String,
pub redirect_uri: String,
pub secret: String,
pub is_confidential: bool,
pub is_archived: bool,
}
impl Apps {
pub async fn insert(
conn: impl SqliteExecutor<'_>,
id: &str,
label: &str,
redirect_uri: &str,
secret: &str,
is_confidential: bool,
) -> Result<Option<()>, Error> {
let query: SqliteQueryResult = sqlx::query_file!(
"queries/apps/insert.sql",
id,
label,
redirect_uri,
secret,
is_confidential
)
.execute(conn)
.await
.map_err(handle_error)?;
Ok((query.rows_affected() == 1).then_some(()))
}
pub async fn get_one(
conn: impl SqliteExecutor<'_>,
id: &str,
redirect: &str,
) -> Result<Option<Self>, Error> {
sqlx::query_file_as!(Self, "queries/apps/get_one.sql", id, redirect)
.fetch_optional(conn)
.await
.map_err(handle_error)
}
pub async fn get_one_by_id(
conn: impl SqliteExecutor<'_>,
id: &str,
) -> Result<Option<Self>, Error> {
sqlx::query_file_as!(Self, "queries/apps/get_one_by_id.sql", id)
.fetch_optional(conn)
.await
.map_err(handle_error)
}
}

View file

@ -21,3 +21,4 @@ id = { path = "../id" }
hash = { path = "../hash" }
openid = { path = "../openid" }
jwt = { path = "../jwt" }
apps = { path = "../apps" }

View file

@ -7,3 +7,5 @@ edition = "2021"
argon2 = { version = "0.4.1" }
rand_core = { version = "0.6.4", features = ["std"] }
thiserror = { workspace = true }
nanoid = "0.4.0"
nanoid-dictionary = "0.4.3"

View file

@ -1,6 +1,8 @@
mod error;
mod hash;
mod password;
mod secret;
pub use error::Error;
pub use password::Password;
pub use secret::{Secret, SecretString};

31
crates/hash/src/secret.rs Normal file
View file

@ -0,0 +1,31 @@
use crate::error::Error;
use crate::hash::{hash, Hash};
use nanoid::nanoid;
use nanoid_dictionary::ALPHANUMERIC;
// Struct to generate the secret
pub struct SecretString(String);
const LENGTH: usize = 64;
impl Default for SecretString {
fn default() -> Self {
Self(nanoid!(LENGTH, ALPHANUMERIC))
}
}
#[derive(Debug)]
pub struct Secret(Hash);
impl Secret {
pub fn new(plain: SecretString) -> Result<Self, Error> {
Ok(Self(Hash::from_hash(hash(&plain.0)?)))
}
pub fn from_hash(hash: impl Into<String>) -> Self {
Self(Hash::from_hash(hash))
}
pub fn hash(&self) -> &str {
self.0.hash()
}
pub fn compare(&self, plain: &str) -> Result<bool, Error> {
self.0.compare(plain).map_err(Error::from)
}
}

80
crates/id/src/app.rs Normal file
View file

@ -0,0 +1,80 @@
use super::Error;
use nanoid::nanoid;
use nanoid_dictionary::NOLOOKALIKES;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
const LENGTH: usize = 10;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AppID(pub String);
impl Display for AppID {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Default for AppID {
fn default() -> Self {
Self(nanoid!(LENGTH, NOLOOKALIKES))
}
}
impl AsRef<str> for AppID {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl FromStr for AppID {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "ezidam" || s.len() == LENGTH && s.chars().all(|c| NOLOOKALIKES.contains(&c)) {
Ok(Self(s.to_string()))
} else {
Err(Error::Invalid("App"))
}
}
}
#[cfg(test)]
mod tests {
use super::{AppID, LENGTH};
use std::str::FromStr;
#[test]
fn invalid_length() {
assert!(AppID::from_str("test").is_err());
}
#[test]
fn invalid_characters() {
let value = "1I0ov5Ss2Z";
assert_eq!(value.len(), LENGTH);
assert!(AppID::from_str(value).is_err());
}
#[test]
fn valid_id() {
let value = "nqxTGaXUgn";
let id = AppID::from_str(value);
assert!(id.is_ok());
let id = id.unwrap();
assert_eq!(id.0, value);
}
#[test]
fn valid_ezidam_id() {
let value = "ezidam";
let id = AppID::from_str(value);
assert!(id.is_ok());
let id = id.unwrap();
assert_eq!(id.0, value);
}
}

View file

@ -1,3 +1,4 @@
mod app;
mod key;
mod user;
@ -10,5 +11,6 @@ pub enum Error {
Invalid(&'static str),
}
pub use app::AppID;
pub use key::KeyID;
pub use user::UserID;