feat: admin role
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "INSERT INTO users (id, email, username, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
|
"query": "INSERT INTO users (id, email, username, password_hash, created_at, role) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 5
|
"Right": 6
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "21751a2efb5fc58f5c1057f332f537419b7067ff665a03e248e6ea0fe7b6919b"
|
"hash": "0acf3515a4d1bef7a2458d878481ba3b11c9f252311579f7daba335f70d78f07"
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT COUNT(*) FROM reviews WHERE user_id = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "COUNT(*)",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "0cd1a7b7255a0ee753deffab7cbb48027d22900a570b98a636c780cb3e2efd23"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id, email, username, password_hash FROM users WHERE id = ?",
|
"query": "SELECT id, email, username, password_hash, role FROM users WHERE username = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -22,6 +22,11 @@
|
|||||||
"name": "password_hash",
|
"name": "password_hash",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -31,8 +36,9 @@
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "e51c6da943bd326a09632aa0cfd30c4f15ca554e229778b6cfa04889b7231b36"
|
"hash": "319b5d09824809a971f6c9546dde6389a0aca5a732531bb9ca9b99675d0a89f4"
|
||||||
}
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n WHERE r.user_id = ?\n ORDER BY r.rating DESC, r.watched_at DESC\n LIMIT ? OFFSET ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "external_metadata_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "title",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "release_year",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "director",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "poster_path",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "review_id",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "movie_id",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_id",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rating",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comment",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "watched_at",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "remote_actor_url",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "4ad3b7d450e46e8d4982a0517c7345b386fc7dc6cf3e72c924e2f5da49dfb469"
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n WHERE r.user_id = ?\n ORDER BY r.watched_at DESC\n LIMIT ? OFFSET ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "external_metadata_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "title",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "release_year",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "director",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "poster_path",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "review_id",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "movie_id",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_id",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rating",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comment",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "watched_at",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "remote_actor_url",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "4c235060b78cc5c73e0400b39472467a36d3920ff4c020f5461b2cdb04361888"
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url,\n COALESCE(u.email, r.remote_actor_url) AS \"user_email!: String\"\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n LEFT JOIN users u ON u.id = r.user_id\n ORDER BY r.watched_at DESC\n LIMIT ? OFFSET ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "external_metadata_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "title",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "release_year",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "director",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "poster_path",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "review_id",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "movie_id",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_id",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rating",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comment",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "watched_at",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "remote_actor_url",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_email!: String",
|
|
||||||
"ordinal": 14,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "70843058606802f0958d216a47473f78b29854518c48c2da3cc11a94d6d37bb1"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id, email, username, password_hash FROM users WHERE email = ?",
|
"query": "SELECT id, email, username, password_hash, role FROM users WHERE id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -22,6 +22,11 @@
|
|||||||
"name": "password_hash",
|
"name": "password_hash",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -31,8 +36,9 @@
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "4eeae6aa887319cab4a9fd673c3a75dec1e6681739d722481233a3d9e7a01955"
|
"hash": "79af0324db4d0b8e4bd66e12816583598865630241deffbb8b4175fa7a755099"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id, email, username, password_hash FROM users WHERE username = ?",
|
"query": "SELECT id, email, username, password_hash, role FROM users WHERE email = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -22,6 +22,11 @@
|
|||||||
"name": "password_hash",
|
"name": "password_hash",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -31,8 +36,9 @@
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "0e36417429360e7e332f60768c2283a78b18c57cb72978921ccc0df963a756ba"
|
"hash": "c43249558d535343e387586fa00499fe813b18b9e7d8d241462f0689969dc64f"
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,9 @@ RUN sqlite3 /build/dev.db \
|
|||||||
sqlite3 /build/dev.db \
|
sqlite3 /build/dev.db \
|
||||||
< crates/adapters/sqlite/migrations/0005_activitypub_v2.sql && \
|
< crates/adapters/sqlite/migrations/0005_activitypub_v2.sql && \
|
||||||
sqlite3 /build/dev.db \
|
sqlite3 /build/dev.db \
|
||||||
< crates/adapters/sqlite/migrations/0006_follower_activity_id.sql
|
< crates/adapters/sqlite/migrations/0006_follower_activity_id.sql && \
|
||||||
|
sqlite3 /build/dev.db \
|
||||||
|
< crates/adapters/sqlite/migrations/0007_user_role.sql
|
||||||
|
|
||||||
ENV DATABASE_URL=sqlite:///build/dev.db
|
ENV DATABASE_URL=sqlite:///build/dev.db
|
||||||
|
|
||||||
|
|||||||
1
crates/adapters/sqlite/migrations/0007_user_role.sql
Normal file
1
crates/adapters/sqlite/migrations/0007_user_role.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'standard';
|
||||||
@@ -5,7 +5,7 @@ use sqlx::SqlitePool;
|
|||||||
use super::models::UserSummaryRow;
|
use super::models::UserSummaryRow;
|
||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::User,
|
models::{User, UserRole},
|
||||||
ports::UserRepository,
|
ports::UserRepository,
|
||||||
value_objects::{Email, PasswordHash, UserId, Username},
|
value_objects::{Email, PasswordHash, UserId, Username},
|
||||||
};
|
};
|
||||||
@@ -24,11 +24,19 @@ impl SqliteUserRepository {
|
|||||||
DomainError::InfrastructureError("Database operation failed".into())
|
DomainError::InfrastructureError("Database operation failed".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_role(s: &str) -> UserRole {
|
||||||
|
match s {
|
||||||
|
"admin" => UserRole::Admin,
|
||||||
|
_ => UserRole::Standard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn row_to_user(
|
fn row_to_user(
|
||||||
id_str: String,
|
id_str: String,
|
||||||
email_str: String,
|
email_str: String,
|
||||||
username_str: String,
|
username_str: String,
|
||||||
hash_str: String,
|
hash_str: String,
|
||||||
|
role: UserRole,
|
||||||
) -> Result<User, DomainError> {
|
) -> Result<User, DomainError> {
|
||||||
let id = uuid::Uuid::parse_str(&id_str)
|
let id = uuid::Uuid::parse_str(&id_str)
|
||||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||||
@@ -43,6 +51,7 @@ impl SqliteUserRepository {
|
|||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
hash,
|
hash,
|
||||||
|
role,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +61,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
|
||||||
let email_str = email.value();
|
let email_str = email.value();
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
"SELECT id, email, username, password_hash FROM users WHERE email = ?",
|
"SELECT id, email, username, password_hash, role FROM users WHERE email = ?",
|
||||||
email_str
|
email_str
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
@@ -65,6 +74,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
r.email,
|
r.email,
|
||||||
r.username,
|
r.username,
|
||||||
r.password_hash,
|
r.password_hash,
|
||||||
|
Self::parse_role(&r.role),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -73,7 +83,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
|
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
|
||||||
let username_str = username.value();
|
let username_str = username.value();
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
"SELECT id, email, username, password_hash FROM users WHERE username = ?",
|
"SELECT id, email, username, password_hash, role FROM users WHERE username = ?",
|
||||||
username_str
|
username_str
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
@@ -86,6 +96,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
r.email,
|
r.email,
|
||||||
r.username,
|
r.username,
|
||||||
r.password_hash,
|
r.password_hash,
|
||||||
|
Self::parse_role(&r.role),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -111,9 +122,13 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
let hash = user.password_hash().value();
|
let hash = user.password_hash().value();
|
||||||
let created_at = Utc::now().to_rfc3339();
|
let created_at = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
let role = match user.role() {
|
||||||
|
UserRole::Admin => "admin",
|
||||||
|
UserRole::Standard => "standard",
|
||||||
|
};
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO users (id, email, username, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO users (id, email, username, password_hash, created_at, role) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
id, email, username, hash, created_at
|
id, email, username, hash, created_at, role
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await
|
.await
|
||||||
@@ -125,7 +140,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
|
||||||
let id_str = id.value().to_string();
|
let id_str = id.value().to_string();
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
"SELECT id, email, username, password_hash FROM users WHERE id = ?",
|
"SELECT id, email, username, password_hash, role FROM users WHERE id = ?",
|
||||||
id_str
|
id_str
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
@@ -138,6 +153,7 @@ impl UserRepository for SqliteUserRepository {
|
|||||||
r.email,
|
r.email,
|
||||||
r.username,
|
r.username,
|
||||||
r.password_hash,
|
r.password_hash,
|
||||||
|
Self::parse_role(&r.role),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
@@ -172,7 +188,7 @@ mod tests {
|
|||||||
async fn setup() -> (SqlitePool, SqliteUserRepository) {
|
async fn setup() -> (SqlitePool, SqliteUserRepository) {
|
||||||
let pool = SqlitePool::connect(":memory:").await.unwrap();
|
let pool = SqlitePool::connect(":memory:").await.unwrap();
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL)"
|
"CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'standard')"
|
||||||
)
|
)
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use domain::models::ExportFormat;
|
use domain::models::{ExportFormat, UserRole};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct LogReviewCommand {
|
pub struct LogReviewCommand {
|
||||||
@@ -30,6 +30,7 @@ pub struct RegisterCommand {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub role: UserRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeleteReviewCommand {
|
pub struct DeleteReviewCommand {
|
||||||
|
|||||||
@@ -41,6 +41,6 @@ pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), Domai
|
|||||||
|
|
||||||
let hash = ctx.password_hasher.hash(&cmd.password).await?;
|
let hash = ctx.password_hasher.hash(&cmd.password).await?;
|
||||||
ctx.user_repository
|
ctx.user_repository
|
||||||
.save(&User::new(email, username, hash))
|
.save(&User::new(email, username, hash, cmd.role))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::default;
|
||||||
|
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -254,21 +256,35 @@ impl ReviewHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub enum UserRole {
|
||||||
|
#[default]
|
||||||
|
Standard,
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
id: UserId,
|
id: UserId,
|
||||||
email: Email,
|
email: Email,
|
||||||
username: Username,
|
username: Username,
|
||||||
password_hash: PasswordHash,
|
password_hash: PasswordHash,
|
||||||
|
role: UserRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn new(email: Email, username: Username, password_hash: PasswordHash) -> Self {
|
pub fn new(
|
||||||
|
email: Email,
|
||||||
|
username: Username,
|
||||||
|
password_hash: PasswordHash,
|
||||||
|
role: UserRole,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: UserId::generate(),
|
id: UserId::generate(),
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
password_hash,
|
password_hash,
|
||||||
|
role,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,12 +293,14 @@ impl User {
|
|||||||
email: Email,
|
email: Email,
|
||||||
username: Username,
|
username: Username,
|
||||||
password_hash: PasswordHash,
|
password_hash: PasswordHash,
|
||||||
|
role: UserRole,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
password_hash,
|
password_hash,
|
||||||
|
role,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +320,9 @@ impl User {
|
|||||||
pub fn password_hash(&self) -> &PasswordHash {
|
pub fn password_hash(&self) -> &PasswordHash {
|
||||||
&self.password_hash
|
&self.password_hash
|
||||||
}
|
}
|
||||||
|
pub fn role(&self) -> &UserRole {
|
||||||
|
&self.role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRef, FromRequestParts},
|
extract::{FromRef, FromRequestParts},
|
||||||
http::{header, header::AUTHORIZATION, request::Parts},
|
http::{StatusCode, header, header::AUTHORIZATION, request::Parts},
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
use domain::{errors::DomainError, value_objects::UserId};
|
use domain::{errors::DomainError, value_objects::UserId};
|
||||||
@@ -91,6 +91,33 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AdminUser(pub UserId);
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for AdminUser
|
||||||
|
where
|
||||||
|
AppState: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = axum::response::Response;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let app_state = AppState::from_ref(state);
|
||||||
|
let RequiredCookieUser(user_id) =
|
||||||
|
RequiredCookieUser::from_request_parts(parts, state).await?;
|
||||||
|
let user = app_state
|
||||||
|
.app_ctx
|
||||||
|
.user_repository
|
||||||
|
.find_by_id(&user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())?
|
||||||
|
.ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?;
|
||||||
|
match user.role() {
|
||||||
|
domain::models::UserRole::Admin => Ok(AdminUser(user_id)),
|
||||||
|
_ => Err(StatusCode::FORBIDDEN.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ pub mod html {
|
|||||||
email: form.email,
|
email: form.email,
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
|
role: domain::models::UserRole::Standard,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1181,6 +1182,7 @@ pub mod api {
|
|||||||
email: req.email,
|
email: req.email,
|
||||||
username: req.username,
|
username: req.username,
|
||||||
password: req.password,
|
password: req.password,
|
||||||
|
role: domain::models::UserRole::Standard,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user