Compare commits
10 Commits
4caa0c8328
...
863dccd4fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 863dccd4fd | |||
| f28eb6c422 | |||
| 0dc7dfa358 | |||
| 47c37e375d | |||
| 726092e31b | |||
| 982ab52e69 | |||
| 0ecd7c8a4c | |||
| ef722fe0d8 | |||
| 58b9923eb9 | |||
| c013f6bad8 |
66
Cargo.lock
generated
66
Cargo.lock
generated
@@ -70,11 +70,15 @@ name = "api"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"hmac",
|
||||||
|
"jwt",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -93,6 +97,18 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.83"
|
version = "0.1.83"
|
||||||
@@ -201,6 +217,12 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -222,6 +244,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -274,6 +305,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
@@ -813,6 +845,21 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jwt"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"crypto-common",
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1039,6 +1086,17 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -1616,7 +1674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
|
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1660,7 +1718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
|
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2027,6 +2085,10 @@ name = "uuid"
|
|||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ sqlx = { version = "0.8.2", features = [
|
|||||||
"macros",
|
"macros",
|
||||||
"uuid",
|
"uuid",
|
||||||
] }
|
] }
|
||||||
uuid = "1.11.0"
|
uuid = { version = "1.4.1", features = ["serde", "v4"] }
|
||||||
chrono = "0.4.39"
|
chrono = { version = "0.4.26", features = ["serde"] }
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
jwt = "0.16.0"
|
||||||
|
hmac = "0.12.1"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
|||||||
3180
data/data.txt
Normal file
3180
data/data.txt
Normal file
File diff suppressed because it is too large
Load Diff
3458
data/muscle-data.json
Normal file
3458
data/muscle-data.json
Normal file
File diff suppressed because it is too large
Load Diff
73
data/parse.ts
Normal file
73
data/parse.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
interface MuscleEntry {
|
||||||
|
muscle: string;
|
||||||
|
location: string;
|
||||||
|
origin: string;
|
||||||
|
insertion: string;
|
||||||
|
artery: string;
|
||||||
|
nerve: string;
|
||||||
|
action: string;
|
||||||
|
antagonist: string;
|
||||||
|
o: number;
|
||||||
|
ta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMuscleTable(table: string): MuscleEntry[] {
|
||||||
|
const rows = table.split(/\|- style="vertical-align: top;"/).slice(1); // Split rows and ignore header
|
||||||
|
|
||||||
|
return rows.map(row => {
|
||||||
|
const columns = row.split('\n').map(chopUntilPipe);
|
||||||
|
columns.shift();
|
||||||
|
// .replace(/\{\{[^}]+\}\}/g, '') // Remove templates like {{...}}
|
||||||
|
// .replace(/<[^>]+>/g, '') // Remove HTML tags
|
||||||
|
// .split('|').slice(1); // Split columns and ignore the first empty element
|
||||||
|
|
||||||
|
return {
|
||||||
|
muscle: extractText(columns[0]),
|
||||||
|
location: extractText(columns[1]),
|
||||||
|
origin: extractText(columns[2]),
|
||||||
|
insertion: extractText(columns[3]),
|
||||||
|
artery: extractText(columns[4]),
|
||||||
|
nerve: extractText(columns[5]),
|
||||||
|
action: extractText(columns[6]),
|
||||||
|
antagonist: extractText(columns[7] || '?'),
|
||||||
|
o: parseInt(columns[8] || '0', 10),
|
||||||
|
ta: parseInt(columns[9] || '0', 10),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(input: string): string {
|
||||||
|
return input
|
||||||
|
.replace(/\[\[(.*?)\|(.*?)\]\]/g, "$2")
|
||||||
|
.replace(/\[\[(.*?)\]\]/g, "$1")
|
||||||
|
.replace(/\<ref[^>]*>.*?<\/ref>/g, '') // Remove references like <ref>...</ref>
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function chopUntilPipe(input: string): string {
|
||||||
|
const pipeIndex = input.indexOf('|');
|
||||||
|
return pipeIndex >= 0 ? input.slice(pipeIndex + 1) : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data from file
|
||||||
|
fs.readFile('data.txt', 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error reading file:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedData = parseMuscleTable(data);
|
||||||
|
// console.log(parsedData[0]);
|
||||||
|
console.log(parsedData);
|
||||||
|
|
||||||
|
fs.writeFile('muscle-data.json', JSON.stringify(parsedData, null, 2), (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error writing file:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Data written to muscle-data.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
data/test.ts
Normal file
9
data/test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const gae = "[[Occipitalis muscle|occipitalis]]";
|
||||||
|
|
||||||
|
const gae2 = "occipitalis";
|
||||||
|
|
||||||
|
function clean(str: string) {
|
||||||
|
return str.replace(/\[\[(.*?)\|(.*?)\]\]/g, "$2");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(clean(gae));
|
||||||
109
gen.ts
Normal file
109
gen.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Define the structure of your data
|
||||||
|
interface Muscle {
|
||||||
|
colloquial_name: string;
|
||||||
|
scientific_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MuscleGroup {
|
||||||
|
major_group: string;
|
||||||
|
group_name: string;
|
||||||
|
muscles: Muscle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your JSON data
|
||||||
|
const data: MuscleGroup[] = [
|
||||||
|
{
|
||||||
|
major_group: "Upper Body",
|
||||||
|
group_name: "Chest",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Upper Pecs", scientific_name: "Clavicular Pectoralis Major" },
|
||||||
|
{ colloquial_name: "Pecs", scientific_name: "Sternal Pectoralis Major" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Upper Body",
|
||||||
|
group_name: "Back",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Lats", scientific_name: "Latissimus dorsi" },
|
||||||
|
{ colloquial_name: "Traps", scientific_name: "Trapezius" },
|
||||||
|
{ colloquial_name: "Rhomboids", scientific_name: "Rhomboideus major" },
|
||||||
|
{ colloquial_name: "Spinal Erectors", scientific_name: "Erector spinae" },
|
||||||
|
{ colloquial_name: "Teres Major", scientific_name: "Teres major" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Upper Body",
|
||||||
|
group_name: "Shoulders",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Front Delts", scientific_name: "Anterior deltoid" },
|
||||||
|
{ colloquial_name: "Rear Delts", scientific_name: "Posterior deltoid" },
|
||||||
|
{ colloquial_name: "Side Delts", scientific_name: "Lateral deltoid" },
|
||||||
|
{ colloquial_name: "Rotator Cuff", scientific_name: "Rotator cuff" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Upper Body",
|
||||||
|
group_name: "Arms",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Biceps", scientific_name: "Biceps brachii" },
|
||||||
|
{ colloquial_name: "Brachialis", scientific_name: "Brachialis" },
|
||||||
|
{ colloquial_name: "Triceps", scientific_name: "Triceps brachii" },
|
||||||
|
{ colloquial_name: "Brachioradialis", scientific_name: "Brachioradialis" },
|
||||||
|
{ colloquial_name: "Forearm Flexors/Extensors", scientific_name: "Flexor and extensor muscles of the forearm" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Core",
|
||||||
|
group_name: "Abdominals",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Abs", scientific_name: "Rectus abdominis" },
|
||||||
|
{ colloquial_name: "Transverse Abs", scientific_name: "Transversus abdominis" },
|
||||||
|
{ colloquial_name: "External Obliques", scientific_name: "Obliquus externus abdominis" },
|
||||||
|
{ colloquial_name: "Internal Obliques", scientific_name: "Obliquus internus abdominis" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Core",
|
||||||
|
group_name: "Lower Back",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "QL", scientific_name: "Quadratus lumborum" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Lower Body",
|
||||||
|
group_name: "Legs",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Quads", scientific_name: "Quadriceps femoris" },
|
||||||
|
{ colloquial_name: "Hamstrings", scientific_name: "Hamstring muscles" },
|
||||||
|
{ colloquial_name: "Glutes", scientific_name: "Gluteus maximus" },
|
||||||
|
{ colloquial_name: "Glute Med", scientific_name: "Gluteus medius" },
|
||||||
|
{ colloquial_name: "Glute Min", scientific_name: "Gluteus minimus" },
|
||||||
|
{ colloquial_name: "Adductors", scientific_name: "Adductor muscles" },
|
||||||
|
{ colloquial_name: "Abductors", scientific_name: "Abductor muscles" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
major_group: "Lower Body",
|
||||||
|
group_name: "Calves",
|
||||||
|
muscles: [
|
||||||
|
{ colloquial_name: "Gastrocs", scientific_name: "Gastrocnemius" },
|
||||||
|
{ colloquial_name: "Soleus", scientific_name: "Soleus" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate SQL INSERT statements
|
||||||
|
function generateSQL(data: MuscleGroup[]): string {
|
||||||
|
let sql = "";
|
||||||
|
|
||||||
|
data.forEach((group) => {
|
||||||
|
group.muscles.forEach((muscle) => {
|
||||||
|
sql += `INSERT INTO muscles (major_group, minor_group, name, scientific_name) VALUES ('${group.major_group}', '${group.group_name}', '${muscle.colloquial_name}', '${muscle.scientific_name}');\n`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlStatements = generateSQL(data);
|
||||||
|
console.log(sqlStatements);
|
||||||
7
migrations/20241210224609_official_exercises.sql
Normal file
7
migrations/20241210224609_official_exercises.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
ALTER TABLE exercises
|
||||||
|
ADD COLUMN official BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN author_id UUID REFERENCES users(id),
|
||||||
|
ADD COLUMN description TEXT,
|
||||||
|
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW();
|
||||||
9
migrations/20241211081452_scientific_names.sql
Normal file
9
migrations/20241211081452_scientific_names.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
ALTER TABLE muscles
|
||||||
|
ADD COLUMN scientific_name VARCHAR(255),
|
||||||
|
ADD COLUMN major_group VARCHAR(255),
|
||||||
|
ADD COLUMN minor_group VARCHAR(255) NOT NULL;
|
||||||
|
|
||||||
|
DROP TABLE muscle_bodypart_relations;
|
||||||
|
DROP TABLE body_parts;
|
||||||
@@ -11,6 +11,7 @@ pub enum Errors {
|
|||||||
Ise(anyhow::Error),
|
Ise(anyhow::Error),
|
||||||
Unimplemented,
|
Unimplemented,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
JWTExpired,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
@@ -73,6 +74,12 @@ impl IntoResponse for AppError {
|
|||||||
Errors::Unimplemented => {
|
Errors::Unimplemented => {
|
||||||
(StatusCode::NOT_IMPLEMENTED, "Not implemented").into_response()
|
(StatusCode::NOT_IMPLEMENTED, "Not implemented").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Errors::JWTExpired => (
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"JWT has expired. Please log in again.",
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/extractors/jwt.rs
Normal file
44
src/extractors/jwt.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
use anyhow::bail;
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::{FromRef, FromRequestParts},
|
||||||
|
http::{header, request::Parts},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use jwt::VerifyWithKey;
|
||||||
|
use sqlx::types::Uuid;
|
||||||
|
use util::auth::JWTClaims;
|
||||||
|
|
||||||
|
pub struct JWT(JWTClaims);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for JWT
|
||||||
|
where
|
||||||
|
AppState: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, s: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let state = AppState::from_ref(s);
|
||||||
|
|
||||||
|
let jwt_string = parts
|
||||||
|
.headers
|
||||||
|
.get(header::AUTHORIZATION)
|
||||||
|
.ok_or(AppError::Error(Errors::Unauthorized))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| AppError::Error(Errors::Unauthorized))?
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.ok_or(AppError::Error(Errors::Unauthorized))?;
|
||||||
|
|
||||||
|
let claims: JWTClaims = jwt_string
|
||||||
|
.verify_with_key(&state.jwt_key)
|
||||||
|
.map_err(|_| AppError::Error(Errors::Unauthorized))?;
|
||||||
|
|
||||||
|
Ok(JWT(claims))
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/extractors/mod.rs
Normal file
2
src/extractors/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod users;
|
||||||
|
pub mod jwt;
|
||||||
46
src/extractors/users.rs
Normal file
46
src/extractors/users.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use crate::*;
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::{FromRef, FromRequestParts},
|
||||||
|
http::{header, request::Parts},
|
||||||
|
};
|
||||||
|
|
||||||
|
use jwt::VerifyWithKey;
|
||||||
|
use sqlx::types::Uuid;
|
||||||
|
use util::auth::JWTClaims;
|
||||||
|
|
||||||
|
/// Remember, this is FromRequestParts, so it has to be ABOVE the extractors
|
||||||
|
/// that eat the entire request
|
||||||
|
pub struct UserId(pub Uuid);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for UserId
|
||||||
|
where
|
||||||
|
AppState: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, s: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let state = AppState::from_ref(s);
|
||||||
|
|
||||||
|
let jwt_string = parts
|
||||||
|
.headers
|
||||||
|
.get(header::AUTHORIZATION)
|
||||||
|
.ok_or(AppError::Error(Errors::Unauthorized))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| AppError::Error(Errors::Unauthorized))?
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.ok_or(AppError::Error(Errors::Unauthorized))?;
|
||||||
|
|
||||||
|
let claims: JWTClaims = jwt_string
|
||||||
|
.verify_with_key(&state.jwt_key)
|
||||||
|
.map_err(|_| AppError::Error(Errors::Unauthorized))?;
|
||||||
|
|
||||||
|
if claims.exp < chrono::Utc::now().timestamp() {
|
||||||
|
return Err(AppError::Error(Errors::JWTExpired));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(UserId(claims.sub))
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main.rs
18
src/main.rs
@@ -1,17 +1,20 @@
|
|||||||
use std::{net::Ipv4Addr, sync::Arc};
|
use std::{net::Ipv4Addr, sync::Arc};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
use v1::auth::AUTH_TAG;
|
use v1::{auth::AUTH_TAG, exercises::EXERCISES_TAG, muscles::MUSCLES_TAG};
|
||||||
|
|
||||||
mod v1;
|
mod v1;
|
||||||
mod structs;
|
mod structs;
|
||||||
mod state;
|
mod state;
|
||||||
mod db;
|
mod db;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod util;
|
||||||
|
mod extractors;
|
||||||
|
|
||||||
pub(crate) use anyhow::Context;
|
pub(crate) use anyhow::Context;
|
||||||
pub(crate) use axum::extract::{Json, State};
|
pub(crate) use axum::extract::{Json, State};
|
||||||
@@ -24,6 +27,8 @@ pub(crate) use utoipa::ToSchema;
|
|||||||
#[openapi(
|
#[openapi(
|
||||||
tags(
|
tags(
|
||||||
(name = AUTH_TAG, description = "Authentication API endpoints"),
|
(name = AUTH_TAG, description = "Authentication API endpoints"),
|
||||||
|
(name = EXERCISES_TAG, description = "Exercise API endpoints"),
|
||||||
|
(name = MUSCLES_TAG, description = "Muscle API endpoints"),
|
||||||
// (name = CUSTOMER_TAG, description = "Customer API endpoints"),
|
// (name = CUSTOMER_TAG, description = "Customer API endpoints"),
|
||||||
// (name = ORDER_TAG, description = "Order API endpoints")
|
// (name = ORDER_TAG, description = "Order API endpoints")
|
||||||
),
|
),
|
||||||
@@ -56,16 +61,19 @@ async fn index() -> &'static str {
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
let db = db::db().await?;
|
|
||||||
let state = state::AppState { db: Arc::new(db) };
|
|
||||||
|
|
||||||
let port: u16 = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()).parse()?;
|
let port: u16 = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()).parse()?;
|
||||||
|
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
|
||||||
|
|
||||||
|
let db = db::db().await?;
|
||||||
|
let state = state::AppState { db: Arc::new(db),
|
||||||
|
jwt_key: Hmac::new_from_slice(jwt_secret.as_bytes()).context("Failed to create HMAC")?
|
||||||
|
};
|
||||||
|
|
||||||
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
|
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
|
||||||
.routes(routes!(health_check))
|
.routes(routes!(health_check))
|
||||||
.routes(routes!(index))
|
.routes(routes!(index))
|
||||||
.with_state(state.clone())
|
.with_state(state.clone())
|
||||||
.nest("/api/v1/auth", v1::auth::router(state.clone()))
|
.nest("/api/v1", v1::router(state.clone()))
|
||||||
.split_for_parts();
|
.split_for_parts();
|
||||||
|
|
||||||
let router = router.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", api));
|
let router = router.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", api));
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use hmac::Hmac;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: DB,
|
pub db: DB,
|
||||||
|
// pub jwt_secret: String,
|
||||||
|
pub jwt_key: Hmac<Sha256>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DB = Arc<sqlx::Pool<sqlx::Postgres>>;
|
pub type DB = Arc<sqlx::Pool<sqlx::Postgres>>;
|
||||||
|
|||||||
55
src/structs/exercise_types.rs
Normal file
55
src/structs/exercise_types.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd, sqlx::Type, Deserialize, Serialize, ToSchema)]
|
||||||
|
#[sqlx(type_name = "exercise_type", rename_all = "snake_case")]
|
||||||
|
pub enum ExerciseType {
|
||||||
|
Dumbbell,
|
||||||
|
Barbell,
|
||||||
|
Bodyweight,
|
||||||
|
Machine,
|
||||||
|
Kettlebell,
|
||||||
|
ResistanceBand,
|
||||||
|
Cable,
|
||||||
|
MedicineBall,
|
||||||
|
Plyometric,
|
||||||
|
PlateLoadedMachine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExerciseType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExerciseType::Dumbbell => write!(f, "dumbbell"),
|
||||||
|
ExerciseType::Barbell => write!(f, "barbell"),
|
||||||
|
ExerciseType::Bodyweight => write!(f, "bodyweight"),
|
||||||
|
ExerciseType::Machine => write!(f, "machine"),
|
||||||
|
ExerciseType::Kettlebell => write!(f, "kettlebell"),
|
||||||
|
ExerciseType::ResistanceBand => write!(f, "resistance_band"),
|
||||||
|
ExerciseType::Cable => write!(f, "cable"),
|
||||||
|
ExerciseType::MedicineBall => write!(f, "medicine_ball"),
|
||||||
|
ExerciseType::Plyometric => write!(f, "plyometric"),
|
||||||
|
ExerciseType::PlateLoadedMachine => write!(f, "plate_loaded_machine"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for ExerciseType {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"dumbbell" => Ok(ExerciseType::Dumbbell),
|
||||||
|
"barbell" => Ok(ExerciseType::Barbell),
|
||||||
|
"bodyweight" => Ok(ExerciseType::Bodyweight),
|
||||||
|
"machine" => Ok(ExerciseType::Machine),
|
||||||
|
"kettlebell" => Ok(ExerciseType::Kettlebell),
|
||||||
|
"resistance_band" => Ok(ExerciseType::ResistanceBand),
|
||||||
|
"cable" => Ok(ExerciseType::Cable),
|
||||||
|
"medicine_ball" => Ok(ExerciseType::MedicineBall),
|
||||||
|
"plyometric" => Ok(ExerciseType::Plyometric),
|
||||||
|
"plate_loaded_machine" => Ok(ExerciseType::PlateLoadedMachine),
|
||||||
|
_ => Err(anyhow!("Invalid exercise type")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod users;
|
pub mod users;
|
||||||
|
pub mod exercise_types;
|
||||||
|
|||||||
49
src/util/auth.rs
Normal file
49
src/util/auth.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct JWTClaims {
|
||||||
|
pub sub: Uuid,
|
||||||
|
pub iat: i64,
|
||||||
|
pub exp: i64,
|
||||||
|
|
||||||
|
pub username: String,
|
||||||
|
pub real_name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JWTClaims {
|
||||||
|
pub fn new(sub: Uuid, username: String, real_name: String, email: String) -> Self {
|
||||||
|
let iat = Utc::now().timestamp();
|
||||||
|
let exp = iat + 60 * 60 * 24 * 7;
|
||||||
|
Self {
|
||||||
|
sub,
|
||||||
|
iat,
|
||||||
|
exp,
|
||||||
|
username,
|
||||||
|
real_name,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new JWT Claims with a custom expiration time. Expiration time is added to current time.
|
||||||
|
pub fn new_with_exp(
|
||||||
|
sub: Uuid,
|
||||||
|
username: String,
|
||||||
|
real_name: String,
|
||||||
|
email: String,
|
||||||
|
exp: i64,
|
||||||
|
) -> Self {
|
||||||
|
let iat = Utc::now().timestamp();
|
||||||
|
Self {
|
||||||
|
sub,
|
||||||
|
iat,
|
||||||
|
exp: iat + exp,
|
||||||
|
username,
|
||||||
|
real_name,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod auth;
|
||||||
48
src/v1/auth/login.rs
Normal file
48
src/v1/auth/login.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use crate::*;
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{PasswordHash, PasswordVerifier},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
use jwt::SignWithKey;
|
||||||
|
use sqlx::query;
|
||||||
|
use util::auth::JWTClaims;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct LoginBody {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login
|
||||||
|
#[utoipa::path(post, path = "/login", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
||||||
|
pub async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(body): Json<LoginBody>,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let user = query!(
|
||||||
|
"SELECT id, real_name, username, email, password_hash, is_admin, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE username = $1",
|
||||||
|
body.username,
|
||||||
|
)
|
||||||
|
.fetch_one(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
let hash = PasswordHash::new(&body.password).expect("Password hashing failed");
|
||||||
|
|
||||||
|
if !argon2
|
||||||
|
.verify_password(body.password.as_bytes(), &hash)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(AppError::Error(Errors::Unauthorized));
|
||||||
|
}
|
||||||
|
|
||||||
|
let claims = JWTClaims::new(user.id, user.username, user.real_name, user.email);
|
||||||
|
|
||||||
|
let token_str = claims
|
||||||
|
.sign_with_key(&state.jwt_key)
|
||||||
|
.context("Failed to sign JWT")?;
|
||||||
|
|
||||||
|
Ok(token_str)
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
use crate::state::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
pub(super) use super::*;
|
pub(super) use super::*;
|
||||||
|
|
||||||
pub mod signup;
|
pub mod signup;
|
||||||
|
pub mod login;
|
||||||
|
|
||||||
pub const AUTH_TAG: &str = "auth";
|
pub const AUTH_TAG: &str = "auth";
|
||||||
|
|
||||||
pub fn router(state: AppState) -> OpenApiRouter {
|
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||||
OpenApiRouter::new()
|
OpenApiRouter::new()
|
||||||
.routes(routes!(signup::signup))
|
.routes(routes!(signup::signup))
|
||||||
|
.routes(routes!(login::login))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
|
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
|
||||||
|
use jwt::SignWithKey;
|
||||||
|
use util::auth::JWTClaims;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SignupBody {
|
pub struct SignupBody {
|
||||||
real_name: String,
|
real_name: String,
|
||||||
@@ -10,11 +18,19 @@ pub struct SignupBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sign up
|
/// Sign up
|
||||||
#[utoipa::path(get, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
#[utoipa::path(post, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
||||||
pub async fn signup(
|
pub async fn signup(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(body): Json<SignupBody>,
|
Json(body): Json<SignupBody>,
|
||||||
) -> Result<String, AppError> {
|
) -> Result<String, AppError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
let hash = argon2
|
||||||
|
.hash_password(body.password.as_bytes(), &salt)
|
||||||
|
.expect("Password hashing failed")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let user = query!(
|
let user = query!(
|
||||||
"INSERT INTO users (real_name, username, email, password_hash)
|
"INSERT INTO users (real_name, username, email, password_hash)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
@@ -22,10 +38,16 @@ pub async fn signup(
|
|||||||
body.real_name,
|
body.real_name,
|
||||||
body.username,
|
body.username,
|
||||||
body.email,
|
body.email,
|
||||||
body.password,
|
hash
|
||||||
)
|
)
|
||||||
.fetch_one(&*state.db)
|
.fetch_one(&*state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user.id.to_string())
|
let claims = JWTClaims::new(user.id, user.username, user.real_name, user.email);
|
||||||
|
|
||||||
|
let token_str = claims
|
||||||
|
.sign_with_key(&state.jwt_key)
|
||||||
|
.context("Failed to sign JWT")?;
|
||||||
|
|
||||||
|
Ok(token_str)
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/v1/exercises/create.rs
Normal file
57
src/v1/exercises/create.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use extractors::users::UserId;
|
||||||
|
use sqlx::query;
|
||||||
|
use structs::exercise_types::ExerciseType;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct CreateExerciseBody {
|
||||||
|
name: String,
|
||||||
|
// todo: make this an enum
|
||||||
|
exercise_type: ExerciseType,
|
||||||
|
description: String,
|
||||||
|
body_parts: Vec<String>,
|
||||||
|
primary_muscles: Vec<String>,
|
||||||
|
secondary_muscles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, sqlx::Type, PartialEq, sqlx::FromRow)]
|
||||||
|
pub struct Exercise {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub exercise_type: ExerciseType,
|
||||||
|
pub description: String,
|
||||||
|
pub author_id: Uuid,
|
||||||
|
pub official: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(post, path = "/create", responses((status = OK, body = String)), tag = super::EXERCISES_TAG)]
|
||||||
|
pub async fn create(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user): UserId,
|
||||||
|
Json(body): Json<CreateExerciseBody>,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let is_admin = query!("SELECT is_admin FROM users WHERE id = $1", user)
|
||||||
|
.fetch_one(&*state.db)
|
||||||
|
.await?
|
||||||
|
.is_admin;
|
||||||
|
|
||||||
|
query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO exercises (name, exercise_type, official, author_id, description)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
"#,
|
||||||
|
body.name,
|
||||||
|
body.exercise_type as ExerciseType,
|
||||||
|
is_admin,
|
||||||
|
user,
|
||||||
|
body.description
|
||||||
|
)
|
||||||
|
.fetch_one(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
13
src/v1/exercises/mod.rs
Normal file
13
src/v1/exercises/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub(super) use super::*;
|
||||||
|
|
||||||
|
pub mod create;
|
||||||
|
|
||||||
|
pub const EXERCISES_TAG: &str = "exercises";
|
||||||
|
|
||||||
|
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||||
|
OpenApiRouter::new()
|
||||||
|
.routes(routes!(create::create))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
pub(super) use utoipa_axum::router::OpenApiRouter;
|
pub(super) use utoipa_axum::router::OpenApiRouter;
|
||||||
pub(super) use utoipa_axum::routes;
|
pub(super) use utoipa_axum::routes;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod exercises;
|
||||||
|
pub mod muscles;
|
||||||
|
|
||||||
|
pub fn router(state: AppState) -> OpenApiRouter {
|
||||||
|
OpenApiRouter::new()
|
||||||
|
.with_state(state.clone())
|
||||||
|
.nest("/auth", auth::router(state.clone()))
|
||||||
|
.nest("/exercises", exercises::router(state.clone()))
|
||||||
|
.nest("/muscles", muscles::router(state.clone()))
|
||||||
|
}
|
||||||
|
|||||||
40
src/v1/muscles/create.rs
Normal file
40
src/v1/muscles/create.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use extractors::users::UserId;
|
||||||
|
use sqlx::query;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct CreateMuscleBody {
|
||||||
|
name: String,
|
||||||
|
scientific_name: String,
|
||||||
|
major_group: String,
|
||||||
|
minor_group: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(post, path = "/create", responses((status = OK, body = String)), tag = super::MUSCLES_TAG)]
|
||||||
|
pub async fn create(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Json(body): Json<CreateMuscleBody>,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let is_admin = query!("SELECT is_admin FROM users WHERE id = $1", user_id)
|
||||||
|
.fetch_one(&*state.db)
|
||||||
|
.await?
|
||||||
|
.is_admin;
|
||||||
|
|
||||||
|
if !is_admin {
|
||||||
|
return Err(AppError::Error(Errors::Unauthorized));
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = query!(
|
||||||
|
"INSERT INTO muscles (name, scientific_name, major_group, minor_group) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||||
|
body.name,
|
||||||
|
body.scientific_name,
|
||||||
|
body.major_group,
|
||||||
|
body.minor_group,
|
||||||
|
)
|
||||||
|
.fetch_one(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(out.id.to_string())
|
||||||
|
}
|
||||||
23
src/v1/muscles/get_all.rs
Normal file
23
src/v1/muscles/get_all.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// use extractors::users::UserId;
|
||||||
|
use sqlx::query_as;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct Muscle {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
scientific_name: Option<String>,
|
||||||
|
major_group: Option<String>,
|
||||||
|
minor_group: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(get, path = "/all", responses((status = OK, body = Muscle)), tag = super::MUSCLES_TAG)]
|
||||||
|
pub async fn get_all(State(state): State<AppState>) -> Result<Json<Vec<Muscle>>, AppError> {
|
||||||
|
let muscles = query_as!(Muscle, "SELECT * FROM muscles")
|
||||||
|
.fetch_all(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(muscles))
|
||||||
|
}
|
||||||
|
|
||||||
15
src/v1/muscles/mod.rs
Normal file
15
src/v1/muscles/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub(super) use super::*;
|
||||||
|
|
||||||
|
pub mod create;
|
||||||
|
pub mod get_all;
|
||||||
|
|
||||||
|
pub const MUSCLES_TAG: &str = "muscles";
|
||||||
|
|
||||||
|
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||||
|
OpenApiRouter::new()
|
||||||
|
.routes(routes!(create::create))
|
||||||
|
.routes(routes!(get_all::get_all))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user