added basic admin tools
i can ban users now!
This commit is contained in:
parent
8d4042e31a
commit
dc7eb57640
@ -1,92 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires\n FROM users WHERE username = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "bio",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "avatar_url",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "email_verified!",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "verification_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "verification_token_expires",
|
||||
"ordinal": 10,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "reset_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "03e12aecfba017ed94a3dcd86e82454d8111c884d2133090ac76c341317f4a9a"
|
||||
}
|
||||
20
.sqlx/query-2a0b3b13b130ee33ac214dcd25fb988b0b5ad8756eacf65a770a733e369d048a.json
generated
Normal file
20
.sqlx/query-2a0b3b13b130ee33ac214dcd25fb988b0b5ad8756eacf65a770a733e369d048a.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as 'count!' FROM threads WHERE hidden = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2a0b3b13b130ee33ac214dcd25fb988b0b5ad8756eacf65a770a733e369d048a"
|
||||
}
|
||||
74
.sqlx/query-378ae2f1e9de949f1cac3c0f463e475a7769a2b6d25e0a091a92393a89e55642.json
generated
Normal file
74
.sqlx/query-378ae2f1e9de949f1cac3c0f463e475a7769a2b6d25e0a091a92393a89e55642.json
generated
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, body, author_id, thread_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by, ban_message\n FROM posts WHERE thread_id = ? AND hidden = FALSE ORDER BY created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "author_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "thread_id",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_message",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "378ae2f1e9de949f1cac3c0f463e475a7769a2b6d25e0a091a92393a89e55642"
|
||||
}
|
||||
68
.sqlx/query-3811471be7d9676565e0967283ffb802271d2d211a04aa30eff969001869a983.json
generated
Normal file
68
.sqlx/query-3811471be7d9676565e0967283ffb802271d2d211a04aa30eff969001869a983.json
generated
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by\n FROM threads WHERE author_id = ? AND hidden = FALSE ORDER BY updated_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "author_id",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3811471be7d9676565e0967283ffb802271d2d211a04aa30eff969001869a983"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires\n FROM users WHERE email = ?",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users WHERE reset_token = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -67,13 +67,48 @@
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@ -85,8 +120,15 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a4c396e317e9a596629e91d1deefa19df638161c6a7a87427cb1f298aaa545ff"
|
||||
"hash": "4a08e2a61580e024d81ac54ab8cb00600131a1ba03431d07885752b41fef4cbb"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at\n FROM threads WHERE author_id = ? ORDER BY updated_at DESC",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by\n FROM threads WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -32,6 +32,21 @@
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -43,8 +58,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ce4cb40fa25111c00d2f4575ce0382de37f2b57bcfccc5f05b4a59ace6a60872"
|
||||
"hash": "5ea6f4356c6d73485f869e8190580d601223d16e320c54fa7e42049e970e32fb"
|
||||
}
|
||||
20
.sqlx/query-5f35fdcf88f734bf92f66012bb7bf9ceb02e4e91dfccae7b16f4365fa46db709.json
generated
Normal file
20
.sqlx/query-5f35fdcf88f734bf92f66012bb7bf9ceb02e4e91dfccae7b16f4365fa46db709.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as 'count!' FROM users",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5f35fdcf88f734bf92f66012bb7bf9ceb02e4e91dfccae7b16f4365fa46db709"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires\n FROM users WHERE verification_token = ?",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -67,6 +67,41 @@
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -85,8 +120,15 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ebb6edb02bb7b9e2f4d7acd344790f3bdcae506ad1c604dd954b6f091e4c66f2"
|
||||
"hash": "6dca909c5e2893eededfb9e2eac10742cbbcad65cc910fa4165f07d376c0b2c2"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at\n FROM threads ORDER BY updated_at DESC",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by\n FROM threads ORDER BY updated_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -32,6 +32,21 @@
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -43,8 +58,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c40c4444269dd206d0dc0618d8a322a76e5f77c663cdbb433d7985fb5528af67"
|
||||
"hash": "734e50b9a4ed85367e9888ea1e4ac602a30efc6a3c5e40e9723a56bd65db8aad"
|
||||
}
|
||||
134
.sqlx/query-77440cead9d333038d59388bc46f548cab89f0b656abfb24f8385675eded8132.json
generated
Normal file
134
.sqlx/query-77440cead9d333038d59388bc46f548cab89f0b656abfb24f8385675eded8132.json
generated
Normal file
@ -0,0 +1,134 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "bio",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "avatar_url",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "email_verified!",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "verification_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "verification_token_expires",
|
||||
"ordinal": 10,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "reset_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "77440cead9d333038d59388bc46f548cab89f0b656abfb24f8385675eded8132"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at\n FROM threads WHERE id = ?",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by\n FROM threads WHERE id = ? AND hidden = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -32,6 +32,21 @@
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -43,8 +58,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6549001e2a522da8788d77d915ca372b07872cd763a48f3a035e7e5c5a70c879"
|
||||
"hash": "9a393c4019349783b7267a7447514fa217e4133b6774a4cdb1b45347c306971c"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires\n FROM users WHERE reset_token = ?",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users WHERE email = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -67,13 +67,48 @@
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@ -85,8 +120,15 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1fdfa7429dc20e693f147c5184e7b2299821d822590a978bed7bdf0f0f7917e3"
|
||||
"hash": "b0fc43ae344ae0bda14d631071271b34f6c9e307c675b920fb0ccac9bba0a797"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, body, author_id, thread_id, created_at, updated_at\n FROM posts WHERE thread_id = ? ORDER BY created_at ASC",
|
||||
"query": "SELECT id, body, author_id, thread_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by, ban_message\n FROM posts WHERE thread_id = ? ORDER BY created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -32,6 +32,26 @@
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_message",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@ -43,8 +63,12 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4a1b2d9c1c66741913ff3a9ddf50d260d8bb01fb891509bf95561d72c3376f98"
|
||||
"hash": "db743bec93d5243e994d5a85a60ec3c896432fede4d849c10d1b02813eb84428"
|
||||
}
|
||||
68
.sqlx/query-e3f0046e263f5a5029bdcf1d7f99d5ef094a447b85376822a9e8052e51372087.json
generated
Normal file
68
.sqlx/query-e3f0046e263f5a5029bdcf1d7f99d5ef094a447b85376822a9e8052e51372087.json
generated
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, title, body, author_id, created_at, updated_at,\n hidden AS \"hidden!\", hidden_at, hidden_by\n FROM threads WHERE hidden = FALSE ORDER BY updated_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "author_id",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden!",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "hidden_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "hidden_by",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e3f0046e263f5a5029bdcf1d7f99d5ef094a447b85376822a9e8052e51372087"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires\n FROM users WHERE id = ?",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users WHERE username = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -67,13 +67,48 @@
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@ -85,8 +120,15 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b949d7b0995654aa1b68bb5b993e3af267b206d42f0342c0ff9cfc15028a862b"
|
||||
"hash": "e8dc3829a39a405465c4d13b92a5533cd8a6fc7fdc9b92c6fed993e20a3fe7d5"
|
||||
}
|
||||
134
.sqlx/query-ea192066870fda12a9f26c27556d8c2651334145a8cc221a3ff83600c94c8c0e.json
generated
Normal file
134
.sqlx/query-ea192066870fda12a9f26c27556d8c2651334145a8cc221a3ff83600c94c8c0e.json
generated
Normal file
@ -0,0 +1,134 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!\", username, email, password, bio, avatar_url, role, created_at,\n email_verified AS \"email_verified!\",\n verification_token,\n verification_token_expires,\n reset_token,\n reset_token_expires,\n banned AS \"banned!\",\n banned_at,\n ban_reason,\n ban_length,\n unban_at,\n banned_by,\n ban_post_content\n FROM users WHERE verification_token = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "bio",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "avatar_url",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 7,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "email_verified!",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "verification_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "verification_token_expires",
|
||||
"ordinal": 10,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "reset_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "reset_token_expires",
|
||||
"ordinal": 12,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned!",
|
||||
"ordinal": 13,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "banned_at",
|
||||
"ordinal": 14,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "ban_reason",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_length",
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "unban_at",
|
||||
"ordinal": 17,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "banned_by",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ban_post_content",
|
||||
"ordinal": 19,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ea192066870fda12a9f26c27556d8c2651334145a8cc221a3ff83600c94c8c0e"
|
||||
}
|
||||
20
.sqlx/query-ee0e90b6ddb5dcc454f47b1326bf5a419eb66acfb7578fff7d019027ce565e8f.json
generated
Normal file
20
.sqlx/query-ee0e90b6ddb5dcc454f47b1326bf5a419eb66acfb7578fff7d019027ce565e8f.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) as 'count!' FROM posts WHERE hidden = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "count!",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ee0e90b6ddb5dcc454f47b1326bf5a419eb66acfb7578fff7d019027ce565e8f"
|
||||
}
|
||||
16
migrations/0006_admin.sql
Normal file
16
migrations/0006_admin.sql
Normal file
@ -0,0 +1,16 @@
|
||||
ALTER TABLE users ADD COLUMN banned BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE users ADD COLUMN banned_at DATETIME;
|
||||
ALTER TABLE users ADD COLUMN ban_reason TEXT;
|
||||
ALTER TABLE users ADD COLUMN ban_length TEXT;
|
||||
ALTER TABLE users ADD COLUMN unban_at DATETIME;
|
||||
ALTER TABLE users ADD COLUMN banned_by TEXT;
|
||||
ALTER TABLE users ADD COLUMN ban_post_content TEXT;
|
||||
|
||||
ALTER TABLE threads ADD COLUMN hidden BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE threads ADD COLUMN hidden_at DATETIME;
|
||||
ALTER TABLE threads ADD COLUMN hidden_by TEXT;
|
||||
|
||||
ALTER TABLE posts ADD COLUMN hidden BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE posts ADD COLUMN hidden_at DATETIME;
|
||||
ALTER TABLE posts ADD COLUMN hidden_by TEXT;
|
||||
ALTER TABLE posts ADD COLUMN ban_message TEXT;
|
||||
53
src/db.rs
53
src/db.rs
@ -1,3 +1,7 @@
|
||||
use argon2::{
|
||||
Argon2,
|
||||
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
|
||||
pub async fn connect(database_url: &str) -> SqlitePool {
|
||||
@ -32,6 +36,55 @@ pub async fn connect(database_url: &str) -> SqlitePool {
|
||||
.await
|
||||
.expect("failed to enable foreign keys");
|
||||
|
||||
seed_admin(&pool).await;
|
||||
|
||||
println!("database ready");
|
||||
pool
|
||||
}
|
||||
|
||||
async fn seed_admin(pool: &SqlitePool) {
|
||||
let admin_username = std::env::var("SARMENTINE_ADMIN_USERNAME")
|
||||
.unwrap_or_else(|_| "admin".into());
|
||||
let admin_email = std::env::var("SARMENTINE_ADMIN_EMAIL")
|
||||
.unwrap_or_else(|_| "admin@localhost".into());
|
||||
let admin_password = std::env::var("SARMENTINE_ADMIN_PASSWORD")
|
||||
.unwrap_or_else(|_| "butterbutter".into());
|
||||
|
||||
let existing: Option<(i64,)> = sqlx::query_as(
|
||||
"SELECT id FROM users WHERE username = ?",
|
||||
)
|
||||
.bind(&admin_username)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some((id,)) = existing {
|
||||
if let Err(e) = sqlx::query("UPDATE users SET role = 'admin' WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
eprintln!("failed to ensure admin role for {}: {}", admin_username, e);
|
||||
}
|
||||
} else {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hash = Argon2::default()
|
||||
.hash_password(admin_password.as_bytes(), &salt)
|
||||
.expect("password hashing failed")
|
||||
.to_string();
|
||||
|
||||
match sqlx::query(
|
||||
"INSERT INTO users (username, email, password, email_verified, role) VALUES (?, ?, ?, TRUE, 'admin')",
|
||||
)
|
||||
.bind(&admin_username)
|
||||
.bind(&admin_email)
|
||||
.bind(&hash)
|
||||
.execute(pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => eprintln!("seeded admin account: {}", admin_username),
|
||||
Err(e) => eprintln!("failed to seed admin account: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
386
src/handlers/admin.rs
Normal file
386
src/handlers/admin.rs
Normal file
@ -0,0 +1,386 @@
|
||||
use axum::{
|
||||
extract::{Form, Path, State},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use askama::Template;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Deserialize;
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::models::{post::Post, thread::Thread, user::User};
|
||||
|
||||
fn render<T: Template>(t: T) -> Response {
|
||||
match t.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(_) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"render error",
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// ~~ Data wrappers
|
||||
|
||||
pub struct AdminUserRow {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub role: String,
|
||||
pub email_verified: bool,
|
||||
pub banned: bool,
|
||||
}
|
||||
|
||||
pub struct AdminThreadRow {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub author_username: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
// ~~ Form types
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BanForm {
|
||||
pub reason: String,
|
||||
pub ban_length: String,
|
||||
pub unban_at: Option<String>,
|
||||
pub nuke: Option<String>,
|
||||
}
|
||||
|
||||
// ~~ Templates
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/dashboard.html")]
|
||||
pub struct DashboardTemplate {
|
||||
pub title: String,
|
||||
pub current_user: Option<crate::CurrentUser>,
|
||||
pub csrf_token: String,
|
||||
pub user_count: i64,
|
||||
pub thread_count: i64,
|
||||
pub post_count: i64,
|
||||
pub hidden_thread_count: i64,
|
||||
pub banned_user_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/users.html")]
|
||||
pub struct UsersTemplate {
|
||||
pub title: String,
|
||||
pub current_user: Option<crate::CurrentUser>,
|
||||
pub csrf_token: String,
|
||||
pub users: Vec<AdminUserRow>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/ban_form.html")]
|
||||
pub struct BanFormTemplate {
|
||||
pub title: String,
|
||||
pub current_user: Option<crate::CurrentUser>,
|
||||
pub csrf_token: String,
|
||||
pub user_id: i64,
|
||||
pub username: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/threads.html")]
|
||||
pub struct ThreadsTemplate {
|
||||
pub title: String,
|
||||
pub current_user: Option<crate::CurrentUser>,
|
||||
pub csrf_token: String,
|
||||
pub threads: Vec<AdminThreadRow>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin/banned.html")]
|
||||
pub struct BannedTemplate {
|
||||
pub title: String,
|
||||
pub current_user: Option<crate::CurrentUser>,
|
||||
pub csrf_token: String,
|
||||
pub banned_at: String,
|
||||
pub ban_reason: Option<String>,
|
||||
pub ban_length: String,
|
||||
pub unban_at: String,
|
||||
pub ban_post_content: String,
|
||||
}
|
||||
|
||||
// ~~ Helpers
|
||||
|
||||
async fn get_csrf(session: &Session) -> String {
|
||||
session
|
||||
.get("csrf_token")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn require_admin(session: &Session, pool: &sqlx::SqlitePool) -> Option<User> {
|
||||
let user_id: Option<i64> = session.get("user_id").await.ok().flatten();
|
||||
let id = user_id?;
|
||||
let user = User::find_by_id(pool, id).await.ok()??;
|
||||
if user.role == "admin" && !user.banned {
|
||||
Some(user)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_redirect() -> Redirect {
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
||||
// ~~ Handlers
|
||||
|
||||
pub async fn dashboard(State(state): State<AppState>, session: Session) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return admin_redirect().into_response(),
|
||||
};
|
||||
let current_user = Some(crate::CurrentUser {
|
||||
username: admin.username,
|
||||
email_verified: admin.email_verified,
|
||||
role: admin.role,
|
||||
});
|
||||
let csrf_token = get_csrf(&session).await;
|
||||
|
||||
let user_count = User::count_all(&state.pool).await.unwrap_or(0);
|
||||
let banned_user_count = User::list_all(&state.pool)
|
||||
.await
|
||||
.map(|users| users.iter().filter(|u| u.banned).count() as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let thread_count = Thread::count_all(&state.pool).await.unwrap_or(0);
|
||||
let hidden_thread_count = Thread::list_all_admin(&state.pool)
|
||||
.await
|
||||
.map(|threads| threads.iter().filter(|t| t.hidden).count() as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let post_count = Post::count_all(&state.pool).await.unwrap_or(0);
|
||||
|
||||
render(DashboardTemplate {
|
||||
title: "admin dashboard".into(),
|
||||
current_user,
|
||||
csrf_token,
|
||||
user_count,
|
||||
thread_count,
|
||||
post_count,
|
||||
hidden_thread_count,
|
||||
banned_user_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_users(State(state): State<AppState>, session: Session) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return admin_redirect().into_response(),
|
||||
};
|
||||
let current_user = Some(crate::CurrentUser {
|
||||
username: admin.username,
|
||||
email_verified: admin.email_verified,
|
||||
role: admin.role,
|
||||
});
|
||||
let csrf_token = get_csrf(&session).await;
|
||||
|
||||
let users = User::list_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|u| AdminUserRow {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
role: u.role,
|
||||
email_verified: u.email_verified,
|
||||
banned: u.banned,
|
||||
})
|
||||
.collect();
|
||||
|
||||
render(UsersTemplate {
|
||||
title: "admin :: users".into(),
|
||||
current_user,
|
||||
csrf_token,
|
||||
users,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn ban_form(Path(id): Path<i64>, State(state): State<AppState>, session: Session) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return admin_redirect().into_response(),
|
||||
};
|
||||
let current_user = Some(crate::CurrentUser {
|
||||
username: admin.username,
|
||||
email_verified: admin.email_verified,
|
||||
role: admin.role,
|
||||
});
|
||||
let csrf_token = get_csrf(&session).await;
|
||||
|
||||
let target = match User::find_by_id(&state.pool, id).await.ok().flatten() {
|
||||
Some(u) => u,
|
||||
None => return Redirect::to("/admin/users").into_response(),
|
||||
};
|
||||
|
||||
render(BanFormTemplate {
|
||||
title: format!("ban {}", target.username),
|
||||
current_user,
|
||||
csrf_token,
|
||||
user_id: id,
|
||||
username: target.username,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn ban_submit(
|
||||
Path(id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
Form(form): Form<BanForm>,
|
||||
) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return Redirect::to("/auth/login").into_response(),
|
||||
};
|
||||
|
||||
let unban_at = if let Some(ref s) = form.unban_at {
|
||||
if !s.is_empty() {
|
||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let reason = if form.reason.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(form.reason.as_str())
|
||||
};
|
||||
let length = if form.ban_length.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(form.ban_length.as_str())
|
||||
};
|
||||
|
||||
if let Err(e) = User::ban(
|
||||
&state.pool,
|
||||
id,
|
||||
&admin.username,
|
||||
reason,
|
||||
length,
|
||||
unban_at.as_ref(),
|
||||
None,
|
||||
).await {
|
||||
eprintln!("failed to ban user {}: {}", id, e);
|
||||
}
|
||||
|
||||
if form.nuke.as_deref() == Some("on") {
|
||||
let ban_msg = form.reason.clone();
|
||||
if let Err(e) = User::nuke_content(&state.pool, id, &admin.username, &ban_msg).await {
|
||||
eprintln!("failed to nuke content for user {}: {}", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
Redirect::to("/admin/users").into_response()
|
||||
}
|
||||
|
||||
pub async fn unban_user(Path(id): Path<i64>, State(state): State<AppState>) -> Response {
|
||||
if let Err(e) = User::unban(&state.pool, id).await {
|
||||
eprintln!("failed to unban user {}: {}", id, e);
|
||||
}
|
||||
Redirect::to("/admin/users").into_response()
|
||||
}
|
||||
|
||||
pub async fn toggle_role(Path(id): Path<i64>, State(state): State<AppState>) -> Response {
|
||||
let user = match User::find_by_id(&state.pool, id).await.ok().flatten() {
|
||||
Some(u) => u,
|
||||
None => return Redirect::to("/admin/users").into_response(),
|
||||
};
|
||||
|
||||
let new_role = if user.role == "admin" { "member" } else { "admin" };
|
||||
if let Err(e) = User::set_role(&state.pool, id, new_role).await {
|
||||
eprintln!("failed to set role for user {}: {}", id, e);
|
||||
}
|
||||
Redirect::to("/admin/users").into_response()
|
||||
}
|
||||
|
||||
pub async fn list_threads_admin(State(state): State<AppState>, session: Session) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return admin_redirect().into_response(),
|
||||
};
|
||||
let current_user = Some(crate::CurrentUser {
|
||||
username: admin.username,
|
||||
email_verified: admin.email_verified,
|
||||
role: admin.role,
|
||||
});
|
||||
let csrf_token = get_csrf(&session).await;
|
||||
|
||||
let threads = Thread::list_all_admin(&state.pool).await.unwrap_or_default();
|
||||
|
||||
let mut rows = Vec::new();
|
||||
for t in threads {
|
||||
let author = User::find_by_id(&state.pool, t.author_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|u| u.username)
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
rows.push(AdminThreadRow {
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
author_username: author,
|
||||
created_at: t.created_at,
|
||||
hidden: t.hidden,
|
||||
});
|
||||
}
|
||||
|
||||
render(ThreadsTemplate {
|
||||
title: "admin :: threads".into(),
|
||||
current_user,
|
||||
csrf_token,
|
||||
threads: rows,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn hide_thread(Path(id): Path<i64>, State(state): State<AppState>, session: Session) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return Redirect::to("/auth/login").into_response(),
|
||||
};
|
||||
if let Err(e) = Thread::hide(&state.pool, id, &admin.username).await {
|
||||
eprintln!("failed to hide thread {}: {}", id, e);
|
||||
}
|
||||
Redirect::to("/admin/threads").into_response()
|
||||
}
|
||||
|
||||
pub async fn unhide_thread(Path(id): Path<i64>, State(state): State<AppState>) -> Response {
|
||||
if let Err(e) = Thread::unhide(&state.pool, id).await {
|
||||
eprintln!("failed to unhide thread {}: {}", id, e);
|
||||
}
|
||||
Redirect::to("/admin/threads").into_response()
|
||||
}
|
||||
|
||||
pub async fn perma_delete_thread(
|
||||
Path(id): Path<i64>,
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
) -> Response {
|
||||
let admin = match require_admin(&session, &state.pool).await {
|
||||
Some(u) => u,
|
||||
None => return Redirect::to("/auth/login").into_response(),
|
||||
};
|
||||
|
||||
if admin.username != "butter" {
|
||||
return Redirect::to("/admin/threads").into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = Thread::permanent_delete(&state.pool, id).await {
|
||||
eprintln!("failed to perma-delete thread {}: {}", id, e);
|
||||
}
|
||||
Redirect::to("/admin/threads").into_response()
|
||||
}
|
||||
@ -12,6 +12,7 @@ use std::net::SocketAddr;
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::handlers::admin::BannedTemplate;
|
||||
use crate::middleware::generate_token;
|
||||
use crate::models::user::User;
|
||||
|
||||
@ -245,6 +246,28 @@ pub async fn login_submit(
|
||||
None => return fail(),
|
||||
};
|
||||
|
||||
if user.banned {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
if let Some(unban) = user.unban_at && now >= unban {
|
||||
let _ = User::unban(&state.pool, user.id).await;
|
||||
} else {
|
||||
let banned_at = user.banned_at.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()).unwrap_or_else(|| "unknown".into());
|
||||
let ban_length = user.ban_length.unwrap_or_else(|| "permanent".into());
|
||||
let unban_at = user.unban_at.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()).unwrap_or_else(|| "never".into());
|
||||
let ban_post_content = user.ban_post_content.unwrap_or_default();
|
||||
return render(BannedTemplate {
|
||||
title: "account banned".into(),
|
||||
current_user: None,
|
||||
csrf_token: csrf_token.clone(),
|
||||
banned_at,
|
||||
ban_reason: user.ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
ban_post_content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let parsed_hash = PasswordHash::new(&user.password).expect("invalid stored hash");
|
||||
if Argon2::default()
|
||||
.verify_password(form.password.as_bytes(), &parsed_hash)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
pub mod filters;
|
||||
pub mod thread;
|
||||
|
||||
@ -99,6 +99,7 @@ pub async fn list_threads(State(state): State<AppState>, session: Session) -> Re
|
||||
let current_user = user.map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let threads = Thread::list_all(&state.pool).await.unwrap_or_default();
|
||||
@ -136,6 +137,7 @@ pub async fn new_thread_page(session: Session, State(state): State<AppState>) ->
|
||||
let current_user = Some(CurrentUser {
|
||||
username: user.username,
|
||||
email_verified: user.email_verified,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
render(NewThreadTemplate {
|
||||
@ -163,6 +165,7 @@ pub async fn create_thread(
|
||||
let current_user = user.clone().map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let user = match user {
|
||||
@ -204,6 +207,7 @@ pub async fn view_thread(
|
||||
let current_user = user.map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let thread = match Thread::find_by_id(&state.pool, id).await.ok().flatten() {
|
||||
@ -226,7 +230,7 @@ pub async fn view_thread(
|
||||
}
|
||||
};
|
||||
|
||||
let posts = Post::by_thread(&state.pool, id).await.unwrap_or_default();
|
||||
let posts = Post::by_thread_all(&state.pool, id).await.unwrap_or_default();
|
||||
|
||||
let mut posts_with_authors = Vec::new();
|
||||
for post in posts {
|
||||
@ -272,6 +276,7 @@ pub async fn create_post(
|
||||
let current_user = user.clone().map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let user = match user {
|
||||
@ -298,9 +303,16 @@ pub async fn create_post(
|
||||
verification_token_expires: None,
|
||||
reset_token: None,
|
||||
reset_token_expires: None,
|
||||
banned: false,
|
||||
banned_at: None,
|
||||
ban_reason: None,
|
||||
ban_length: None,
|
||||
unban_at: None,
|
||||
banned_by: None,
|
||||
ban_post_content: None,
|
||||
});
|
||||
|
||||
let posts = Post::by_thread(&state.pool, id).await.unwrap_or_default();
|
||||
let posts = Post::by_thread_all(&state.pool, id).await.unwrap_or_default();
|
||||
let mut posts_with_authors = Vec::new();
|
||||
for post in posts {
|
||||
let author = User::find_by_id(&state.pool, post.author_id)
|
||||
@ -341,9 +353,16 @@ pub async fn create_post(
|
||||
verification_token_expires: None,
|
||||
reset_token: None,
|
||||
reset_token_expires: None,
|
||||
banned: false,
|
||||
banned_at: None,
|
||||
ban_reason: None,
|
||||
ban_length: None,
|
||||
unban_at: None,
|
||||
banned_by: None,
|
||||
ban_post_content: None,
|
||||
});
|
||||
|
||||
let posts = Post::by_thread(&state.pool, id).await.unwrap_or_default();
|
||||
let posts = Post::by_thread_all(&state.pool, id).await.unwrap_or_default();
|
||||
let mut posts_with_authors = Vec::new();
|
||||
for post in posts {
|
||||
let author = User::find_by_id(&state.pool, post.author_id)
|
||||
@ -381,6 +400,7 @@ pub async fn view_profile(
|
||||
let current_user = user.map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let profile_user = match User::find_by_username(&state.pool, &username)
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@ -18,6 +18,7 @@ use tower_http::services::ServeDir;
|
||||
use tower_sessions::{SessionManagerLayer, cookie::SameSite};
|
||||
use tower_sessions_sqlx_store::SqliteStore;
|
||||
|
||||
use handlers::admin::{ban_form, ban_submit, dashboard, hide_thread, list_threads_admin, list_users, perma_delete_thread, toggle_role, unban_user, unhide_thread};
|
||||
use handlers::auth::{
|
||||
forgot_password_page, forgot_password_submit, get_current_user, login_page, login_submit,
|
||||
logout, register_page, register_submit, resend_verification, reset_password_page,
|
||||
@ -30,6 +31,7 @@ use handlers::thread::{
|
||||
pub struct CurrentUser {
|
||||
pub username: String,
|
||||
pub email_verified: bool,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -45,6 +47,7 @@ async fn index(State(state): State<AppState>, session: tower_sessions::Session)
|
||||
let current_user = user.map(|u| CurrentUser {
|
||||
username: u.username,
|
||||
email_verified: u.email_verified,
|
||||
role: u.role,
|
||||
});
|
||||
|
||||
let csrf_token = session
|
||||
@ -147,9 +150,26 @@ async fn main() {
|
||||
.route("/auth/reset", post(reset_password_submit))
|
||||
.nest_service("/static", ServeDir::new(&static_dir))
|
||||
.layer(from_fn(csrf_middleware))
|
||||
.layer(session_layer.clone())
|
||||
.with_state(app_state.clone());
|
||||
|
||||
let admin_routes = Router::new()
|
||||
.route("/admin", get(dashboard))
|
||||
.route("/admin/users", get(list_users))
|
||||
.route("/admin/users/:id/ban", get(ban_form))
|
||||
.route("/admin/users/:id/ban", post(ban_submit))
|
||||
.route("/admin/users/:id/unban", post(unban_user))
|
||||
.route("/admin/users/:id/toggle-role", post(toggle_role))
|
||||
.route("/admin/threads", get(list_threads_admin))
|
||||
.route("/admin/threads/:id/hide", post(hide_thread))
|
||||
.route("/admin/threads/:id/unhide", post(unhide_thread))
|
||||
.route("/admin/threads/:id/delete", post(perma_delete_thread))
|
||||
.layer(from_fn(csrf_middleware))
|
||||
.layer(session_layer)
|
||||
.with_state(app_state);
|
||||
|
||||
let app = app.merge(admin_routes);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&bind_addr).await.unwrap();
|
||||
|
||||
println!("listening on http://{}", bind_addr);
|
||||
|
||||
@ -126,7 +126,7 @@ pub fn generate_token() -> String {
|
||||
base64_url_encode(&bytes)
|
||||
}
|
||||
|
||||
fn base64_url_encode(input: &[u8]) -> String {
|
||||
fn base64_url_encode(input: &[u8]) -> String {
|
||||
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
input
|
||||
.chunks(3)
|
||||
@ -147,4 +147,4 @@ fn base64_url_encode(input: &[u8]) -> String {
|
||||
out
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,13 +10,30 @@ pub struct Post {
|
||||
pub thread_id: i64,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub hidden: bool,
|
||||
pub hidden_at: Option<NaiveDateTime>,
|
||||
pub hidden_by: Option<String>,
|
||||
pub ban_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub async fn by_thread(pool: &SqlitePool, thread_id: i64) -> sqlx::Result<Vec<Post>> {
|
||||
sqlx::query_as!(
|
||||
Post,
|
||||
r#"SELECT id, body, author_id, thread_id, created_at, updated_at
|
||||
r#"SELECT id, body, author_id, thread_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by, ban_message
|
||||
FROM posts WHERE thread_id = ? AND hidden = FALSE ORDER BY created_at ASC"#,
|
||||
thread_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn by_thread_all(pool: &SqlitePool, thread_id: i64) -> sqlx::Result<Vec<Post>> {
|
||||
sqlx::query_as!(
|
||||
Post,
|
||||
r#"SELECT id, body, author_id, thread_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by, ban_message
|
||||
FROM posts WHERE thread_id = ? ORDER BY created_at ASC"#,
|
||||
thread_id
|
||||
)
|
||||
@ -39,4 +56,44 @@ impl Post {
|
||||
|
||||
Ok(result.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub async fn hide(pool: &SqlitePool, id: i64, hidden_by: &str, ban_message: Option<&str>) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE posts SET hidden = TRUE, hidden_at = CURRENT_TIMESTAMP, hidden_by = ?, ban_message = ? WHERE id = ?",
|
||||
)
|
||||
.bind(hidden_by)
|
||||
.bind(ban_message)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unhide(pool: &SqlitePool, id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE posts SET hidden = FALSE, hidden_at = NULL, hidden_by = NULL, ban_message = NULL WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn permanent_delete(pool: &SqlitePool, id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query("DELETE FROM posts WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn count_all(pool: &SqlitePool) -> sqlx::Result<i64> {
|
||||
let count: i32 = sqlx::query_scalar!("SELECT COUNT(*) as 'count!' FROM posts WHERE hidden = FALSE")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(count as i64)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,13 +10,28 @@ pub struct Thread {
|
||||
pub author_id: i64,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub hidden: bool,
|
||||
pub hidden_at: Option<NaiveDateTime>,
|
||||
pub hidden_by: Option<String>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub async fn list_all(pool: &SqlitePool) -> sqlx::Result<Vec<Thread>> {
|
||||
sqlx::query_as!(
|
||||
Thread,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by
|
||||
FROM threads WHERE hidden = FALSE ORDER BY updated_at DESC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_all_admin(pool: &SqlitePool) -> sqlx::Result<Vec<Thread>> {
|
||||
sqlx::query_as!(
|
||||
Thread,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by
|
||||
FROM threads ORDER BY updated_at DESC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@ -26,7 +41,20 @@ impl Thread {
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: i64) -> sqlx::Result<Option<Thread>> {
|
||||
sqlx::query_as!(
|
||||
Thread,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by
|
||||
FROM threads WHERE id = ? AND hidden = FALSE"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_id_admin(pool: &SqlitePool, id: i64) -> sqlx::Result<Option<Thread>> {
|
||||
sqlx::query_as!(
|
||||
Thread,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by
|
||||
FROM threads WHERE id = ?"#,
|
||||
id
|
||||
)
|
||||
@ -37,8 +65,9 @@ impl Thread {
|
||||
pub async fn by_author(pool: &SqlitePool, author_id: i64) -> sqlx::Result<Vec<Thread>> {
|
||||
sqlx::query_as!(
|
||||
Thread,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at
|
||||
FROM threads WHERE author_id = ? ORDER BY updated_at DESC"#,
|
||||
r#"SELECT id, title, body, author_id, created_at, updated_at,
|
||||
hidden AS "hidden!", hidden_at, hidden_by
|
||||
FROM threads WHERE author_id = ? AND hidden = FALSE ORDER BY updated_at DESC"#,
|
||||
author_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@ -60,4 +89,43 @@ impl Thread {
|
||||
|
||||
Ok(result.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub async fn hide(pool: &SqlitePool, id: i64, hidden_by: &str) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE threads SET hidden = TRUE, hidden_at = CURRENT_TIMESTAMP, hidden_by = ? WHERE id = ?",
|
||||
)
|
||||
.bind(hidden_by)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unhide(pool: &SqlitePool, id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE threads SET hidden = FALSE, hidden_at = NULL, hidden_by = NULL WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn permanent_delete(pool: &SqlitePool, id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query("DELETE FROM threads WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn count_all(pool: &SqlitePool) -> sqlx::Result<i64> {
|
||||
let count: i32 = sqlx::query_scalar!("SELECT COUNT(*) as 'count!' FROM threads WHERE hidden = FALSE")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(count as i64)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,13 @@ pub struct User {
|
||||
pub verification_token_expires: Option<NaiveDateTime>,
|
||||
pub reset_token: Option<String>,
|
||||
pub reset_token_expires: Option<NaiveDateTime>,
|
||||
pub banned: bool,
|
||||
pub banned_at: Option<NaiveDateTime>,
|
||||
pub ban_reason: Option<String>,
|
||||
pub ban_length: Option<String>,
|
||||
pub unban_at: Option<NaiveDateTime>,
|
||||
pub banned_by: Option<String>,
|
||||
pub ban_post_content: Option<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
@ -29,7 +36,14 @@ impl User {
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users WHERE id = ?"#,
|
||||
id
|
||||
)
|
||||
@ -45,7 +59,14 @@ impl User {
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users WHERE username = ?"#,
|
||||
username
|
||||
)
|
||||
@ -61,7 +82,14 @@ impl User {
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users WHERE email = ?"#,
|
||||
email
|
||||
)
|
||||
@ -80,7 +108,14 @@ impl User {
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users WHERE verification_token = ?"#,
|
||||
token
|
||||
)
|
||||
@ -96,7 +131,14 @@ impl User {
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users WHERE reset_token = ?"#,
|
||||
token
|
||||
)
|
||||
@ -104,6 +146,28 @@ impl User {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_all(pool: &SqlitePool) -> sqlx::Result<Vec<User>> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
r#"SELECT id AS "id!", username, email, password, bio, avatar_url, role, created_at,
|
||||
email_verified AS "email_verified!",
|
||||
verification_token,
|
||||
verification_token_expires,
|
||||
reset_token,
|
||||
reset_token_expires,
|
||||
banned AS "banned!",
|
||||
banned_at,
|
||||
ban_reason,
|
||||
ban_length,
|
||||
unban_at,
|
||||
banned_by,
|
||||
ban_post_content
|
||||
FROM users ORDER BY created_at DESC"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
pool: &SqlitePool,
|
||||
username: &str,
|
||||
@ -166,7 +230,9 @@ impl User {
|
||||
token: &str,
|
||||
expires: &NaiveDateTime,
|
||||
) -> sqlx::Result<()> {
|
||||
sqlx::query("UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?")
|
||||
sqlx::query(
|
||||
"UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?",
|
||||
)
|
||||
.bind(token)
|
||||
.bind(expires)
|
||||
.bind(user_id)
|
||||
@ -177,7 +243,9 @@ impl User {
|
||||
}
|
||||
|
||||
pub async fn clear_reset_token(pool: &SqlitePool, user_id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query("UPDATE users SET reset_token = NULL, reset_token_expires = NULL WHERE id = ?")
|
||||
sqlx::query(
|
||||
"UPDATE users SET reset_token = NULL, reset_token_expires = NULL WHERE id = ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
@ -198,4 +266,86 @@ impl User {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_role(pool: &SqlitePool, user_id: i64, role: &str) -> sqlx::Result<()> {
|
||||
sqlx::query("UPDATE users SET role = ? WHERE id = ?")
|
||||
.bind(role)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_role_to_admin(pool: &SqlitePool, username: &str) -> sqlx::Result<()> {
|
||||
sqlx::query("UPDATE users SET role = 'admin' WHERE username = ?")
|
||||
.bind(username)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ban(
|
||||
pool: &SqlitePool,
|
||||
user_id: i64,
|
||||
banned_by: &str,
|
||||
reason: Option<&str>,
|
||||
length: Option<&str>,
|
||||
unban_at: Option<&NaiveDateTime>,
|
||||
post_content: Option<&str>,
|
||||
) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET banned = TRUE, banned_at = CURRENT_TIMESTAMP, banned_by = ?, ban_reason = ?, ban_length = ?, unban_at = ?, ban_post_content = ? WHERE id = ?",
|
||||
)
|
||||
.bind(banned_by)
|
||||
.bind(reason)
|
||||
.bind(length)
|
||||
.bind(unban_at)
|
||||
.bind(post_content)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unban(pool: &SqlitePool, user_id: i64) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET banned = FALSE, banned_at = NULL, ban_reason = NULL, ban_length = NULL, unban_at = NULL, banned_by = NULL, ban_post_content = NULL WHERE id = ?",
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn nuke_content(pool: &SqlitePool, user_id: i64, hidden_by: &str, ban_message: &str) -> sqlx::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE threads SET hidden = TRUE, hidden_at = CURRENT_TIMESTAMP, hidden_by = ? WHERE author_id = ? AND hidden = FALSE",
|
||||
)
|
||||
.bind(hidden_by)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE posts SET hidden = TRUE, hidden_at = CURRENT_TIMESTAMP, hidden_by = ?, ban_message = ? WHERE author_id = ? AND hidden = FALSE",
|
||||
)
|
||||
.bind(hidden_by)
|
||||
.bind(ban_message)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn count_all(pool: &SqlitePool) -> sqlx::Result<i64> {
|
||||
let count: i32 = sqlx::query_scalar!("SELECT COUNT(*) as 'count!' FROM users")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(count as i64)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
static/meeple.png
Normal file
BIN
static/meeple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
46
templates/admin/ban_form.html
Normal file
46
templates/admin/ban_form.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:14px; color:#d4879c; margin-bottom:16px; text-transform:uppercase;">
|
||||
<a href="/admin" style="color:#888;">admin</a> :: <a href="/admin/users" style="color:#888;">users</a> :: ban {{ username }}
|
||||
</div>
|
||||
|
||||
{% if let Some(err) = error %}
|
||||
<div class="post" style="border-color:#ff6b6b;">
|
||||
<div class="post-body" style="color:#ff6b6b;">{{ err }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="post">
|
||||
<form method="post" action="/admin/users/{{ user_id }}/ban">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<label style="display:block; color:#888; font-size:11px; margin-bottom:4px; text-transform:uppercase;">reason</label>
|
||||
<input type="text" name="reason" style="width:100%; padding:6px; background:#1a1a2e; border:1px solid #333; color:#e0e0e0; font-family:'VT323',monospace; font-size:14px;" placeholder="leave empty for no reason">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<label style="display:block; color:#888; font-size:11px; margin-bottom:4px; text-transform:uppercase;">ban length</label>
|
||||
<select name="ban_length" style="padding:6px; background:#1a1a2e; border:1px solid #333; color:#e0e0e0; font-family:'VT323',monospace; font-size:14px;">
|
||||
<option value="permanent">permanent</option>
|
||||
<option value="temporary">temporary</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<label style="display:block; color:#888; font-size:11px; margin-bottom:4px; text-transform:uppercase;">unban at (optional, for temporary bans)</label>
|
||||
<input type="datetime-local" name="unban_at" style="padding:6px; background:#1a1a2e; border:1px solid #333; color:#e0e0e0; font-family:'VT323',monospace; font-size:14px;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="color:#ff6b6b; font-size:11px; text-transform:uppercase;">
|
||||
<input type="checkbox" name="nuke" value="on"> nuke all user content (hide threads and posts)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" style="background:#ff6b6b;">ban user</button>
|
||||
<a href="/admin/users" class="btn" style="margin-left:8px;">cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
templates/admin/banned.html
Normal file
41
templates/admin/banned.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:14px; color:#ff6b6b; margin-bottom:16px; text-transform:uppercase;">
|
||||
account banned
|
||||
</div>
|
||||
|
||||
<div class="post" style="border-color:#ff6b6b;">
|
||||
<div style="color:#ff6b6b; font-size:16px; margin-bottom:12px;">
|
||||
your account was b& on {{ banned_at }}
|
||||
</div>
|
||||
|
||||
{% if !ban_post_content.is_empty() %}
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase; margin-bottom:4px;">the post that got you b&</div>
|
||||
<div style="color:#d4879c; font-size:12px; padding:8px; background:#1a1a2e; border:1px solid #333;">{{ ban_post_content }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase; margin-bottom:4px;">reason</div>
|
||||
{% if let Some(reason) = ban_reason %}
|
||||
<div style="color:#e0e0e0; font-size:12px;">{{ reason }}</div>
|
||||
{% else %}
|
||||
<div style="color:#888; font-size:12px;">admin didn't care enough to give a reason</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase; margin-bottom:4px;">ban length</div>
|
||||
<div style="color:#e0e0e0; font-size:12px;">{{ ban_length }}</div>
|
||||
</div>
|
||||
|
||||
{% if unban_at != "never" %}
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase; margin-bottom:4px;">unban at</div>
|
||||
<div style="color:#4ade80; font-size:12px;">{{ unban_at }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
templates/admin/dashboard.html
Normal file
42
templates/admin/dashboard.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:14px; color:#d4879c; margin-bottom:16px; text-transform:uppercase;">
|
||||
admin dashboard
|
||||
</div>
|
||||
|
||||
<div class="post">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:16px;">
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:28px; color:#e0e0e0;">{{ user_count }}</div>
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase;">users</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:28px; color:#e0e0e0;">{{ thread_count }}</div>
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase;">threads</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:28px; color:#e0e0e0;">{{ post_count }}</div>
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase;">posts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post" style="margin-top:16px;">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:28px; color:#ff6b6b;">{{ hidden_thread_count }}</div>
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase;">hidden threads</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:28px; color:#ff6b6b;">{{ banned_user_count }}</div>
|
||||
<div style="color:#888; font-size:10px; text-transform:uppercase;">banned users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px; display:flex; gap:12px;">
|
||||
<a href="/admin/users" class="btn">manage users</a>
|
||||
<a href="/admin/threads" class="btn">manage threads</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
templates/admin/threads.html
Normal file
43
templates/admin/threads.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:14px; color:#d4879c; margin-bottom:16px; text-transform:uppercase;">
|
||||
<a href="/admin" style="color:#888;">admin</a> :: threads
|
||||
</div>
|
||||
|
||||
{% for t in threads %}
|
||||
<div class="post" {% if t.hidden %}style="border-color:#ff6b6b;"{% endif %}>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
{% if t.hidden %}
|
||||
<span style="color:#ff6b6b; font-size:10px;">[hidden]</span>
|
||||
{% endif %}
|
||||
<span style="color:#e0e0e0;">{{ t.title }}</span>
|
||||
<span style="color:#888; font-size:10px; margin-left:8px;">by {{ t.author_username }}</span>
|
||||
<span style="color:#888; font-size:10px; margin-left:8px;">{{ t.created_at }}</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
{% if !t.hidden %}
|
||||
<form method="post" action="/admin/threads/{{ t.id }}/hide" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm">hide</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="/admin/threads/{{ t.id }}/unhide" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm">unhide</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/threads/{{ t.id }}/delete" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm" style="background:#ff0000;">delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if threads.is_empty() %}
|
||||
<div style="color:#888;">no threads found</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
45
templates/admin/users.html
Normal file
45
templates/admin/users.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:14px; color:#d4879c; margin-bottom:16px; text-transform:uppercase;">
|
||||
<a href="/admin" style="color:#888;">admin</a> :: users
|
||||
</div>
|
||||
|
||||
{% for u in users %}
|
||||
<div class="post" {% if u.banned %}style="border-color:#ff6b6b;"{% endif %}>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<a href="/profile/{{ u.username }}" style="color:#e0e0e0;">{{ u.username }}</a>
|
||||
<span style="color:#888; font-size:10px; margin-left:8px;">{{ u.email }}</span>
|
||||
{% if u.role == "admin" %}
|
||||
<span style="color:#ffd700; font-size:10px; margin-left:8px;">[admin]</span>
|
||||
{% endif %}
|
||||
{% if u.banned %}
|
||||
<span style="color:#ff6b6b; font-size:10px; margin-left:8px;">[banned]</span>
|
||||
{% endif %}
|
||||
{% if !u.email_verified %}
|
||||
<span style="color:#888; font-size:10px; margin-left:8px;">[unverified]</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
{% if !u.banned %}
|
||||
<a href="/admin/users/{{ u.id }}/ban" class="btn btn-sm">ban</a>
|
||||
{% else %}
|
||||
<form method="post" action="/admin/users/{{ u.id }}/unban" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm">unban</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/users/{{ u.id }}/toggle-role" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm">{% if u.role == "admin" %}demote{% else %}promote{% endif %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if users.is_empty() %}
|
||||
<div style="color:#888;">no users found</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -17,6 +17,9 @@
|
||||
<a href="/threads">threads</a>
|
||||
<div class="nav-right">
|
||||
{% if let Some(user) = current_user %}
|
||||
{% if user.role == "admin" %}
|
||||
<a href="/admin" style="color:#ffd700;">admin</a>
|
||||
{% endif %}
|
||||
<a href="/profile/{{ user.username }}">{{ user.username }}</a>
|
||||
<form method="post" action="/auth/logout" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
@ -21,8 +21,17 @@
|
||||
<div style="font-family:'Silkscreen',monospace; font-size:11px; color:#888; margin:16px 0 8px; text-transform:uppercase;">replies</div>
|
||||
{% for pwa in posts %}
|
||||
<div class="post">
|
||||
{% if pwa.post.hidden %}
|
||||
<div style="color:#ff6b6b; font-size:12px;">[hidden]</div>
|
||||
{% if let Some(msg) = pwa.post.ban_message %}
|
||||
<div style="color:#ff6b6b; font-size:10px;">REASON: {{ msg }}</div>
|
||||
{% else %}
|
||||
<div style="color:#ff6b6b; font-size:10px;">REASON: admin didn't care enough to give a reason</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="post-body" style="color:#888; font-size:11px; margin-bottom:8px;">by <a href="/profile/{{ pwa.author.username }}">{{ pwa.author.username }}</a> · {{ pwa.post.created_at }}</div>
|
||||
<div class="post-body markdown-content">{{ pwa.post.body|render_markdown|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user