This commit is contained in:
Franklin 2023-09-20 16:23:29 -04:00
commit 94502b0b54
34 changed files with 2133 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/user-lib.iml" filepath="$PROJECT_DIR$/.idea/user-lib.iml" />
</modules>
</component>
</project>

11
.idea/user-lib.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1846
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "user-lib"
version = "0.1.0"
edition = "2021"
authors = ["Franklin E. Blanco"]
description = "A library to add secure user authentication to any service."
license = "MIT"
readme = "README.md"
repository = "https://github.com/franklinblanco/user-lib.git"
[lib]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "postgres", "chrono" ] }
chrono = { version = "0.4", features = [ "serde" ] }
ring = "0.16.20"
data-encoding = "2.3.2"
futures-util = "0.3"
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] }

14
Readme.md Normal file
View File

@ -0,0 +1,14 @@
# User-lib
by Franklin Blanco
This library is my attempt at developing a recyclable utility for different projects, and not having to setup an authentication microservice each time I start a new project.
### Must use Postgres!
## How to use?
Setup:
- Add this library to your Cargo.toml
- Copy the migrations from the migrations folder inside this library into your migrations
- Add the user_lib::setup() function to your main. Make sure to pass it a PgPool
- Add the user_lib::routes to your actix_web server (register, authenticate, change_password, refresh_token)
Usage:

8
migrations/1_user.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS user (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
password TEXT NOT NULL,
salt TEXT NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
last_updated TIMESTAMPTZ NOT NULL,
)

8
migrations/2_token.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS token (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
auth_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
last_updated TIMESTAMPTZ NOT NULL
)

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS credential (
user_id INT NOT NULL,
credential_type VARCHAR NOT NULL,
credential VARCHAR NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
last_updated TIMESTAMPTZ NOT NULL,
PRIMARY KEY(user_id, credential_type)
);

View File

@ -0,0 +1,4 @@
DELETE FROM token
where
TIMESTAMPDIFF(DAY, NOW(), last_updated) > ? AND
TIMESTAMPDIFF(DAY, NOW(), time_created) > ?

View File

@ -0,0 +1,3 @@
SELECT *
FROM token
WHERE user_id = ?

View File

@ -0,0 +1,3 @@
INSERT INTO token
(id, user_id, time_created, last_updated, auth_token, refresh_token)
values (NULL, ?, NOW(), NOW(), ?, ?)

View File

@ -0,0 +1,3 @@
UPDATE token
SET last_updated = NOW(), auth_token = ?, refresh_token = ?
WHERE id = ?

View File

@ -0,0 +1,5 @@
SELECT *
FROM user
WHERE user.credential = ? AND
user.credential_type = ? AND
user.app = ?

View File

@ -0,0 +1,3 @@
SELECT *
FROM user
WHERE user.id = ?

View File

@ -0,0 +1,3 @@
INSERT INTO user
(id, time_created, last_updated, app, credential, credential_type, name, password, salt) values
($1, $2, $3, $4, $5, $6, $7, $8, $9)

View File

1
src/dao/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod pg_queries;

0
src/dao/pg_queries.rs Normal file
View File

24
src/domain/credential.rs Normal file
View File

@ -0,0 +1,24 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
/// Is used in the user struct to signal what type of credential will be used in the credential Column.
/// Defaults to email.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum CredentialType {
PhoneNumber,
#[default]
Email,
Username,
}
/// Can only have one per user per cred_type
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")]
pub struct Credential {
pub user_id: i32,
pub credential_type: CredentialType,
pub credential: String,
pub time_created: DateTime<Utc>,
pub last_updated: DateTime<Utc>,
}

14
src/domain/error.rs Normal file
View File

@ -0,0 +1,14 @@
use std::fmt::Display;
/// Used to return a simple error from FromStr implementations
#[derive(Debug)]
pub struct FromStrError;
impl Display for FromStrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error parsing string into value. FromStrError.")
}
}
impl std::error::Error for FromStrError {}

View File

@ -0,0 +1,58 @@
use std::{str::FromStr, fmt::Display};
use sqlx::{Postgres, postgres::{PgValueRef, PgArgumentBuffer, PgTypeInfo}, error::BoxDynError, encode::IsNull};
use crate::domain::{credential::CredentialType, error::FromStrError};
impl FromStr for CredentialType {
type Err = FromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"PhoneNumber" => Ok(Self::PhoneNumber),
"Email" => Ok(Self::Email),
"Username" => Ok(Self::Username),
_ => Err(FromStrError)
}
}
}
impl Display for CredentialType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CredentialType::PhoneNumber => write!(f, "PhoneNumber"),
CredentialType::Email => write!(f, "Email"),
CredentialType::Username => write!(f, "Username"),
}
}
}
//
// Sqlx implementations so that the CredentialType enum can be inserted & retrieved from the database
//
impl sqlx::Encode<'_, Postgres> for CredentialType {
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
let binding = self.to_string();
<&str as sqlx::Encode<Postgres>>::encode(&binding, buf)
}
}
impl sqlx::Decode<'_, Postgres> for CredentialType {
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
let column = value.as_str()?;
match Self::from_str(column) {
Ok(listing_state) => Ok(listing_state),
Err(error) => Err(Box::new(error)),
}
}
}
impl sqlx::Type<Postgres> for CredentialType {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("VARCHAR")
}
fn compatible(ty: &<Postgres as sqlx::Database>::TypeInfo) -> bool {
*ty == Self::type_info()
}
}

1
src/domain/impls/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod credential;

5
src/domain/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod credential;
pub mod token;
pub mod user;
pub mod impls;
pub mod error;

21
src/domain/token.rs Normal file
View File

@ -0,0 +1,21 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
#[derive(FromRow)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Token {
#[serde(skip_serializing, skip_deserializing)]
pub id: i32,
#[serde(rename = "userId")]
pub user_id: i32,
#[serde(rename = "authToken")]
pub auth_token: String,
#[serde(rename = "refreshToken")]
pub refresh_token: String,
#[serde(rename = "timeCreated")]
pub time_created: DateTime<Utc>,
#[serde(rename = "lastUpdated")]
pub last_updated: DateTime<Utc>,
}

20
src/domain/user.rs Normal file
View File

@ -0,0 +1,20 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
use super::credential::CredentialType;
#[derive(FromRow)]
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct User {
pub id: i32,
pub name: String,
#[serde(skip_serializing, skip_deserializing)]
pub password: String,
#[serde(skip_serializing, skip_deserializing)]
pub salt: String,
#[serde(rename = "timeCreated")]
pub time_created: DateTime<Utc>,
#[serde(rename = "lastUpdated")]
pub last_updated: DateTime<Utc>,
}

1
src/dto/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod users;

14
src/dto/users.rs Normal file
View File

@ -0,0 +1,14 @@
pub struct UsernameUserLoginPayload {
pub username: String,
pub password: String,
}
pub struct EmailUserLoginPayload {
pub email: String,
pub password: String,
}
pub struct PhoneNumberUserLoginPayload {
pub phone_number: String,
pub password: String,
}

5
src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod dao;
pub mod service;
pub mod utils;
pub mod domain;
pub mod dto;

0
src/service/mod.rs Normal file
View File

0
src/utils/mod.rs Normal file
View File