+2
-2
.github/workflows/docker-image.yml
+2
-2
.github/workflows/docker-image.yml
···
29
29
30
30
- name: Install cosign
31
31
if: github.event_name != 'pull_request'
32
-
uses: sigstore/cosign-installer@v3.7.0
32
+
uses: sigstore/cosign-installer@v3.8.1
33
33
with:
34
-
cosign-release: "v2.4.1"
34
+
cosign-release: "v2.4.3"
35
35
36
36
- name: Setup Docker buildx
37
37
uses: docker/setup-buildx-action@v3
+9
-39
Cargo.lock
+9
-39
Cargo.lock
···
1440
1440
1441
1441
[[package]]
1442
1442
name = "nu-ansi-term"
1443
-
version = "0.46.0"
1443
+
version = "0.50.1"
1444
1444
source = "registry+https://github.com/rust-lang/crates.io-index"
1445
-
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
1445
+
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
1446
1446
dependencies = [
1447
-
"overload",
1448
-
"winapi",
1447
+
"windows-sys 0.52.0",
1449
1448
]
1450
1449
1451
1450
[[package]]
···
1525
1524
version = "1.20.2"
1526
1525
source = "registry+https://github.com/rust-lang/crates.io-index"
1527
1526
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
1528
-
1529
-
[[package]]
1530
-
name = "overload"
1531
-
version = "0.1.1"
1532
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1533
-
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
1534
1527
1535
1528
[[package]]
1536
1529
name = "parking"
···
1751
1744
1752
1745
[[package]]
1753
1746
name = "ring"
1754
-
version = "0.17.8"
1747
+
version = "0.17.13"
1755
1748
source = "registry+https://github.com/rust-lang/crates.io-index"
1756
-
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
1749
+
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
1757
1750
dependencies = [
1758
1751
"cc",
1759
1752
"cfg-if",
1760
1753
"getrandom",
1761
1754
"libc",
1762
-
"spin",
1763
1755
"untrusted",
1764
1756
"windows-sys 0.52.0",
1765
1757
]
···
2432
2424
2433
2425
[[package]]
2434
2426
name = "tokio"
2435
-
version = "1.43.0"
2427
+
version = "1.43.1"
2436
2428
source = "registry+https://github.com/rust-lang/crates.io-index"
2437
-
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
2429
+
checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c"
2438
2430
dependencies = [
2439
2431
"backtrace",
2440
2432
"bytes",
···
2529
2521
2530
2522
[[package]]
2531
2523
name = "tracing-subscriber"
2532
-
version = "0.3.19"
2524
+
version = "0.3.20"
2533
2525
source = "registry+https://github.com/rust-lang/crates.io-index"
2534
-
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
2526
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
2535
2527
dependencies = [
2536
2528
"nu-ansi-term",
2537
2529
"sharded-slab",
···
2739
2731
]
2740
2732
2741
2733
[[package]]
2742
-
name = "winapi"
2743
-
version = "0.3.9"
2744
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2745
-
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
2746
-
dependencies = [
2747
-
"winapi-i686-pc-windows-gnu",
2748
-
"winapi-x86_64-pc-windows-gnu",
2749
-
]
2750
-
2751
-
[[package]]
2752
-
name = "winapi-i686-pc-windows-gnu"
2753
-
version = "0.4.0"
2754
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2755
-
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
2756
-
2757
-
[[package]]
2758
2734
name = "winapi-util"
2759
2735
version = "0.1.9"
2760
2736
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2762
2738
dependencies = [
2763
2739
"windows-sys 0.59.0",
2764
2740
]
2765
-
2766
-
[[package]]
2767
-
name = "winapi-x86_64-pc-windows-gnu"
2768
-
version = "0.4.0"
2769
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2770
-
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
2771
2741
2772
2742
[[package]]
2773
2743
name = "windows-core"
+1
-1
Cargo.toml
+1
-1
Cargo.toml
···
13
13
actix-web = "4.4"
14
14
actix-files = "0.6"
15
15
actix-cors = "0.6"
16
-
tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] }
16
+
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
17
17
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "chrono"] }
18
18
serde = { version = "1.0", features = ["derive"] }
19
19
serde_json = "1.0"
+1
-1
Dockerfile
+1
-1
Dockerfile
+3
-3
README.md
+3
-3
README.md
···
8
8
9
9
## How to Run
10
10
11
-
### From Docker:
11
+
### From Docker
12
12
13
13
```bash
14
14
docker run -p 8080:8080 \
···
16
16
-e SIMPLELINK_USER=admin@example.com \
17
17
-e SIMPLELINK_PASS=your-secure-password \
18
18
-v simplelink_data:/data \
19
-
ghcr.io/waveringana/simplelink:v2
19
+
ghcr.io/waveringana/simplelink:v2.2
20
20
```
21
21
22
22
### Environment Variables
···
31
31
32
32
If `SIMPLELINK_USER` and `SIMPLELINK_PASS` are not passed, an admin-setup-token is pasted to the console and as a text file in the project root.
33
33
34
-
### From Docker Compose:
34
+
### From Docker Compose
35
35
36
36
Edit the docker-compose.yml file. It comes included with a PostgreSQL db configuration.
37
37
+1
-1
docker-compose.yml
+1
-1
docker-compose.yml
+3
migrations/20250219000000_extend_short_code.sql
+3
migrations/20250219000000_extend_short_code.sql
+8
-7
src/auth.rs
+8
-7
src/auth.rs
···
1
+
use crate::{error::AppError, models::Claims};
1
2
use actix_web::{dev::Payload, FromRequest, HttpRequest};
2
3
use jsonwebtoken::{decode, DecodingKey, Validation};
3
4
use std::future::{ready, Ready};
4
-
use crate::{error::AppError, models::Claims};
5
5
6
6
pub struct AuthenticatedUser {
7
7
pub user_id: i32,
···
12
12
type Future = Ready<Result<Self, Self::Error>>;
13
13
14
14
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
15
-
let auth_header = req.headers()
15
+
let auth_header = req
16
+
.headers()
16
17
.get("Authorization")
17
18
.and_then(|h| h.to_str().ok());
18
19
19
20
if let Some(auth_header) = auth_header {
20
21
if auth_header.starts_with("Bearer ") {
21
22
let token = &auth_header[7..];
22
-
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string());
23
-
23
+
let secret =
24
+
std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string());
24
25
match decode::<Claims>(
25
26
token,
26
27
&DecodingKey::from_secret(secret.as_bytes()),
27
-
&Validation::default()
28
+
&Validation::default(),
28
29
) {
29
30
Ok(token_data) => {
30
31
return ready(Ok(AuthenticatedUser {
···
35
36
}
36
37
}
37
38
}
38
-
39
39
ready(Err(AppError::Unauthorized))
40
40
}
41
-
}
41
+
}
42
+
+1
-5
src/handlers.rs
+1
-5
src/handlers.rs
···
131
131
Ok(())
132
132
}
133
133
134
-
fn validate_url(url: &String) -> Result<(), AppError> {
134
+
fn validate_url(url: &str) -> Result<(), AppError> {
135
135
if url.is_empty() {
136
136
return Err(AppError::InvalidInput("URL cannot be empty".to_string()));
137
137
}
···
707
707
WHERE link_id = $1
708
708
GROUP BY DATE(created_at)
709
709
ORDER BY DATE(created_at) ASC
710
-
LIMIT 30
711
710
"#,
712
711
)
713
712
.bind(link_id)
···
724
723
WHERE link_id = ?
725
724
GROUP BY DATE(created_at)
726
725
ORDER BY DATE(created_at) ASC
727
-
LIMIT 30
728
726
"#,
729
727
)
730
728
.bind(link_id)
···
789
787
AND query_source != ''
790
788
GROUP BY DATE(created_at), query_source
791
789
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
792
-
LIMIT 300
793
790
"#,
794
791
)
795
792
.bind(link_id)
···
809
806
AND query_source != ''
810
807
GROUP BY DATE(created_at), query_source
811
808
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
812
-
LIMIT 300
813
809
"#,
814
810
)
815
811
.bind(link_id)
+102
src/main.rs
+102
src/main.rs
···
1
1
use actix_cors::Cors;
2
2
use actix_web::{web, App, HttpResponse, HttpServer};
3
3
use anyhow::Result;
4
+
use clap::Parser;
4
5
use rust_embed::RustEmbed;
5
6
use simplelink::check_and_generate_admin_token;
7
+
use simplelink::models::DatabasePool;
6
8
use simplelink::{create_db_pool, run_migrations};
7
9
use simplelink::{handlers, AppState};
8
10
use sqlx::{Postgres, Sqlite};
···
24
26
}
25
27
None => HttpResponse::NotFound().body("404 Not Found"),
26
28
}
29
+
}
30
+
31
+
async fn create_initial_links(pool: &DatabasePool) -> Result<()> {
32
+
if let Ok(links) = std::env::var("INITIAL_LINKS") {
33
+
for link_entry in links.split(';') {
34
+
let parts: Vec<&str> = link_entry.split(',').collect();
35
+
if parts.len() >= 2 {
36
+
let url = parts[0];
37
+
let code = parts[1];
38
+
39
+
match pool {
40
+
DatabasePool::Postgres(pool) => {
41
+
sqlx::query(
42
+
"INSERT INTO links (original_url, short_code, user_id)
43
+
VALUES ($1, $2, $3)
44
+
ON CONFLICT (short_code)
45
+
DO UPDATE SET short_code = EXCLUDED.short_code
46
+
WHERE links.original_url = EXCLUDED.original_url",
47
+
)
48
+
.bind(url)
49
+
.bind(code)
50
+
.bind(1)
51
+
.execute(pool)
52
+
.await?;
53
+
}
54
+
DatabasePool::Sqlite(pool) => {
55
+
// First check if the exact combination exists
56
+
let exists = sqlx::query_scalar::<_, bool>(
57
+
"SELECT EXISTS(
58
+
SELECT 1 FROM links
59
+
WHERE original_url = ?1
60
+
AND short_code = ?2
61
+
)",
62
+
)
63
+
.bind(url)
64
+
.bind(code)
65
+
.fetch_one(pool)
66
+
.await?;
67
+
68
+
// Only insert if the exact combination doesn't exist
69
+
if !exists {
70
+
sqlx::query(
71
+
"INSERT INTO links (original_url, short_code, user_id)
72
+
VALUES (?1, ?2, ?3)",
73
+
)
74
+
.bind(url)
75
+
.bind(code)
76
+
.bind(1)
77
+
.execute(pool)
78
+
.await?;
79
+
info!("Created initial link: {} -> {} for user_id: 1", code, url);
80
+
} else {
81
+
info!("Skipped existing link: {} -> {} for user_id: 1", code, url);
82
+
}
83
+
}
84
+
}
85
+
}
86
+
}
87
+
}
88
+
Ok(())
89
+
}
90
+
91
+
async fn create_admin_user(pool: &DatabasePool, email: &str, password: &str) -> Result<()> {
92
+
use argon2::{
93
+
password_hash::{rand_core::OsRng, SaltString},
94
+
Argon2, PasswordHasher,
95
+
};
96
+
97
+
let salt = SaltString::generate(&mut OsRng);
98
+
let argon2 = Argon2::default();
99
+
let password_hash = argon2
100
+
.hash_password(password.as_bytes(), &salt)
101
+
.map_err(|e| anyhow::anyhow!("Password hashing error: {}", e))?
102
+
.to_string();
103
+
104
+
match pool {
105
+
DatabasePool::Postgres(pool) => {
106
+
sqlx::query(
107
+
"INSERT INTO users (email, password_hash)
108
+
VALUES ($1, $2)
109
+
ON CONFLICT (email) DO NOTHING",
110
+
)
111
+
.bind(email)
112
+
.bind(&password_hash)
113
+
.execute(pool)
114
+
.await?;
115
+
}
116
+
DatabasePool::Sqlite(pool) => {
117
+
sqlx::query(
118
+
"INSERT OR IGNORE INTO users (email, password_hash)
119
+
VALUES (?1, ?2)",
120
+
)
121
+
.bind(email)
122
+
.bind(&password_hash)
123
+
.execute(pool)
124
+
.await?;
125
+
}
126
+
}
127
+
info!("Created admin user: {}", email);
128
+
Ok(())
27
129
}
28
130
29
131
#[actix_web::main]