71 Commits
Author SHA1 Message Date
phoenix 010ee6aef5 Adding license (#23)
schedtxt_auth Build / Rustfmt (push) Successful in 57s
schedtxt_auth Build / Test Suite (push) Successful in 1m23s
schedtxt_auth Build / Check (push) Successful in 1m23s
schedtxt_auth Build / build (push) Successful in 1m20s
schedtxt_auth Build / Clippy (push) Successful in 1m47s
Reviewed-on: #23
2026-07-14 15:27:37 -04:00
phoenix 039609a99c Dependency name change (#22)
schedtxt_auth Build / Rustfmt (push) Successful in 46s
schedtxt_auth Build / Check (push) Successful in 1m35s
schedtxt_auth Build / Clippy (push) Successful in 1m4s
schedtxt_auth Build / Test Suite (push) Successful in 2m6s
schedtxt_auth Build / build (push) Successful in 4m28s
Reviewed-on: #22
2026-07-13 17:24:39 -04:00
phoenix b3899c1480 Update rust (#21)
textsender_auth Build / Rustfmt (push) Successful in 44s
textsender_auth Build / Check (push) Successful in 1m42s
textsender_auth Build / Test Suite (push) Successful in 2m5s
textsender_auth Build / Clippy (push) Successful in 1m38s
textsender_auth Build / build (push) Successful in 3m5s
Reviewed-on: phoenix/textsender_auth#21
2026-07-10 17:42:38 -04:00
phoenix 3423c1eeb1 Update rust (#20)
Rust Build / Rustfmt (push) Successful in 48s
Rust Build / Check (push) Successful in 1m30s
Rust Build / Test Suite (push) Successful in 2m29s
Rust Build / Clippy (push) Successful in 1m47s
Rust Build / build (push) Successful in 2m13s
Rust Build / Check (pull_request) Successful in 3m34s
Rust Build / Rustfmt (pull_request) Successful in 36s
Rust Build / Clippy (pull_request) Successful in 38s
Reviewed-on: phoenix/textsender_auth#20
2026-07-06 22:03:16 -04:00
phoenix 21bfaf7657 Update dependencies (#19)
Rust Build / Rustfmt (push) Successful in 50s
Rust Build / Check (push) Successful in 1m7s
Rust Build / Test Suite (push) Failing after 2m2s
Rust Build / Clippy (push) Successful in 1m27s
Rust Build / build (push) Successful in 1m30s
Reviewed-on: phoenix/textsender_auth#19
2026-07-04 20:00:46 -04:00
phoenix 9dba08f179 Get User profile endpoint (#18)
Rust Build / Rustfmt (push) Successful in 38s
Rust Build / Test Suite (push) Failing after 1m43s
Rust Build / Check (push) Successful in 1m52s
Rust Build / Clippy (push) Successful in 1m10s
Rust Build / build (push) Successful in 2m4s
Rust Build / Rustfmt (pull_request) Successful in 1m9s
Rust Build / Clippy (pull_request) Successful in 1m17s
Rust Build / Check (pull_request) Successful in 1m36s
Reviewed-on: phoenix/textsender_auth#18
2026-07-01 22:37:25 -04:00
phoenix 438b19e170 Update dependencies (#17)
Rust Build / Rustfmt (push) Successful in 44s
Rust Build / Clippy (push) Successful in 1m10s
Rust Build / Test Suite (push) Failing after 2m0s
Rust Build / build (push) Successful in 2m7s
Rust Build / Check (push) Successful in 1m12s
Rust Build / Check (pull_request) Has been cancelled
Rust Build / Clippy (pull_request) Has been cancelled
Rust Build / Rustfmt (pull_request) Has been cancelled
Reviewed-on: phoenix/textsender_auth#17
2026-06-27 17:47:19 -04:00
phoenix 69d908bac3 Update (#16)
Rust Build / Rustfmt (push) Successful in 1m26s
Rust Build / build (push) Successful in 1m45s
Rust Build / Check (push) Successful in 2m14s
Rust Build / Test Suite (push) Successful in 2m18s
Rust Build / Clippy (push) Successful in 1m40s
Rust Build / Rustfmt (pull_request) Successful in 54s
Rust Build / Check (pull_request) Successful in 1m28s
Rust Build / Clippy (pull_request) Successful in 1m41s
Reviewed-on: phoenix/textsender_auth#16
2026-06-14 18:51:51 -04:00
phoenix 7d2faba4f2 Update packages (#15)
Rust Build / Rustfmt (push) Successful in 59s
Rust Build / Test Suite (push) Successful in 1m50s
Rust Build / Clippy (push) Successful in 1m12s
Rust Build / Check (push) Successful in 2m30s
Rust Build / build (push) Successful in 1m45s
Reviewed-on: phoenix/textsender_auth#15
2026-06-13 19:06:51 -04:00
phoenix e9b59fc1dd Update packages (#14)
Rust Build / Rustfmt (push) Successful in 59s
Rust Build / Test Suite (push) Successful in 1m34s
Rust Build / Check (push) Successful in 2m1s
Rust Build / Clippy (push) Successful in 1m13s
Rust Build / build (push) Successful in 2m17s
Reviewed-on: phoenix/textsender_auth#14
2026-06-13 17:01:54 -04:00
phoenix 1ab7bd4ae1 gitignore cleanup (#13)
Rust Build / Rustfmt (push) Successful in 41s
Rust Build / Check (push) Successful in 1m31s
Rust Build / Clippy (push) Successful in 1m46s
Rust Build / Test Suite (push) Successful in 2m35s
Rust Build / build (push) Successful in 1m49s
Rust Build / Rustfmt (pull_request) Successful in 40s
Rust Build / Check (pull_request) Successful in 1m44s
Rust Build / Test Suite (pull_request) Successful in 1m31s
Rust Build / Clippy (pull_request) Successful in 2m10s
Rust Build / build (pull_request) Successful in 3m26s
Reviewed-on: phoenix/textsender_auth#13
2026-06-13 12:54:17 -04:00
phoenix d83591c8a4 Updating package name (#12)
Rust Build / Rustfmt (push) Successful in 52s
Rust Build / Check (push) Successful in 1m21s
Rust Build / Test Suite (push) Successful in 1m52s
Rust Build / Clippy (push) Successful in 2m0s
Rust Build / build (push) Successful in 1m42s
Reviewed-on: phoenix/textsender-auth#12
2026-06-13 12:42:25 -04:00
phoenix 0cab4e8530 Update postgresql port (#11)
Rust Build / Test Suite (push) Successful in 1m6s
Rust Build / Check (push) Successful in 1m7s
Rust Build / Rustfmt (push) Successful in 1m59s
Rust Build / Clippy (push) Successful in 1m43s
Rust Build / build (push) Successful in 2m28s
Reviewed-on: phoenix/textsender-auth#11
2026-06-13 11:26:43 -04:00
phoenix b678c098cb Update name of user endpoint (#9)
Rust Build / Test Suite (push) Successful in 48s
Rust Build / Rustfmt (push) Successful in 1m23s
Rust Build / Check (push) Successful in 1m35s
Rust Build / Clippy (push) Successful in 1m13s
Rust Build / build (push) Successful in 2m57s
Rust Build / Rustfmt (pull_request) Successful in 1m15s
Rust Build / Test Suite (pull_request) Successful in 1m38s
Rust Build / Check (pull_request) Successful in 1m50s
Rust Build / Clippy (pull_request) Successful in 1m24s
Rust Build / build (pull_request) Successful in 2m59s
Reviewed-on: phoenix/textsender-auth#9
2026-06-12 17:27:46 -04:00
phoenix e3ee24c131 Update password endpoint (#8)
Rust Build / Rustfmt (push) Successful in 40s
Rust Build / Clippy (push) Successful in 1m12s
Rust Build / Check (push) Successful in 2m3s
Rust Build / Test Suite (push) Successful in 2m10s
Rust Build / build (push) Successful in 1m48s
Rust Build / Rustfmt (pull_request) Successful in 37s
Rust Build / Test Suite (pull_request) Successful in 1m10s
Rust Build / Check (pull_request) Successful in 1m30s
Rust Build / Clippy (pull_request) Successful in 1m36s
Rust Build / build (pull_request) Successful in 3m29s
Reviewed-on: phoenix/textsender-auth#8
2026-06-12 13:08:34 -04:00
phoenix 8e0b8b737b Refresh endpoint (#7)
Rust Build / Rustfmt (push) Successful in 1m0s
Rust Build / Clippy (pull_request) Successful in 1m48s
Rust Build / Check (push) Successful in 1m10s
Rust Build / Rustfmt (pull_request) Successful in 29s
Rust Build / Clippy (push) Successful in 1m4s
Rust Build / Test Suite (pull_request) Successful in 1m1s
Rust Build / Check (pull_request) Successful in 1m45s
Rust Build / build (pull_request) Successful in 3m11s
Rust Build / Test Suite (push) Successful in 2m25s
Rust Build / build (push) Successful in 1m41s
Reviewed-on: phoenix/textsender-auth#7
2026-06-12 10:51:40 -04:00
phoenix e5e34b4ad3 Service login (#6)
Rust Build / build (push) Successful in 1m42s
Rust Build / Clippy (push) Successful in 1m52s
Rust Build / Rustfmt (pull_request) Successful in 53s
Rust Build / Test Suite (pull_request) Successful in 1m25s
Rust Build / Clippy (pull_request) Successful in 1m40s
Rust Build / build (pull_request) Successful in 1m36s
Rust Build / Check (pull_request) Successful in 1m47s
Rust Build / Rustfmt (push) Successful in 1m24s
Rust Build / Check (push) Successful in 1m29s
Rust Build / Test Suite (push) Successful in 1m29s
Reviewed-on: phoenix/textsender-auth#6
2026-06-11 18:02:23 -04:00
phoenix 2385307fd3 Register service user endpoint (#5)
Rust Build / Rustfmt (push) Successful in 55s
Rust Build / Check (push) Successful in 1m33s
Rust Build / Test Suite (push) Successful in 1m33s
Rust Build / build (push) Successful in 1m38s
Rust Build / Clippy (push) Successful in 2m39s
Rust Build / Rustfmt (pull_request) Successful in 4m0s
Rust Build / Test Suite (pull_request) Successful in 5m18s
Rust Build / Clippy (pull_request) Successful in 1m25s
Rust Build / Check (pull_request) Successful in 5m51s
Rust Build / build (pull_request) Successful in 2m7s
Reviewed-on: phoenix/textsender-auth#5
2026-06-11 14:36:10 -04:00
phoenix 04e1fbfc7a Add Login (#4)
Rust Build / Check (push) Successful in 1m21s
Rust Build / Rustfmt (push) Successful in 31s
Rust Build / Test Suite (push) Successful in 2m21s
Rust Build / Clippy (push) Successful in 2m3s
Rust Build / build (push) Successful in 1m55s
Rust Build / Rustfmt (pull_request) Successful in 44s
Rust Build / Check (pull_request) Successful in 1m21s
Rust Build / Clippy (pull_request) Successful in 1m56s
Rust Build / build (pull_request) Successful in 1m57s
Rust Build / Test Suite (pull_request) Failing after 3m35s
Reviewed-on: phoenix/textsender-auth#4
2026-06-10 00:13:55 -04:00
phoenix 27f4443818 Fix docker (#3)
Rust Build / Test Suite (push) Successful in 1m20s
Rust Build / Check (push) Successful in 1m31s
Rust Build / build (push) Successful in 1m38s
Rust Build / Rustfmt (push) Successful in 33s
Rust Build / Clippy (push) Successful in 1m27s
Rust Build / Check (pull_request) Successful in 1m16s
Rust Build / Test Suite (pull_request) Successful in 1m34s
Rust Build / Rustfmt (pull_request) Successful in 36s
Rust Build / Clippy (pull_request) Successful in 1m53s
Rust Build / build (pull_request) Successful in 1m47s
Reviewed-on: phoenix/textsender-auth#3
2026-06-07 18:45:08 -04:00
phoenix 5edeba0799 Merge pull request 'Using rust' (#2) from v0.2.0 into main
Reviewed-on: phoenix/textsender-auth#2
2026-05-31 18:18:42 -04:00
phoenix f596a1bbf9 docker-compose fix
Rust Build / Check (push) Successful in 34s
Rust Build / Test Suite (push) Successful in 50s
Rust Build / Clippy (push) Successful in 26s
Rust Build / build (push) Successful in 43s
Rust Build / Rustfmt (push) Successful in 25s
2026-05-31 18:09:56 -04:00
phoenix 7e7e561008 cargo fmt
Rust Build / Check (push) Successful in 25s
Rust Build / Test Suite (push) Successful in 37s
Rust Build / Rustfmt (push) Successful in 26s
Rust Build / Clippy (push) Successful in 29s
Rust Build / build (push) Successful in 26s
2026-05-31 18:00:58 -04:00
phoenix a19262d9a1 Adding docker 2026-05-31 18:00:23 -04:00
phoenix 1cea3442ff Adding sample .env.local
Rust Build / Test Suite (push) Successful in 26s
Rust Build / Rustfmt (push) Successful in 24s
Rust Build / Clippy (push) Successful in 25s
Rust Build / Check (push) Successful in 24s
Rust Build / build (push) Successful in 23s
2026-05-31 17:55:55 -04:00
phoenix 1ae7dc8aab Adding init code
Rust Build / Check (push) Successful in 28s
Rust Build / Test Suite (push) Successful in 38s
Rust Build / Rustfmt (push) Successful in 29s
Rust Build / build (push) Successful in 25s
Rust Build / Clippy (push) Successful in 53s
2026-05-31 17:53:03 -04:00
phoenix dbebaaed39 Cargo.toml cleanup and cargo fmt
Rust Build / Check (push) Successful in 25s
Rust Build / Test Suite (push) Successful in 32s
Rust Build / Rustfmt (push) Successful in 30s
Rust Build / Clippy (push) Successful in 27s
Rust Build / build (push) Successful in 28s
2026-05-31 17:46:01 -04:00
phoenix be379543bb Adding code to repo module
Rust Build / Check (push) Successful in 24s
Rust Build / Clippy (push) Successful in 32s
Rust Build / build (push) Successful in 26s
Rust Build / Test Suite (push) Successful in 25s
Rust Build / Rustfmt (push) Successful in 26s
2026-05-31 17:45:15 -04:00
phoenix b60ce6ee53 Adding more callers code 2026-05-31 17:45:05 -04:00
phoenix 6dc22d0a1d Adding salt table and updating user schema 2026-05-31 17:26:55 -04:00
phoenix 776740e087 Adding caller code 2026-05-31 17:25:05 -04:00
phoenix 6713508de9 Adding vacant callers module 2026-05-31 16:51:24 -04:00
phoenix 112b94f59e Changed name and cargo fmt
Rust Build / Test Suite (push) Successful in 35s
Rust Build / Check (push) Successful in 36s
Rust Build / Clippy (push) Successful in 33s
Rust Build / Rustfmt (push) Successful in 37s
Rust Build / build (push) Successful in 25s
2026-05-31 16:48:16 -04:00
phoenix 775410907b Adding token_stuff repo
Rust Build / Check (push) Successful in 27s
Rust Build / Test Suite (push) Successful in 36s
Rust Build / Rustfmt (push) Successful in 26s
Rust Build / Clippy (push) Successful in 29s
Rust Build / build (push) Successful in 28s
2026-05-31 16:46:32 -04:00
phoenix 2badd0ca4d Adding repo module
Rust Build / Rustfmt (push) Successful in 33s
Rust Build / Check (push) Successful in 30s
Rust Build / Test Suite (push) Successful in 1m4s
Rust Build / build (push) Successful in 36s
Rust Build / Clippy (push) Successful in 37s
2026-05-31 16:28:51 -04:00
phoenix c053a0a6cf Adding hashing module
Rust Build / Check (push) Successful in 33s
Rust Build / Rustfmt (push) Successful in 37s
Rust Build / Test Suite (push) Successful in 35s
Rust Build / Clippy (push) Successful in 32s
Rust Build / build (push) Successful in 24s
2026-05-31 16:17:34 -04:00
phoenix 42dd25691c Adding db module
Rust Build / Test Suite (push) Successful in 29s
Rust Build / Check (push) Successful in 36s
Rust Build / Rustfmt (push) Successful in 28s
Rust Build / Clippy (push) Successful in 35s
Rust Build / build (push) Successful in 30s
2026-05-31 16:11:34 -04:00
phoenix 26842175fe Adding config module 2026-05-31 16:11:09 -04:00
phoenix dd46122270 bump: textsender-models
Rust Build / Test Suite (push) Successful in 53s
Rust Build / Clippy (push) Successful in 26s
Rust Build / Rustfmt (push) Successful in 57s
Rust Build / Check (push) Successful in 32s
Rust Build / build (push) Successful in 31s
2026-05-31 15:48:33 -04:00
phoenix cfae02d68f Removing env module 2026-05-31 15:47:24 -04:00
phoenix b4cc08bfaa Adding env module
Rust Build / Test Suite (push) Successful in 34s
Rust Build / Clippy (push) Successful in 34s
Rust Build / Rustfmt (push) Successful in 39s
Rust Build / build (push) Successful in 25s
Rust Build / Check (push) Successful in 34s
2026-05-30 23:39:38 -04:00
phoenix 37e411f58f Adding dependencies
Rust Build / Check (push) Successful in 24s
Rust Build / Test Suite (push) Successful in 44s
Rust Build / Rustfmt (push) Successful in 50s
Rust Build / Clippy (push) Successful in 27s
Rust Build / build (push) Successful in 23s
2026-05-30 23:30:28 -04:00
phoenix 1a6be5f541 Updating main rs
Rust Build / Check (push) Successful in 25s
Rust Build / Test Suite (push) Successful in 35s
Rust Build / Rustfmt (push) Successful in 27s
Rust Build / Clippy (push) Successful in 29s
Rust Build / build (push) Successful in 25s
2026-05-30 23:29:21 -04:00
phoenix f6c43c283a Adding workflow file
Rust Build / Check (push) Successful in 27s
Rust Build / Rustfmt (push) Successful in 30s
Rust Build / Test Suite (push) Successful in 1m8s
Rust Build / build (push) Successful in 27s
Rust Build / Clippy (push) Successful in 36s
2026-05-30 23:18:56 -04:00
phoenix 601193738b Adding textsender_models 2026-05-30 23:15:41 -04:00
phoenix 9d70311648 Updating gitignore 2026-05-30 23:11:40 -04:00
phoenix 8d358356b9 Adding rust files 2026-05-30 23:11:04 -04:00
phoenix bceadfe7a6 Blank readme 2026-05-30 23:04:00 -04:00
phoenix e2f3dbbb32 Removing workflow file 2026-05-30 23:03:28 -04:00
phoenix 0d1c0961e2 Removing go files 2026-05-30 23:02:55 -04:00
phoenix 609cc22b51 Updated docker (#1)
Go / build (push) Successful in 7s
Go / Test (push) Successful in 18s
Reviewed-on: phoenix/textsender-auth#1
2026-05-29 17:31:18 -04:00
phoenixandphoenix 2da394c7a4 update go (#42)
Reviewed-on: phoenix/textsender-auth#42
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-05-03 17:07:10 -04:00
phoenixandphoenix f556d729a5 Update go (#40)
Reviewed-on: phoenix/textsender-auth#40
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-04-04 22:09:10 -04:00
phoenixandphoenix e06a54947f tsk-6: Populate IssuedAt for LoginResult (#39)
Closes #36

Reviewed-on: phoenix/textsender-auth#39
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-04 02:00:39 +00:00
phoenixandphoenix 689762c72a tsk-35: Update name of user (#38)
Closes #35

Reviewed-on: phoenix/textsender-auth#38
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 22:41:08 +00:00
phoenixandphoenix de171b790d tsk-34: Update last login of user (#37)
Closes #34

Reviewed-on: phoenix/textsender-auth#37
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-31 23:13:02 +00:00
phoenixandphoenix d34cad033d tsk-30: Update password (#33)
Closes #30

Reviewed-on: phoenix/textsender-auth#33
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-26 21:14:40 +00:00
phoenix 760a778a27 next_changes (#32)
Reviewed-on: phoenix/textsender-auth#32
2025-12-22 22:42:45 +00:00
phoenix 7597e05243 textsender-models version bump 2025-12-22 17:40:08 -05:00
phoenixandphoenix 636b0280c7 tsk-10: CORS support (#28)
Closes #10

Reviewed-on: phoenix/textsender-auth#28
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-21 00:28:08 +00:00
phoenixandphoenix 223b7919f9 tsk-29: Expand LoginResult (#31)
Closes #29

Reviewed-on: phoenix/textsender-auth#31
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-16 19:09:18 +00:00
phoenix 66221e504c tsk-10: Got it working 2025-12-13 15:18:06 -05:00
phoenix 093888f7fb tsk-10: Package change 2025-12-13 15:17:55 -05:00
phoenix 225dc0c963 tsk-10: Added cors 2025-12-11 17:29:34 -05:00
phoenix c7b45952fe tsk-10: go mod 2025-12-11 17:29:12 -05:00
phoenix f305e5b2e9 tsk-10: Added cors package 2025-12-11 16:53:30 -05:00
phoenixandphoenix 9f5029dd6e tsk-21: Disable registration (#27)
Closes #21

Reviewed-on: phoenix/textsender-auth#27
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-27 01:01:32 +00:00
phoenixandphoenix d01fae0775 tsk-25: go version bump (#26)
Closes #25

Reviewed-on: phoenix/textsender-auth#26
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-24 21:10:44 +00:00
phoenixandphoenix 371dc38958 tsk-20: Obtain refresh token (#24)
Closes #20

Reviewed-on: phoenix/textsender-auth#24
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-21 20:49:38 +00:00
phoenixandphoenix 0a00282ba7 tsk-19: Service login (#23)
Closes #19

Reviewed-on: phoenix/textsender-auth#23
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-21 18:42:58 +00:00
phoenixandphoenix c340693b24 tsk-18: Create service user (#22)
Closes #18

Reviewed-on: phoenix/textsender-auth#22
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-20 19:11:36 +00:00
54 changed files with 6135 additions and 2090 deletions
+10 -6
View File
@@ -1,7 +1,11 @@
SECRET_KEY=NULqYIzgt28bTiyziCd7IOO7b6LnWDW! SECRET_KEY=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_auth_db DB_AUTH_NAME=schedtxt_auth_db
DB_USER=textsender_auth DB_AUTH_USER=schedtxt_auth
DB_PASSWORD=password DB_AUTH_PASSWORD=password
DB_HOST=auth_db DB_AUTH_HOST=auth_db
DB_PORT=5432 DB_AUTH_PORT=5432
DB_SSLMODE=disable DB_AUTH_SSLMODE=disable
DATABASE_URL=postgres://${DB_AUTH_USER}:${DB_AUTH_PASSWORD}@${DB_AUTH_HOST}:${DB_AUTH_PORT}/${DB_AUTH_NAME}
ENABLE_REGISTRATION=true
ALLOWED_ORIGINS="http://schedtxt.com,http://localhost:5173,http://localhost:9080"
APP_ENV=production
+11
View File
@@ -0,0 +1,11 @@
SECRET_KEY=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_AUTH_NAME=schedtxt_auth_db
DB_AUTH_USER=schedtxt_auth
DB_AUTH_PASSWORD=password
DB_AUTH_HOST=localhost
DB_AUTH_PORT=5432
DB_AUTH_SSLMODE=disable
DATABASE_URL=postgres://${DB_AUTH_USER}:${DB_AUTH_PASSWORD}@${DB_AUTH_HOST}:${DB_AUTH_PORT}/${DB_AUTH_NAME}
ENABLE_REGISTRATION=true
ALLOWED_ORIGINS="http://schedtxt.com,http://localhost:5173,http://localhost:9080"
APP_ENV=production
-7
View File
@@ -1,7 +0,0 @@
SECRET_KEY=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_auth_db
DB_USER=textsender_auth
DB_PASSWORD=yEDjWZCH2vdctjn!
DB_HOST=localhost
DB_PORT=5432
DB_SSLMODE=disable
+72
View File
@@ -0,0 +1,72 @@
name: pr Build
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo check
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add clippy
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo clippy -- -D warnings
-126
View File
@@ -1,126 +0,0 @@
name: Go
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-24.04 # You can change this to macos-latest or windows-latest if needed
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25.3' # You can specify a specific version or 'stable'
- name: Build
run: |
echo "Initializing config"
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key
chmod 600 ~/.ssh/textsender_models_deploy_key
ssh-keyscan ${{ vars.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender_models_deploy_key
go env -w GOPRIVATE='${{ secrets.GIT_HOST_ROOT }}'
echo "Creating local .gitconfig"
touch ~/.gitconfig
cat > ~/.gitconfig << "EOF"
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = https://${{ secrets.GIT_HOST_ROOT }}
EOF
echo "Building binary"
make build
echo "Binary built"
file textsender-auth
test:
name: Test
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:18.0
env:
POSTGRES_USER: ${{ secrets.DB_TEST_USER }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
POSTGRES_DB: ${{ secrets.DB_TEST_NAME }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.25.3'
- name: Install PostgreSQL client
run: sudo apt update && sudo apt-get install -y postgresql-client
- name: Wait for PostgreSQL to be ready
run: |
for i in {1..30}; do
if pg_isready -h postgres -p 5432; then
echo "PostgreSQL is ready"
exit 0
fi
echo "Waiting for PostgreSQL... Attempt $i"
sleep 2
done
echo "PostgreSQL did not start in time"
exit 1
- name: Run tests
env:
DB_NAME: ${{ secrets.DB_TEST_NAME }}
DB_USER: ${{ secrets.DB_TEST_USER }}
DB_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
DB_HOST: postgres
DB_PORT: 5432
DB_SSLMODE: disable
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
echo "Parent directory"
echo `pwd`
echo "SECRET_KEY=$SECRET_KEY" >> .env
echo "DB_NAME=$DB_NAME" > .env
echo "DB_USER=$DB_USER" >> .env
echo "DB_PASSWORD=$DB_PASSWORD" >> .env
echo "DB_HOST=$DB_HOST" >> .env
echo "DB_PORT=$DB_PORT" >> .env
echo "DB_SSLMODE=$DB_SSLMODE" >> .env
go test -v ./...
# - name: Run gofmt (optional)
# Uncomment to check code formatting with gofmt
# run: |
# if [ -n "$(gofmt -l.)" ]; then
# echo "Go code is not formatted. Please run 'gofmt -w.' to fix it."
# exit 1
# fi
# - name: Run golint (optional)
# Uncomment to check code style with golint
# run: |
# if [ -n "$(golint./...)" ]; then
# echo "Go code has style issues. Please run 'golint./...' to see them."
# exit 1
# fi
+150
View File
@@ -0,0 +1,150 @@
name: schedtxt_auth Build
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo check
test:
name: Test Suite
runs-on: ubuntu-24.04
# --- Add database service definition ---
services:
postgres:
image: postgres:18.4-alpine
env:
# Use secrets for DB init, with fallbacks for flexibility
POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}
POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }}
POSTGRES_PORT: ${{ secrets.DB_PORT || 5432 }}
# Options to wait until the database is ready
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
# --- Add this step for explicit verification ---
- name: Verify Docker Environment
run: |
echo "Runner User Info:"
id
echo "Checking Docker Version:"
docker --version
echo "Checking Docker Daemon Status (info):"
docker info
echo "Checking Docker Daemon Status (ps):"
docker ps -a
echo "Docker environment check complete."
# NOTE: Do NOT use continue-on-error here.
# If Docker isn't working as expected, the job SHOULD fail here.
- name: Run tests
env:
# Define DATABASE_URL for tests to use
DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }}
RUST_LOG: info # Optional: configure test log level
SECRET_KEY: ${{ secrets.TOKEN_SECRET_KEY }}
# Make SSH agent available if tests fetch private dependencies
SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }}
ENABLE_REGISTRATION: 'TRUE'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo test
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: rustup component add clippy
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo clippy -- -D warnings
build:
name: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo build --release
+1 -3
View File
@@ -1,7 +1,5 @@
/textsender-auth
.env .env
.env.local .env.local
.env.docker .env.docker
/vendor /target
Generated
+2443
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "schedtxt_auth"
version = "0.2.4"
edition = "2024"
license = "MIT"
description = "Auth API for sending text messages"
rust-version = "1.97"
[dependencies]
axum = { version = "0.8.9" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150" }
tokio = { version = "1.52.3", features = ["rt-multi-thread"] }
tracing-subscriber = { version = "0.3.23" }
tower-http = { version = "0.7.0", features = ["cors"] }
sqlx = { version = "0.9.0", features = ["runtime-tokio", "tls-native-tls", "postgres", "time", "uuid"] }
uuid = { version = "1.23.5", features = ["v4", "serde"] }
argon2 = { version = "0.5.3", features = ["std"] }
time = { version = "0.3.53", features = ["macros", "serde"] }
josekit = { version = "0.10.3" }
utoipa = { version = "5.5.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
schedtxt_models = { git = "ssh://git@git.kundeng.us/phoenix/schedtxt_models.git", tag = "v0.5.3", features = ["config", "user"] }
[dev-dependencies]
tower = { version = "0.5.3", features = ["full"] }
url = { version = "2.5.8" }
+25 -34
View File
@@ -1,53 +1,44 @@
# Multi-stage Dockerfile for Go application FROM rust:1.97 as builder
FROM golang:1.25.3 AS builder
WORKDIR /app WORKDIR /usr/src/app
# Install build dependencies if needed (e.g., git for cloning)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl3 \
ca-certificates \ ca-certificates \
openssh-client git openssh-client git \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m 0700 ~/.ssh && \ RUN mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Configure Git to use SSH for GitHub COPY Cargo.toml Cargo.lock ./
RUN git config --global url."ssh://git@git.kundeng.us".insteadOf "https://git.kundeng.us"
# Set up the Go environment for private modules
ENV GOPRIVATE=git.kundeng.us
# Copy go mod and sum files
COPY go.mod go.sum ./
RUN --mount=type=ssh mkdir src && \ RUN --mount=type=ssh mkdir src && \
go mod download echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
cargo build --release --quiet && \
rm -rf src target/release/deps/schedtxt_auth*
# Copy source code COPY src ./src
COPY ./cmd ./cmd COPY .env ./.env
COPY ./internal ./internal COPY migrations ./migrations
COPY ./Makefile .
COPY ./.env .
COPY ./migrations ./migrations
COPY ./docs ./docs
# Build the application RUN --mount=type=ssh \
RUN CGO_ENABLED=0 GOOS=linux make build cargo build --release --quiet
# Runtime stage FROM debian:trixie-slim
FROM alpine:latest AS production
RUN apk --no-cache add ca-certificates # Install runtime dependencies if needed (e.g., SSL certificates)
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
WORKDIR /root/ WORKDIR /usr/local/bin
# Copy the pre-built binary file from the previous stage COPY --from=builder /usr/src/app/target/release/schedtxt_auth .
COPY --from=builder /app/textsender-auth .
COPY --from=builder /app/.env ./ COPY --from=builder /usr/src/app/.env .
COPY --from=builder /app/migrations ./migrations COPY --from=builder /usr/src/app/migrations ./migrations
# Expose port
EXPOSE 9080 EXPOSE 9080
# Command to run the executable # Set the command to run your application
CMD ["./textsender-auth"] CMD ["./schedtxt_auth"]
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
-21
View File
@@ -1,21 +0,0 @@
VERSION ?= $(shell git describe --tags 2>/dev/null || echo "dev")
COMMIT ?= $(shell git rev-parse --short HEAD)
BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
GO_VERSION ?= $(shell go version | awk '{print $$3}')
.PHONY: build
build:
go build -ldflags="\
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.Version=$(VERSION)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.BuildTime=$(BUILD_TIME)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.Commit=$(COMMIT)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.GoVersion=$(GO_VERSION)'" \
-o textsender-auth cmd/api/main.go
.PHONY: install
install:
go install -ldflags="\
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.Version=$(VERSION)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.BuildTime=$(BUILD_TIME)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.Commit=$(COMMIT)' \
-X 'git.kundeng.us/phoenix/textsender-auth/internal/version.GoVersion=$(GO_VERSION)'"
+12 -21
View File
@@ -1,31 +1,22 @@
# textsender-auth # schedtxt_auth
A service that handles the authorization aspect of the textsender project. Auth API for `schedtxt` project.
## Getting started ## Getting started
Assumes that the postgresql database has already been created with privileges to Quickest way to get started is with docker, make sure that it is installed. Copy over
create and drop databases. Copy the `.env.sample` file to `.env`. Within the `.env` the `.env.docker.sample` file and name it `.env`.
file, update the database keys. The `SECRET_KEY` is used for token generation.
For `SECRET_KEY` provide a key that is at least 32-characters. This is used for token
creation. Additionally, provide credentials for the database for the `DB_*` keys.
### Building api Make sure that `ENABLE_REGISTRATION` is set to true, otherwise registration will not work.
Build the container
``` ```
make build docker compose build
``` ```
Bring up the container
### Resetting the database
``` ```
./textsender-auth -reset-db docker compose up
``` ```
Generate API documentation
```
go install github.com/swaggo/swag/cmd/swag@latest
go get -u github.com/swaggo/http-swagger/v2
swag init --generalInfo main.go --dir ./cmd/api,./internal/handler --output docs/ --parseDependency --parseInternal
```
The API documentation can be viewed from `http://localhost:9080/swagger/index.html`.
-124
View File
@@ -1,124 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/swaggo/http-swagger/v2"
_ "git.kundeng.us/phoenix/textsender-auth/docs"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
database "git.kundeng.us/phoenix/textsender-auth/internal/db"
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
)
// @title textsender-auth
// @version 1.0
// @description Auth API to send text messages
// @host localhost:9080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description JWT Bearer Token
func main() {
cfg := config.Load()
if cfg == nil {
fmt.Println("Error initializing config")
os.Exit(-1)
}
db, err := database.NewDatabase(cfg.GetDBConnString())
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
ctx := context.Background()
if cfg.ResetDB {
if err := db.ResetDatabase(ctx); err != nil {
log.Fatalf("Failed to reset database: %v", err)
} else {
log.Println("Resetting database")
log.Println("Database reset completed. Exiting.")
}
return
} else {
if exists, err := database.TableExists(ctx, db.Pool, "users"); err == nil {
if !exists {
if err = db.ResetDatabase(ctx); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Database reset")
}
}
} else {
fmt.Println("Error:", err)
}
}
// Services
userStore := model.NewUserStore(db.Pool)
userHandler := handler.NewUserHandler(userStore)
loginHandler := handler.NewLoginHandler(userStore)
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Use(middleware.Recoverer)
router.Use(middleware.Timeout(60 * time.Second))
router.Use(mdleware.JSONContentType)
router.Method("Post", endpoint.Register, http.HandlerFunc(userHandler.Register))
router.Method("Post", endpoint.Login, http.HandlerFunc(loginHandler.Login))
router.Method("GET", "/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.Port)),
))
// Start server
server := &http.Server{
Addr: ":" + cfg.ServerPort,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
go func() {
log.Printf("Server starting on port %s", cfg.ServerPort)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
-84
View File
@@ -1,84 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path"
"testing"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/db"
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
)
var testRouter *mux.Router
func TestMain(m *testing.M) {
cfg := load()
db, err := db.NewDatabase(cfg.GetDBConnString())
if err != nil {
fmt.Println(err.Error())
panic("Failed to initialize database")
}
defer db.Close()
ctx := context.Background()
err = db.ResetDatabase(ctx)
if err != nil {
fmt.Println(err.Error())
panic("Failed to initialize database")
}
userStore := model.NewUserStore(db.Pool)
userHandler := handler.NewUserHandler(userStore)
loginHandler := handler.NewLoginHandler(userStore)
testRouter = mux.NewRouter()
testRouter.HandleFunc(endpoint.Register, userHandler.Register).Methods("POST")
testRouter.HandleFunc(endpoint.Login, loginHandler.Login).Methods("POST")
code := m.Run()
os.Exit(code)
}
func load() *config.Config {
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", config.Port, "Server port")
flag.Parse()
cwd, _ := os.Getwd()
envPath := path.Join(cwd, ".env")
err := godotenv.Load(envPath)
if err != nil {
envPath = path.Join(cwd, "../..", ".env")
if err := godotenv.Load(envPath); err != nil {
panic("Error loading .env file: " + err.Error())
}
}
unpackedConnString := config.UnpackDBConnString()
dbConnString := unpackedConnString.Parse()
return &config.Config{
DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
}
}
func resetTestDB(t *testing.T) {
t.Helper()
_, err := db.Pool.Exec(context.Background(), "DELETE FROM users")
if err != nil {
t.Fatalf("Failed to reset test database: %v", err)
}
}
-44
View File
@@ -1,44 +0,0 @@
version: '3.8' # Use a recent version
services:
auth_api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: .
ssh: ["default"] # Uses host's SSH agent
container_name: textsender_auth # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
- "9080:9080"
env_file:
- .env
depends_on:
auth_db:
condition: service_healthy # Wait for the DB to be healthy before starting the app
restart: unless-stopped # Optional: Restart policy
# PostgreSQL Database Service
auth_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_auth}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data
ports: []
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always # Optional: Restart policy
# Define the named volume for data persistence
volumes:
postgres_data:
driver: local # Use the default local driver
+42
View File
@@ -0,0 +1,42 @@
version: '3.8' # Use a recent version
services:
auth_api:
build:
context: .
ssh: ["default"]
container_name: schedtxt_auth
ports:
- "9080:9080"
env_file:
- .env
depends_on:
auth_db:
condition: service_healthy
restart: unless-stopped
# PostgreSQL Database Service
auth_db:
image: postgres:18.4-alpine
container_name: schedtxt_auth_db
environment:
POSTGRES_USER: ${DB_AUTH_USER:-schedtxt_op}
POSTGRES_PASSWORD: ${DB_AUTH_PASSWORD:-password}
POSTGRES_DB: ${DB_AUTH_NAME:-schedtxt_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql
ports:
- "5433:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
volumes:
postgres_data:
driver: local
-230
View File
@@ -1,230 +0,0 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/login": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Login and be given an access token (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Login",
"parameters": [
{
"description": "Data to obtain a token",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.LoginAccount"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
}
}
}
},
"/register": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a user that can send texts (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Register user",
"parameters": [
{
"description": "Data to add user",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RegisterUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
}
}
}
}
},
"definitions": {
"handler.LoginAccount": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/token.Login"
}
},
"message": {
"type": "string"
}
}
},
"handler.RegisterResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handler.RegisterResponseItem"
}
},
"message": {
"type": "string"
}
}
},
"handler.RegisterResponseItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.RegisterUser": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"expires_in": {
"type": "integer"
},
"token_type": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "JWT Bearer Token",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:9080",
BasePath: "/api/v1",
Schemes: []string{},
Title: "textsender-auth",
Description: "Auth API to send text messages",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
-206
View File
@@ -1,206 +0,0 @@
{
"swagger": "2.0",
"info": {
"description": "Auth API to send text messages",
"title": "textsender-auth",
"contact": {},
"version": "1.0"
},
"host": "localhost:9080",
"basePath": "/api/v1",
"paths": {
"/login": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Login and be given an access token (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Login",
"parameters": [
{
"description": "Data to obtain a token",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.LoginAccount"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.LoginResponse"
}
}
}
}
},
"/register": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a user that can send texts (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Register user",
"parameters": [
{
"description": "Data to add user",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RegisterUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.RegisterResponse"
}
}
}
}
}
},
"definitions": {
"handler.LoginAccount": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/token.Login"
}
},
"message": {
"type": "string"
}
}
},
"handler.RegisterResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handler.RegisterResponseItem"
}
},
"message": {
"type": "string"
}
}
},
"handler.RegisterResponseItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.RegisterUser": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"expires_in": {
"type": "integer"
},
"token_type": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "JWT Bearer Token",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}
-132
View File
@@ -1,132 +0,0 @@
basePath: /api/v1
definitions:
handler.LoginAccount:
properties:
password:
type: string
username:
type: string
type: object
handler.LoginResponse:
properties:
data:
items:
$ref: '#/definitions/token.Login'
type: array
message:
type: string
type: object
handler.RegisterResponse:
properties:
data:
items:
$ref: '#/definitions/handler.RegisterResponseItem'
type: array
message:
type: string
type: object
handler.RegisterResponseItem:
properties:
id:
type: string
phone_number:
type: string
username:
type: string
type: object
handler.RegisterUser:
properties:
password:
type: string
phone_number:
type: string
username:
type: string
type: object
token.Login:
properties:
access_token:
type: string
expires_in:
type: integer
token_type:
type: string
type: object
host: localhost:9080
info:
contact: {}
description: Auth API to send text messages
title: textsender-auth
version: "1.0"
paths:
/login:
post:
consumes:
- application/json
description: Login and be given an access token (requires JWT)
parameters:
- description: Data to obtain a token
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.LoginAccount'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.LoginResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.LoginResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.LoginResponse'
security:
- BearerAuth: []
summary: Login
tags:
- users
/register:
post:
consumes:
- application/json
description: Create a user that can send texts (requires JWT)
parameters:
- description: Data to add user
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RegisterUser'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RegisterResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.RegisterResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.RegisterResponse'
security:
- BearerAuth: []
summary: Register user
tags:
- users
securityDefinitions:
BearerAuth:
description: JWT Bearer Token
in: header
name: Authorization
type: apiKey
swagger: "2.0"
-40
View File
@@ -1,40 +0,0 @@
module git.kundeng.us/phoenix/textsender-auth
go 1.25.3
require (
git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556
github.com/go-chi/chi/v5 v5.2.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.5
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.42.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-90
View File
@@ -1,90 +0,0 @@
git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556 h1:2NI8cXaxt2rsXrhy/rDhgoOzvjLQwbhwr4nv2g35Ah8=
git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-125
View File
@@ -1,125 +0,0 @@
package config
import (
"flag"
"fmt"
"log"
"os"
"strconv"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/version"
)
type Config struct {
DBConnString string
ServerPort string
ResetDB bool
}
type ConnectionInfo struct {
Username string
Password string
Database string
Host string
Port int
SslMode string
}
const Port = "9080"
const App_Name = "textsender_auth"
func (ci ConnectionInfo) Parse() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", ci.Username, ci.Password, ci.Host, ci.Port, ci.Database, ci.SslMode)
}
func PrintName() {
fmt.Println(App_Name)
fmt.Println(version.String())
}
func Load() *Config {
versionFlag := flag.Bool("version", false, "Print version information")
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", Port, "Server port")
flag.Parse()
if *versionFlag {
fmt.Println(version.String())
os.Exit(-1)
}
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
unpackedConnString := UnpackDBConnString()
dbConnString := unpackedConnString.Parse()
return &Config{
DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
}
}
func GetSecretKey() string {
return os.Getenv("SECRET_KEY")
}
func UnpackDBConnString() (connInfo ConnectionInfo) {
username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
host := os.Getenv("DB_HOST")
port := os.Getenv("DB_PORT")
database := os.Getenv("DB_NAME")
sslMode := os.Getenv("DB_SSLMODE")
if username != "" {
connInfo.Username = username
} else {
connInfo.Username = "user"
}
if password != "" {
connInfo.Password = password
} else {
connInfo.Password = "password"
}
if host != "" {
connInfo.Host = host
} else {
connInfo.Host = "localhost"
}
if port != "" {
num, err := strconv.Atoi(port)
if err != nil {
return
}
connInfo.Port = num
} else {
connInfo.Port = 5432
}
if database != "" {
connInfo.Database = database
} else {
connInfo.Database = "textsender_auth_db"
}
if sslMode != "" {
connInfo.SslMode = sslMode
} else {
connInfo.SslMode = "disable"
}
return
}
func (c *Config) GetDBConnString() string {
return c.DBConnString
}
-136
View File
@@ -1,136 +0,0 @@
// db/connection.go
package db
import (
"context"
"fmt"
"log"
"os"
"path"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
var Pool *pgxpool.Pool
type Database struct {
Pool *pgxpool.Pool
}
func NewDatabase(connString string) (*Database, error) {
ctx := context.Background()
// Parse the connection string and create pool configuration
config, err := pgxpool.ParseConfig(connString)
if err != nil {
return nil, fmt.Errorf("unable to parse DSN: %v", err)
}
// Configure connection pool settings
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.HealthCheckPeriod = time.Minute
// Configure connection timeouts
config.ConnConfig.ConnectTimeout = 10 * time.Second
config.ConnConfig.RuntimeParams["timezone"] = "UTC"
// Create the connection pool
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %v", err)
}
// Test the connection with a short timeout
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("unable to ping database: %v", err)
}
log.Printf("Successfully connected to database")
return &Database{Pool: pool}, nil
}
func TableExists(ctx context.Context, conn *pgxpool.Pool, tableName string) (bool, error) {
var exists bool
query := `
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
);
`
err := conn.QueryRow(ctx, query, tableName).Scan(&exists)
if err != nil {
return false, fmt.Errorf("error checking if table exists: %w", err)
}
return exists, nil
}
func (db *Database) Close() {
if db.Pool != nil {
db.Pool.Close()
log.Println("Database connection pool closed")
}
}
// HealthCheck verifies the database connection is still alive
func (db *Database) HealthCheck(ctx context.Context) error {
return db.Pool.Ping(ctx)
}
// GetPoolStats returns connection pool statistics
func (db *Database) GetPoolStats() string {
stats := db.Pool.Stat()
return fmt.Sprintf("TotalConns: %d, IdleConns: %d, AcquiredConns: %d",
stats.TotalConns(), stats.IdleConns(), stats.AcquiredConns())
}
func (db *Database) ResetDatabase(ctx context.Context) error {
tx, err := db.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("Transaction unable to begin: %v", err)
}
defer tx.Rollback(ctx)
schemaContent, err := os.ReadFile("migrations/schema.sql")
if err != nil {
log.Println("Default migrations not found. Checking different directory")
cwd, _ := os.Getwd()
migrationsPath := path.Join(cwd, "../..", "migrations/schema.sql")
schemaContent, err = os.ReadFile(migrationsPath)
if err != nil {
return fmt.Errorf("error reading schema file: %v", err)
}
}
for _, stmt := range strings.Split(string(schemaContent), ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if strings.HasPrefix(stmt, "--") {
continue
}
_, err := tx.Exec(ctx, stmt)
if err != nil {
return fmt.Errorf("error executing SQL statement: %v\nStatement: %s", err, stmt)
}
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("unable to commit transaction: %v", err)
}
log.Println("Database schema applied successfully")
return nil
}
-5
View File
@@ -1,5 +0,0 @@
package endpoint
// Endpoint for registering a user
const Register = "/api/v1/register"
const Login = "/api/v1/login"
-92
View File
@@ -1,92 +0,0 @@
package handler
import (
"fmt"
"net/http"
"git.kundeng.us/phoenix/textsender-models/pkg/token"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Message string `json:"message"`
Data []token.Login `json:"data"`
}
type LoginHandler struct {
UserStore model.UserStore
}
func NewLoginHandler(userStore model.UserStore) *LoginHandler {
return &LoginHandler{UserStore: userStore}
}
// Login godoc
// @Summary Login
// @Description Login and be given an access token (requires JWT)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body LoginAccount true "Data to obtain a token"
// @Success 200 {object} LoginResponse
// @Failure 400 {object} LoginResponse
// @Failure 500 {object} LoginResponse
// @Router /login [post]
func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginAccount
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp LoginResponse
ctx := r.Context()
if exists, err := l.UserStore.UserExists(ctx, req.Username); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if !exists {
statusCode = http.StatusBadRequest
resp.Message = "Failure in user check"
} else {
if user, err := l.UserStore.GetUserByUsername(ctx, req.Username); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{Password: req.Password}
if hashing.CheckPasswordHash(req.Password, user.Password) {
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*user); err != nil {
fmt.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
}
}
}
}
RespondWithJson(w, statusCode, &resp)
}
-50
View File
@@ -1,50 +0,0 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func TestLogin(t *testing.T) {
mockstore := NewMockUserStore()
handler := NewLoginHandler(mockstore)
testUser := GetTestUser()
unhashedPassword := testUser.Password
hashing := utility.HashMash{Password: testUser.Password}
hashedPassword, err := hashing.HashPassword()
assert.NoError(t, err)
testUser.Password = hashedPassword
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mockstore.CreateUser(ctx, &testUser)
loginUser := LoginAccount{Username: testUser.Username, Password: unhashedPassword}
jsonValue, _ := json.Marshal(loginUser)
req, _ := http.NewRequest("POST", endpoint.Login, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.Login(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response LoginResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response.Data, "An access token should have been returned")
}
-114
View File
@@ -1,114 +0,0 @@
package handler
import (
"context"
"errors"
"sync"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/user"
)
type MockUserStore struct {
Users map[uuid.UUID]*user.User
UsersByUsername map[string]*user.User
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockUserStore() *MockUserStore {
return &MockUserStore{
Users: make(map[uuid.UUID]*user.User),
UsersByUsername: make(map[string]*user.User),
}
}
func (m *MockUserStore) CreateUser(ctx context.Context, user *user.User) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if user.Id == uuid.Nil {
user.Id = uuid.New()
}
if _, exists := m.UsersByUsername[user.Username]; exists {
return errors.New("User with email already exists")
}
m.Users[user.Id] = user
m.UsersByUsername[user.Username] = user
return nil
}
func (m *MockUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if m.Error != nil {
return nil, m.Error
}
user, exists := m.Users[id]
if !exists {
return nil, errors.New("User not found")
}
return user, nil
}
func (m *MockUserStore) GetUserByUsername(ctx context.Context, username string) (*user.User, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
user, exists := m.UsersByUsername[username]
if !exists {
return nil, errors.New("User not found")
}
return user, nil
}
func (m *MockUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
users := make([]*user.User, 0, len(m.Users))
for _, user := range m.Users {
users = append(users, user)
}
return users, nil
}
func (m *MockUserStore) UserExists(ctx context.Context, username string) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.UsersByUsername[username]
if !exists {
return exists, nil
} else {
return exists, nil
}
}
-100
View File
@@ -1,100 +0,0 @@
package handler
import (
"fmt"
"net/http"
"git.kundeng.us/phoenix/textsender-models/pkg/user"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-auth/internal/model"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type RegisterUser struct {
PhoneNumber string `json:"phone_number"`
Username string `json:"username"`
Password string `json:"password"`
}
type RegisterResponseItem struct {
Id uuid.UUID `json:"id"`
PhoneNumber string `json:"phone_number"`
Username string `json:"username"`
}
type RegisterResponse struct {
Message string `json:"message"`
Data []RegisterResponseItem `json:"data"`
}
type UserHandler struct {
UserStore model.UserStore
}
func NewUserHandler(userStore model.UserStore) *UserHandler {
return &UserHandler{UserStore: userStore}
}
// Register godoc
// @Summary Register user
// @Description Create a user that can send texts (requires JWT)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RegisterUser true "Data to add user"
// @Success 200 {object} RegisterResponse
// @Failure 400 {object} RegisterResponse
// @Failure 500 {object} RegisterResponse
// @Router /register [post]
func (u *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req RegisterUser
err := ExtractFromRequest(r, &req)
if err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
var statusCode int
var resp RegisterResponse
user := user.User{Username: req.Username, Password: req.Password, PhoneNumber: req.PhoneNumber}
fmt.Println("Username:", user.Username)
ctx := r.Context()
if exists, err := u.UserStore.UserExists(ctx, user.Username); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if exists {
// User already exists
statusCode = http.StatusBadRequest
resp.Message = "Failure in creating User"
} else {
hashing := utility.HashMash{Password: user.Password}
if hashedPassword, err := hashing.HashPassword(); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
user.Password = hashedPassword
err := u.UserStore.CreateUser(ctx, &user)
if err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, RegisterResponseItem{Id: user.Id, PhoneNumber: user.PhoneNumber, Username: user.Username})
}
}
}
}
RespondWithJson(w, statusCode, &resp)
}
-34
View File
@@ -1,34 +0,0 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
)
func TestCreateUserWithMock(t *testing.T) {
mockstore := NewMockUserStore()
handler := NewUserHandler(mockstore)
testUser := GetTestUser()
jsonValue, _ := json.Marshal(testUser)
req, _ := http.NewRequest("POST", endpoint.Register, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.Register(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response RegisterResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
-21
View File
@@ -1,21 +0,0 @@
package handler
import (
"encoding/json"
"net/http"
)
func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
err := json.NewDecoder(r.Body).Decode(&reqItem)
if err != nil {
return err
} else {
return nil
}
}
func RespondWithJson(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
-9
View File
@@ -1,9 +0,0 @@
package handler
import (
"git.kundeng.us/phoenix/textsender-models/pkg/user"
)
func GetTestUser() user.User {
return user.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"}
}
-22
View File
@@ -1,22 +0,0 @@
package middleware
import (
"log"
"net/http"
"time"
)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func JSONContentType(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
-116
View File
@@ -1,116 +0,0 @@
package model
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/user"
)
type UserStore interface {
CreateUser(ctx context.Context, user *user.User) error
GetUserByID(ctx context.Context, id uuid.UUID) (*user.User, error)
GetUserByUsername(ctx context.Context, username string) (*user.User, error)
GetAllUsers(ctx context.Context) ([]*user.User, error)
UserExists(ctx context.Context, username string) (bool, error)
}
type PGUserStore struct {
db *pgxpool.Pool
}
func NewUserStore(db *pgxpool.Pool) *PGUserStore {
return &PGUserStore{db: db}
}
func (s *PGUserStore) CreateUser(ctx context.Context, user *user.User) error {
query := `
INSERT INTO users (phone_number, username, password)
VALUES ($1, $2, $3)
RETURNING id, phone_number, username
`
return s.db.QueryRow(ctx, query, user.PhoneNumber, user.Username, user.Password).Scan(
&user.Id, &user.PhoneNumber, &user.Username,
)
}
func (s *PGUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE id = $1`
var user user.User
err := s.db.QueryRow(ctx, query, id).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("getting user by ID: %w", err)
}
return &user, nil
}
func (s *PGUserStore) GetUserByUsername(ctx context.Context, username string) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE username = $1`
var user user.User
err := s.db.QueryRow(ctx, query, username).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("getting user by ID: %w", err)
}
return &user, nil
}
func (s *PGUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users`
rows, err := s.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("querying all users: %w", err)
}
defer rows.Close()
var users []*user.User
for rows.Next() {
var user user.User
if err := rows.Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
); err != nil {
return nil, fmt.Errorf("scanning user row: %w", err)
}
users = append(users, &user)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating user rows: %w", err)
}
return users, nil
}
func (s *PGUserStore) UserExists(ctx context.Context, username string) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)`
var exists bool
err := s.db.QueryRow(ctx, query, username).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking if user exists: %w", err)
}
return exists, nil
}
-23
View File
@@ -1,23 +0,0 @@
package utility
import (
"golang.org/x/crypto/bcrypt"
)
type HashMash struct {
Password string
}
func (h HashMash) HashPassword() (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(h.Password), bcrypt.DefaultCost)
return string(bytes), err
}
func (h HashMash) CheckPasswordHash(password string, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func (h *HashMash) SetPassword(password string) {
h.Password = password
}
-47
View File
@@ -1,47 +0,0 @@
package utility
import (
"time"
"github.com/golang-jwt/jwt/v5"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-models/pkg/token"
"git.kundeng.us/phoenix/textsender-models/pkg/user"
)
const ROLE_TYPE = "regular"
const TOKEN_TYPE = "Bearer"
type TokenGenerator struct {
SecretKey []byte
}
func (t *TokenGenerator) SetSecretKey(secretKey string) {
t.SecretKey = []byte(secretKey)
}
func (t *TokenGenerator) GenerateToken(user user.User) (*token.Login, error) {
issuedAt := time.Now()
expirationTime := time.Now().Add(4 * time.Hour)
claims := t.generateClaims(user, TOKEN_TYPE, issuedAt, expirationTime)
myToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
if tokenString, err := myToken.SignedString(t.SecretKey); err != nil {
return nil, err
} else {
return &token.Login{AccessToken: tokenString, TokenType: TOKEN_TYPE, ExpiresIn: expirationTime.Unix()}, nil
}
}
func (t *TokenGenerator) generateClaims(user user.User, role string, issuedAt time.Time, expiredAt time.Time) token.Claims {
return token.Claims{
UserId: user.Id,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: config.App_Name,
ExpiresAt: jwt.NewNumericDate(expiredAt),
IssuedAt: jwt.NewNumericDate(issuedAt),
},
}
}
-17
View File
@@ -1,17 +0,0 @@
package version
import "fmt"
var (
Version = "dev"
BuildTime = "unknown"
Commit = "unknown"
GoVersion = "unknown"
)
func String() string {
return fmt.Sprintf(
"Version: %s\nBuild Date: %s\nCommit: %s\nGo Version: %s",
Version, BuildTime, Commit, GoVersion,
)
}
+30
View File
@@ -0,0 +1,30 @@
-- Add migration script here
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS "user" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
firstname TEXT NOT NULL,
lastname TEXT NOT NULL,
phone_number TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login TIMESTAMPTZ NOT NULL DEFAULT NOW(),
salt_id UUID NOT NULL
);
CREATE TABLE IF NOT EXISTS "salt" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
salt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "service_user" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL,
passphrase TEXT NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login timestamptz NULL,
salt_id UUID NOT NULL
);
-11
View File
@@ -1,11 +0,0 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DROP TABLE IF EXISTS users CASCADE;
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
);
+54
View File
@@ -0,0 +1,54 @@
pub mod response {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct TestResult {
pub message: String,
}
}
pub mod endpoint {
use super::*;
use axum::{Extension, Json, http::StatusCode};
/// Endpoint to hit the root
/// basic handler that responds with a static string
#[utoipa::path(
get,
path = super::super::endpoints::ROOT,
responses(
(status = 200, description = "Test", body = &str),
)
)]
pub async fn root() -> &'static str {
"Hello, World!"
}
/// Endpoint to do a database ping
#[utoipa::path(
get,
path = super::super::endpoints::DBTEST,
responses(
(status = 200, description = "Successful ping of the db", body = super::response::TestResult),
(status = 400, description = "Failure in pinging the db", body = super::response::TestResult)
)
)]
pub async fn db_ping(
Extension(pool): Extension<sqlx::PgPool>,
) -> (StatusCode, Json<response::TestResult>) {
match sqlx::query("SELECT 1").execute(&pool).await {
Ok(_) => {
let tr = response::TestResult {
message: String::from("This works"),
};
(StatusCode::OK, Json(tr))
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(response::TestResult {
message: e.to_string(),
}),
),
}
}
}
+870
View File
@@ -0,0 +1,870 @@
use crate::hashing;
use crate::repo;
use crate::token_stuff;
pub mod request {
use serde::Deserialize;
#[derive(Default, Deserialize, utoipa::ToSchema)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ServiceUserLoginRequest {
pub username: String,
pub passphrase: String,
}
impl ServiceUserLoginRequest {
pub fn is_empty(&self) -> (bool, Option<String>) {
if self.username.is_empty() && self.passphrase.is_empty() {
(
true,
Some(String::from("Username and passphrase are empty")),
)
} else if self.username.is_empty() {
(true, Some(String::from("Username is empty")))
} else if self.username.is_empty() {
(true, Some(String::from("Passphrase is empty")))
} else {
(false, None)
}
}
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct RefreshTokenRequest {
pub access_token: String,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdatePasswordRequest {
pub user_id: uuid::Uuid,
pub current_password: String,
pub updated_password: String,
pub confirmed_password: String,
}
impl UpdatePasswordRequest {
pub fn is_valid(&self) -> bool {
if self.user_id.is_nil()
|| self.current_password.is_empty()
|| self.updated_password.is_empty()
|| self.confirmed_password.is_empty()
{
false
} else {
self.updated_password == self.confirmed_password
}
}
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct UserUpdateNameRequest {
pub user_id: uuid::Uuid,
pub firstname: String,
pub lastname: String,
}
impl UserUpdateNameRequest {
pub fn is_valid(&self) -> (bool, Option<String>) {
if self.user_id.is_nil() || self.firstname.is_empty() || self.lastname.is_empty() {
let reason = String::from("Missing fields");
(false, Some(reason))
} else {
(true, None)
}
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct LoginResponse {
pub message: String,
pub data: Vec<schedtxt_models::token::LoginResult>,
}
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct ServiceUserLoginResponse {
pub message: String,
pub data: Vec<schedtxt_models::token::LoginResult>,
}
pub async fn extract(
response: axum::response::Response,
) -> Result<LoginResponse, std::io::Error> {
let body = match axum::body::to_bytes(response.into_body(), usize::MAX).await {
Ok(body) => body,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
let parsed_body: LoginResponse = match serde_json::from_slice(&body) {
Ok(body) => body,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
Ok(parsed_body)
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct RefreshTokenResponse {
pub message: String,
pub data: Vec<schedtxt_models::token::LoginResult>,
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct UpdatePasswordResponse {
pub message: String,
pub data: Vec<uuid::Uuid>,
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct UserUpdateNameResponse {
pub message: String,
pub data: Vec<schedtxt_models::user::User>,
}
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct GetUserProfileResponse {
pub message: String,
pub data: Vec<schedtxt_models::user::UserProfile>,
}
}
/// Endpoint for a user login
#[utoipa::path(
post,
path = super::endpoints::LOGIN,
request_body(
content = request::LoginRequest,
description = "Data required for a user to lgoin",
content_type = "application/json"
),
responses(
(status = 201, description = "User login successful", body = response::LoginResponse),
(status = 400, description = "Bad data", body = response::LoginResponse),
(status = 500, description = "Something went wrong", body = response::LoginResponse)
)
)]
pub async fn user_login(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<request::LoginRequest>,
) -> (axum::http::StatusCode, axum::Json<response::LoginResponse>) {
if payload.username.is_empty() || payload.password.is_empty() {
let reason = if payload.username.is_empty() {
String::from("Username not provided")
} else {
String::from("Password not provided")
};
(
axum::http::StatusCode::BAD_REQUEST,
axum::Json(response::LoginResponse {
message: reason,
data: Vec::new(),
}),
)
} else {
match repo::user::exists(&pool, &payload.username).await {
Ok(exists) => {
if !exists {
println!("User does not exists");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: String::from("Unable to login"),
data: Vec::new(),
}),
)
} else {
let user = match repo::user::get(&pool, &payload.username).await {
Ok(user) => user,
Err(_err) => {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: String::from("Unable to login"),
data: Vec::new(),
}),
);
}
};
let hashed_password = user.password.clone();
match hashing::verify_password(&payload.password, hashed_password) {
Ok(matches) => {
if matches {
// Create token
let key =
schedtxt_models::envy::environment::get_secret_key().value;
let cst = match token_stuff::create_token(&key, &user.id) {
Ok(token) => token,
Err(_err) => {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: String::from("Error creating token"),
data: Vec::new(),
}),
);
}
};
if token_stuff::verify_token(&key, &cst.access_token) {
let current_time = time::OffsetDateTime::now_utc();
let _ =
repo::user::update_last_login(&pool, &user, &current_time)
.await;
(
axum::http::StatusCode::OK,
axum::Json(response::LoginResponse {
message: String::from("Successful"),
data: vec![schedtxt_models::token::LoginResult {
user_id: user.id,
access_token: cst.access_token,
token_type: String::from(
schedtxt_models::token::TOKEN_TYPE,
),
issued_at: cst.issued,
expires_in: cst.expires_in,
}],
}),
)
} else {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: String::from("Invalid attempt"),
data: Vec::new(),
}),
)
}
} else {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: String::from("Invalid attempt"),
data: Vec::new(),
}),
)
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::LoginResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
/// Endpoint for service user login
#[utoipa::path(
post,
path = super::endpoints::LOGIN_SERVICE_USER,
request_body(
content = request::ServiceUserLoginRequest,
description = "Data required for service user to lgoin",
content_type = "application/json"
),
responses(
(status = 201, description = "Service uuser login successful", body = response::ServiceUserLoginResponse),
(status = 400, description = "Bad data", body = response::ServiceUserLoginResponse),
(status = 500, description = "Something went wrong", body = response::ServiceUserLoginResponse)
)
)]
pub async fn service_user_login(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<request::ServiceUserLoginRequest>,
) -> (
axum::http::StatusCode,
axum::Json<response::ServiceUserLoginResponse>,
) {
if payload.username.is_empty() || payload.passphrase.is_empty() {
let reason = if payload.username.is_empty() {
String::from("Username not provided")
} else {
String::from("Passphrase not provided")
};
(
axum::http::StatusCode::BAD_REQUEST,
axum::Json(response::ServiceUserLoginResponse {
message: reason,
data: Vec::new(),
}),
)
} else {
match repo::service::exists(&pool, &payload.username).await {
Ok(exists) => {
if !exists {
println!("User does not exists");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: String::from("Unable to login"),
data: Vec::new(),
}),
)
} else {
println!("Good to create");
let service_user =
match repo::service::get_with_username(&pool, &payload.username).await {
Ok(service_user) => service_user,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: String::from("Unable to login"),
data: Vec::new(),
}),
);
}
};
println!("Service user: {service_user:?}");
println!("Payload: {:?}", payload.passphrase);
let hashed_password = service_user.passphrase.clone();
println!("Hash password: {hashed_password:?}");
match hashing::verify_password(&payload.passphrase, hashed_password) {
Ok(matches) => {
if matches {
// Create token
println!("Creating token");
let key =
schedtxt_models::envy::environment::get_secret_key().value;
let cst =
token_stuff::create_token(&key, &service_user.id).unwrap();
if token_stuff::verify_token(&key, &cst.access_token) {
let current_time = time::OffsetDateTime::now_utc();
let _ = repo::service::update_last_login(
&pool,
&service_user,
&current_time,
)
.await;
(
axum::http::StatusCode::OK,
axum::Json(response::ServiceUserLoginResponse {
message: String::from(
super::messages::SUCCESSFUL_MESSAGE,
),
data: vec![schedtxt_models::token::LoginResult {
user_id: service_user.id,
access_token: cst.access_token,
token_type: String::from(
schedtxt_models::token::TOKEN_TYPE,
),
issued_at: cst.issued,
expires_in: cst.expires_in,
}],
}),
)
} else {
eprintln!("Invalid token");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: String::from("Invalid attempt"),
data: Vec::new(),
}),
)
}
} else {
eprintln!("No match");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: String::from("Invalid attempt"),
data: Vec::new(),
}),
)
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::ServiceUserLoginResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
/// Endpoint for service user login
#[utoipa::path(
post,
path = super::endpoints::REFRESH_TOKEN,
request_body(
content = request::RefreshTokenRequest,
description = "Data required refresh token",
content_type = "application/json"
),
responses(
(status = 200, description = "Service uuser login successful", body = response::RefreshTokenResponse),
(status = 400, description = "Bad data", body = response::RefreshTokenResponse),
(status = 500, description = "Something went wrong", body = response::RefreshTokenResponse)
)
)]
pub async fn refresh_token(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<request::RefreshTokenRequest>,
) -> (
axum::http::StatusCode,
axum::Json<response::RefreshTokenResponse>,
) {
if payload.access_token.is_empty() {
let reason = String::from("Access token not provided");
(
axum::http::StatusCode::BAD_REQUEST,
axum::Json(response::RefreshTokenResponse {
message: reason,
data: Vec::new(),
}),
)
} else {
let key = schedtxt_models::envy::environment::get_secret_key().value;
if token_stuff::verify_token(&key, &payload.access_token) {
match token_stuff::extract_id_from_token(&key, &payload.access_token) {
Ok(id) => {
let generate_service_token =
|id, key| -> Option<schedtxt_models::token::CreateTokenResult> {
token_stuff::create_service_refresh_token(key, id).ok()
};
let mut response = response::RefreshTokenResponse {
message: String::new(),
data: Vec::new(),
};
match repo::user::get_with_id(&pool, &id).await {
Ok(_user) => match generate_service_token(&id, &key) {
Some(cst) => {
response.message =
String::from(super::messages::SUCCESSFUL_MESSAGE);
let lr = schedtxt_models::token::LoginResult {
user_id: id,
access_token: cst.access_token,
issued_at: cst.issued,
expires_in: cst.expires_in,
token_type: String::from(schedtxt_models::token::TOKEN_TYPE),
};
response.data.push(lr);
(axum::http::StatusCode::OK, axum::Json(response))
}
None => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::RefreshTokenResponse {
message: String::from("Refresh token not generated"),
data: Vec::new(),
}),
),
},
Err(err) => {
println!("Unable to find user, checking service user: {err:?}");
match repo::service::get(&pool, &id).await {
Ok(_service_user) => match generate_service_token(&id, &key) {
Some(cst) => {
response.message =
String::from(super::messages::SUCCESSFUL_MESSAGE);
let lr = schedtxt_models::token::LoginResult {
user_id: id,
access_token: cst.access_token,
issued_at: cst.issued,
expires_in: cst.expires_in,
token_type: String::from(
schedtxt_models::token::TOKEN_TYPE,
),
};
response.data.push(lr);
(axum::http::StatusCode::OK, axum::Json(response))
}
None => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::RefreshTokenResponse {
message: String::from("Issued at not returned"),
data: Vec::new(),
}),
),
},
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::RefreshTokenResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::RefreshTokenResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
} else {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::RefreshTokenResponse {
message: String::from("Unable to verify token"),
data: Vec::new(),
}),
)
}
}
}
/// Endpoint for a updating password
#[utoipa::path(
patch,
path = super::endpoints::UPDATE_PASSWORD,
request_body(
content = request::UpdatePasswordRequest,
description = "Data required to update password",
content_type = "application/json"
),
responses(
(status = 200, description = "User login successful", body = response::UpdatePasswordResponse),
(status = 400, description = "Bad data", body = response::UpdatePasswordResponse),
(status = 500, description = "Something went wrong", body = response::UpdatePasswordResponse)
)
)]
pub async fn update_password(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<request::UpdatePasswordRequest>,
) -> (
axum::http::StatusCode,
axum::Json<response::UpdatePasswordResponse>,
) {
if !payload.is_valid() {
(
axum::http::StatusCode::BAD_REQUEST,
axum::Json(response::UpdatePasswordResponse {
message: String::from("Invalid passwords"),
data: Vec::new(),
}),
)
} else {
let verify_password = |current_password, hashed_password| -> Result<bool, std::io::Error> {
match hashing::verify_password(current_password, hashed_password) {
Ok(matches) => Ok(matches),
Err(err) => Err(std::io::Error::other(err.to_string())),
}
};
match repo::user::get_with_id(&pool, &payload.user_id).await {
Ok(user) => {
let hashed_password = user.password.clone();
match verify_password(&payload.current_password, hashed_password) {
Ok(matches) => {
if matches {
let (generate_salt, mut salt) = super::register::generate_the_salt();
salt.id = repo::salt::insert(&pool, &salt).await.unwrap();
let updated_hashed_password = match hashing::hash_password(
&payload.updated_password,
&generate_salt,
) {
Ok(hashed) => hashed,
Err(err) => {
eprintln!("Error: {err:?}");
String::new()
}
};
match repo::user::update_password(
&pool,
&user,
&updated_hashed_password,
)
.await
{
Ok(()) => (
axum::http::StatusCode::OK,
axum::Json(response::UpdatePasswordResponse {
message: String::from(super::messages::SUCCESSFUL_MESSAGE),
data: vec![user.id],
}),
),
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
} else {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: String::from("Issue updating password"),
data: Vec::new(),
}),
)
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
Err(err) => {
println!("No User found, trying Service User: {err:?}");
// Try service user
match repo::service::get(&pool, &payload.user_id).await {
Ok(service_user) => {
let hashed_password = service_user.passphrase.clone();
match verify_password(&payload.current_password, hashed_password) {
Ok(matches) => {
if matches {
let (generate_salt, mut salt) =
super::register::generate_the_salt();
salt.id = repo::salt::insert(&pool, &salt).await.unwrap();
let updated_hashed_password = match hashing::hash_password(
&payload.updated_password,
&generate_salt,
) {
Ok(hashed) => hashed,
Err(err) => {
eprintln!("Error: {err:?}");
String::new()
}
};
match repo::service::update_passphrase(
&pool,
&service_user,
&updated_hashed_password,
)
.await
{
Ok(()) => (
axum::http::StatusCode::OK,
axum::Json(response::UpdatePasswordResponse {
message: String::from(
super::messages::SUCCESSFUL_MESSAGE,
),
data: vec![service_user.id],
}),
),
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
} else {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: String::from("Issue updating password"),
data: Vec::new(),
}),
)
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UpdatePasswordResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
}
}
/// Endpoint for a updating password
#[utoipa::path(
patch,
path = super::endpoints::UPDATE_USER_NAME,
request_body(
content = request::UserUpdateNameRequest,
description = "Data required to update name of a user",
content_type = "application/json"
),
responses(
(status = 200, description = "Names were updated", body = response::UserUpdateNameResponse),
(status = 304, description = "Nothing to change", body = response::UserUpdateNameResponse),
(status = 400, description = "Bad data", body = response::UserUpdateNameResponse),
(status = 500, description = "Something went wrong", body = response::UserUpdateNameResponse)
)
)]
pub async fn update_name_of_user(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<request::UserUpdateNameRequest>,
) -> (
axum::http::StatusCode,
axum::Json<response::UserUpdateNameResponse>,
) {
let (valid_request, reason) = payload.is_valid();
if !valid_request {
(
axum::http::StatusCode::BAD_REQUEST,
axum::Json(response::UserUpdateNameResponse {
message: reason.unwrap_or_default(),
data: Vec::new(),
}),
)
} else {
match repo::user::get_with_id(&pool, &payload.user_id).await {
Ok(user) => {
if user.firstname == payload.firstname || user.lastname == payload.lastname {
(
axum::http::StatusCode::NOT_MODIFIED,
axum::Json(response::UserUpdateNameResponse {
message: String::from("No change"),
data: Vec::new(),
}),
)
} else {
match repo::user::update_name(
&pool,
&user.id,
&payload.firstname,
&payload.lastname,
)
.await
{
Ok(_) => (
axum::http::StatusCode::OK,
axum::Json(response::UserUpdateNameResponse {
message: String::from(super::messages::SUCCESSFUL_MESSAGE),
data: vec![schedtxt_models::user::User {
id: user.id,
phone_number: user.phone_number,
firstname: payload.firstname,
lastname: payload.lastname,
username: user.username,
created: user.created,
last_login: user.last_login,
..Default::default()
}],
}),
),
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UserUpdateNameResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response::UserUpdateNameResponse {
message: err.to_string(),
data: Vec::new(),
}),
),
}
}
}
/// Endpoint to get User Profile
#[utoipa::path(
delete,
path = super::endpoints::GET_USER_PROFILE,
params(("id" = uuid::Uuid, Path, description = "User Id")),
responses(
(status = 200, description = "Deleted", body = response::GetUserProfileResponse),
(status = 400, description = "Bad request", body = response::GetUserProfileResponse),
(status = 500, description = "Error", body = response::GetUserProfileResponse)
)
)]
pub async fn get_user_profile(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (
axum::http::StatusCode,
axum::Json<response::GetUserProfileResponse>,
) {
let mut response = response::GetUserProfileResponse::default();
if id.is_nil() {
response.message = String::from("Invalid");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
match repo::user::get_with_id(&pool, &id).await {
Ok(user) => {
let user_profile = schedtxt_models::user::UserProfile {
user_id: user.id,
phone_number: user.phone_number,
firstname: user.firstname,
lastname: user.lastname,
last_login: user.last_login,
created: user.created,
username: user.username,
};
response.message = String::from(super::messages::SUCCESSFUL_MESSAGE);
response.data.push(user_profile);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Bad request");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
+1
View File
@@ -0,0 +1 @@
pub const SUCCESSFUL_MESSAGE: &str = "Successful";
+24
View File
@@ -0,0 +1,24 @@
pub mod common;
pub mod login;
pub mod messages;
pub mod register;
pub mod endpoints {
pub const ROOT: &str = "/";
pub const REGISTER: &str = "/api/v1/register";
/// Endpoint for a user to login
pub const LOGIN: &str = "/api/v1/login";
pub const DBTEST: &str = "/api/v1/test/db";
/// Endpoint constant for service user registration
pub const REGISTER_SERVICE_USER: &str = "/api/v1/service/register";
/// Endpoint constant for service login user
pub const LOGIN_SERVICE_USER: &str = "/api/v1/service/login";
/// Endpoint constant for refresh token
pub const REFRESH_TOKEN: &str = "/api/v1/token/refresh";
/// Endpoint constant for updating password
pub const UPDATE_PASSWORD: &str = "/api/v1/user/password/update";
/// Endpoint constant for updating user's name
pub const UPDATE_USER_NAME: &str = "/api/v1/user/name/update";
/// Endpoint constant for getting user profile
pub const GET_USER_PROFILE: &str = "/api/v1/user/profile/{id}";
}
+292
View File
@@ -0,0 +1,292 @@
use axum::{Json, http::StatusCode};
use crate::hashing;
use crate::repo;
pub mod request {
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct Request {
pub username: String,
pub password: String,
pub phone_number: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub firstname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lastname: Option<String>,
}
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct RegisterServiceUserRequest {
pub username: String,
pub passphrase: String,
}
impl RegisterServiceUserRequest {
pub fn is_empty(&self) -> (bool, Option<String>) {
if self.username.is_empty() && self.passphrase.is_empty() {
(
true,
Some(String::from("Username and Passphrase are empty")),
)
} else if self.username.is_empty() {
(true, Some(String::from("Username is empty")))
} else if self.passphrase.is_empty() {
(true, Some(String::from("Passphrase is empty")))
} else {
(false, None)
}
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct Response {
pub message: String,
pub data: Vec<schedtxt_models::user::User>,
}
#[derive(Default, Deserialize, Serialize, utoipa::ToSchema)]
pub struct RegisterServiceUserResponse {
pub message: String,
pub data: Vec<schedtxt_models::user::ServiceUser>,
}
}
pub fn generate_the_salt() -> (
argon2::password_hash::SaltString,
schedtxt_models::user::Salt,
) {
let salt_string = hashing::generate_salt().unwrap();
let salt = schedtxt_models::user::Salt::default();
(salt_string, salt)
}
/// Endpoint to register a user
#[utoipa::path(
post,
path = super::endpoints::REGISTER,
request_body(
content = request::Request,
description = "Data required to register",
content_type = "application/json"
),
responses(
(status = 201, description = "User created", body = response::Response),
(status = 404, description = "User already exists", body = response::Response),
(status = 500, description = "Issue creating user", body = response::Response)
)
)]
pub async fn register_user(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<request::Request>,
) -> (StatusCode, Json<response::Response>) {
let registration_enabled = match is_registration_enabled().await {
Ok(value) => value,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(response::Response {
message: String::from("Registration check failed"),
data: Vec::new(),
}),
);
}
};
if registration_enabled {
let mut user = schedtxt_models::user::User {
username: payload.username.clone(),
password: payload.password.clone(),
phone_number: payload.phone_number.clone(),
..Default::default()
};
user.firstname = payload.firstname.unwrap_or_default();
user.lastname = payload.lastname.unwrap_or_default();
println!("Checking if user exists");
match repo::user::exists(&pool, &user.username).await {
Ok(res) => {
if res {
println!("Already exists");
(
StatusCode::BAD_REQUEST,
Json(response::Response {
message: String::from("Error"),
data: Vec::new(),
}),
)
} else {
println!("Good to create");
println!("Generate salt string");
let (generated_salt, mut salt) = generate_the_salt();
println!("Creating salt");
salt.id = repo::salt::insert(&pool, &salt).await.unwrap();
user.salt_id = salt.id;
let hashed_password =
hashing::hash_password(&user.password, &generated_salt).unwrap();
user.password = hashed_password;
println!("Creating user");
match repo::user::insert(&pool, &user).await {
Ok((id, date_created)) => {
user.id = id;
user.created = date_created;
(
StatusCode::CREATED,
Json(response::Response {
message: String::from("User created"),
data: vec![user],
}),
)
}
Err(err) => (
StatusCode::BAD_REQUEST,
Json(response::Response {
message: err.to_string(),
data: vec![user],
}),
),
}
}
}
Err(err) => (
StatusCode::BAD_REQUEST,
Json(response::Response {
message: err.to_string(),
data: vec![user],
}),
),
}
} else {
(
axum::http::StatusCode::NOT_ACCEPTABLE,
Json(response::Response {
message: String::from("Registration is not enabled"),
data: Vec::new(),
}),
)
}
}
/// Checks to see if registration is enabled
async fn is_registration_enabled() -> Result<bool, std::io::Error> {
let key = String::from("ENABLE_REGISTRATION");
let var = schedtxt_models::envy::environment::get_env(&key);
let parsed_value = var.value.to_uppercase();
if parsed_value == "TRUE" {
Ok(true)
} else if parsed_value == "FALSE" {
Ok(false)
} else {
Err(std::io::Error::other(
"Could not determine value of ENABLE_REGISTRATION",
))
}
}
/// Endpoint to register a service user
#[utoipa::path(
post,
path = super::endpoints::REGISTER_SERVICE_USER,
request_body(
content = request::RegisterServiceUserRequest,
description = "Data required to register service user",
content_type = "application/json"
),
responses(
(status = 201, description = "Service user created", body = response::RegisterServiceUserResponse),
(status = 400, description = "Issue creating service user", body = response::RegisterServiceUserResponse),
(status = 406, description = "Cannot create service user", body = response::RegisterServiceUserResponse),
(status = 500, description = "Issue creating service user", body = response::RegisterServiceUserResponse),
)
)]
pub async fn register_service_user(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
Json(payload): Json<request::RegisterServiceUserRequest>,
) -> (
axum::http::StatusCode,
axum::Json<response::RegisterServiceUserResponse>,
) {
let mut resp = response::RegisterServiceUserResponse {
..Default::default()
};
let registration_enabled = match is_registration_enabled().await {
Ok(value) => value,
Err(err) => {
eprintln!("Error: {err:?}");
resp.message = String::from("Registration check failed");
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(resp));
}
};
let (res, msg) = payload.is_empty();
if res {
resp.message = msg.unwrap();
(axum::http::StatusCode::BAD_REQUEST, axum::Json(resp))
} else {
if registration_enabled {
match repo::service::exists(&pool, &payload.username).await {
Ok(exists) => {
if exists {
resp.message = String::from("Invalid");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(resp),
)
} else {
let (generate_salt, mut salt) = generate_the_salt();
salt.id = repo::salt::insert(&pool, &salt).await.unwrap();
let mut service_user = schedtxt_models::user::ServiceUser {
username: payload.username.clone(),
passphrase: hashing::hash_password(&payload.passphrase, &generate_salt)
.unwrap(),
salt_id: salt.id,
..Default::default()
};
println!("Creating user");
match repo::service::insert(&pool, &service_user).await {
Ok((service_user_id, created)) => {
resp.message = String::from(super::messages::SUCCESSFUL_MESSAGE);
service_user.created = Some(created);
service_user.id = service_user_id;
resp.data.push(service_user);
(axum::http::StatusCode::CREATED, axum::Json(resp))
}
Err(err) => {
resp.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(resp),
)
}
}
}
}
Err(err) => {
resp.message = err.to_string();
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(resp),
)
}
}
} else {
resp.message = String::from("Registration is not enabled");
(axum::http::StatusCode::NOT_ACCEPTABLE, Json(resp))
}
}
}
+6
View File
@@ -0,0 +1,6 @@
const ADDRESS: &str = "0.0.0.0";
const PORT: &str = "9080";
pub fn get_full() -> String {
format!("{ADDRESS}:{PORT}")
}
+20
View File
@@ -0,0 +1,20 @@
use sqlx::postgres::PgPoolOptions;
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = schedtxt_models::envy::environment::get_db_url().value;
println!("Database url: {database_url}");
PgPoolOptions::new()
.max_connections(super::connection_settings::MAXCONN)
.connect(&database_url)
.await
}
pub async fn migrations(pool: &sqlx::PgPool) {
// Run migrations using the sqlx::migrate! macro
// Assumes your migrations are in a ./migrations folder relative to Cargo.toml
sqlx::migrate!("./migrations")
.run(pool)
.await
.expect("Failed to run migrations");
}
+5
View File
@@ -0,0 +1,5 @@
pub mod init;
mod connection_settings {
pub const MAXCONN: u32 = 5;
}
+101
View File
@@ -0,0 +1,101 @@
use argon2::{
Argon2, // The Argon2 algorithm struct
PasswordVerifier,
password_hash::{
PasswordHasher,
SaltString,
rand_core::OsRng, // Secure random number generator
},
};
pub fn generate_salt() -> Result<SaltString, argon2::Error> {
// Generate a random salt
// SaltString::generate uses OsRng internally for cryptographic security
Ok(SaltString::generate(&mut OsRng))
}
pub fn get_salt(s: &str) -> Result<SaltString, argon2::password_hash::Error> {
SaltString::from_b64(s)
}
pub fn hash_password(
password: &String,
salt: &SaltString,
) -> Result<String, argon2::password_hash::Error> {
let password_bytes = password.as_bytes();
// Create an Argon2 instance with default parameters (recommended)
// You could customize parameters here if needed, but defaults are strong
let argon2 = Argon2::default();
// Hash the password with the salt
// The output is a PasswordHash string format that includes algorithm, version,
// parameters, salt, and the hash itself.
Ok(argon2.hash_password(password_bytes, salt)?.to_string())
}
pub fn verify_password(
password_attempt: &String,
stored_hash: String,
) -> Result<bool, argon2::password_hash::Error> {
let password_bytes = password_attempt.as_bytes();
// Parse the stored hash string
// This extracts the salt, parameters, and hash digest
let parsed_hash = argon2::PasswordHash::new(stored_hash.as_str())?;
// Create an Argon2 instance (it will use the parameters from the parsed hash)
// Verify the password against the parsed hash
// This automatically uses the correct salt and parameters embedded in `parsed_hash`
match Argon2::default().verify_password(password_bytes, &parsed_hash) {
Ok(()) => Ok(true), // Passwords match
Err(argon2::password_hash::Error::Password) => Ok(false), // Passwords don't match
Err(e) => Err(e), // Some other error occurred (e.g., invalid hash format)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_password() {
let some_password = String::from("somethingrandom");
match hash_password(&some_password, &generate_salt().unwrap()) {
Ok(p) => match verify_password(&some_password, p.clone()) {
Ok(res) => {
assert_eq!(res, true);
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
},
Err(eerr) => {
assert!(false, "Error: {:?}", eerr.to_string());
}
}
}
#[test]
fn test_wrong_password() {
let some_password = String::from("somethingrandom");
match hash_password(&some_password, &generate_salt().unwrap()) {
Ok(p) => {
match verify_password(&some_password, p.clone()) {
Ok(res) => {
assert_eq!(res, true, "Passwords are not verified");
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
}
let wrong_password = String::from("Differentanotherlevel");
let result = verify_password(&wrong_password, p.clone()).unwrap();
assert_eq!(false, result, "Passwords should not match");
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
}
}
}
+151
View File
@@ -0,0 +1,151 @@
pub mod callers;
pub mod config;
pub mod db;
pub mod hashing;
pub mod repo;
pub mod token_stuff;
pub mod init {
use axum::{
Router,
routing::{get, patch, post},
};
use utoipa::OpenApi;
use super::callers;
use callers::common as common_callers;
use callers::login as login_caller;
use callers::register as register_caller;
use login_caller::response as login_responses;
use register_caller::response as register_responses;
#[derive(utoipa::OpenApi)]
#[openapi(
paths(
common_callers::endpoint::db_ping, common_callers::endpoint::root,
register_caller::register_user, login_caller::user_login,
login_caller::get_user_profile,
),
components(schemas(common_callers::response::TestResult,
register_responses::Response, login_responses::LoginResponse,
login_responses::GetUserProfileResponse,
)),
tags(
(name = "TextSender Auth API", description = "Auth API for TextSender API")
)
)]
struct ApiDoc;
mod cors {
pub async fn configure_cors() -> tower_http::cors::CorsLayer {
let cors = tower_http::cors::CorsLayer::new()
.allow_methods([
axum::http::Method::GET,
axum::http::Method::PATCH,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::DELETE,
])
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
])
.allow_credentials(true) // If you need to send cookies or authentication headers:cite[2]
.max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2]
// Dynamically set the allowed origin based on the environment
match std::env::var(schedtxt_models::envy::keys::APP_ENV).as_deref() {
Ok("production") => {
let allowed_origins_env =
schedtxt_models::envy::environment::get_allowed_origins();
match schedtxt_models::envy::utility::delimitize(&allowed_origins_env) {
Ok(alwd) => {
let allowed_origins: Vec<axum::http::HeaderValue> = alwd
.into_iter()
.map(|s| s.parse::<axum::http::HeaderValue>().unwrap())
.collect();
cors.allow_origin(allowed_origins)
}
Err(err) => {
eprintln!(
"Could not parse out allowed origins from env: Error: {err:?}"
);
std::process::exit(-1);
}
}
}
_ => {
// Development (default): Allow localhost origins
cors.allow_origin(vec![
"http://localhost:4200".parse().unwrap(),
"http://127.0.0.1:4200".parse().unwrap(),
"http://localhost:5173".parse().unwrap(),
"http://127.0.0.1:5173".parse().unwrap(),
"http://localhost:9080".parse().unwrap(),
"http://127.0.0.1:9080".parse().unwrap(),
"http://localhost:9081".parse().unwrap(),
"http://127.0.0.1:9081".parse().unwrap(),
])
}
}
}
}
pub async fn routes() -> Router {
Router::new()
.route(
callers::endpoints::DBTEST,
get(callers::common::endpoint::db_ping),
)
.route(
callers::endpoints::ROOT,
get(callers::common::endpoint::root),
)
.route(
callers::endpoints::REGISTER,
post(callers::register::register_user),
)
.route(
callers::endpoints::REGISTER_SERVICE_USER,
post(callers::register::register_service_user),
)
.route(callers::endpoints::LOGIN, post(callers::login::user_login))
.route(
callers::endpoints::LOGIN_SERVICE_USER,
post(callers::login::service_user_login),
)
.route(
callers::endpoints::REFRESH_TOKEN,
post(callers::login::refresh_token),
)
.route(
callers::endpoints::UPDATE_PASSWORD,
patch(callers::login::update_password),
)
.route(
callers::endpoints::UPDATE_USER_NAME,
patch(callers::login::update_name_of_user),
)
.route(
callers::endpoints::GET_USER_PROFILE,
get(callers::login::get_user_profile),
)
.layer(cors::configure_cors().await)
}
pub async fn app() -> Router {
let pool = super::db::init::create_pool()
.await
.expect("Failed to create pool");
super::db::init::migrations(&pool).await;
routes()
.await
.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
.layer(axum::Extension(pool))
}
}
+12
View File
@@ -0,0 +1,12 @@
#[tokio::main]
async fn main() {
// initialize tracing
tracing_subscriber::fmt::init();
let app = schedtxt_auth::init::app().await;
// run our app with hyper, listening globally on port 9080
let url = schedtxt_auth::config::get_full();
let listener = tokio::net::TcpListener::bind(url).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
+291
View File
@@ -0,0 +1,291 @@
pub mod service;
pub mod user {
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
pub date_created: Option<time::OffsetDateTime>,
}
pub async fn get(
pool: &sqlx::PgPool,
username: &String,
) -> Result<schedtxt_models::user::User, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, username, password, phone_number, salt_id, firstname, lastname, created, last_login FROM "user" WHERE username = $1
"#,
)
.bind(username)
.fetch_optional(pool)
.await;
match result {
Ok(r) => match r {
Some(r) => Ok(schedtxt_models::user::User {
id: r.try_get("id")?,
username: r.try_get("username")?,
password: r.try_get("password")?,
phone_number: r.try_get("phone_number")?,
salt_id: r.try_get("salt_id")?,
firstname: r.try_get("firstname")?,
lastname: r.try_get("lastname")?,
created: r.try_get("created")?,
last_login: r.try_get("last_login")?,
}),
None => Err(sqlx::Error::RowNotFound),
},
Err(e) => Err(e),
}
}
pub async fn get_with_id(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::user::User, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, username, password, phone_number, salt_id, firstname, lastname, created, last_login FROM "user" WHERE id = $1
"#,
)
.bind(id)
.fetch_optional(pool)
.await {
Ok(r) => match r {
Some(r) => Ok(schedtxt_models::user::User {
id: r.try_get("id")?,
username: r.try_get("username")?,
password: r.try_get("password")?,
phone_number: r.try_get("phone_number")?,
salt_id: r.try_get("salt_id")?,
firstname: r.try_get("firstname")?,
lastname: r.try_get("lastname")?,
created: r.try_get("created")?,
last_login: r.try_get("last_login")?,
}),
None => Err(sqlx::Error::RowNotFound),
},
Err(e) => Err(e),
}
}
pub async fn update_last_login(
pool: &sqlx::PgPool,
user: &schedtxt_models::user::User,
time: &time::OffsetDateTime,
) -> Result<time::OffsetDateTime, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "user" SET last_login = $1 WHERE id = $2 RETURNING last_login
"#,
)
.bind(time)
.bind(user.id)
.fetch_optional(pool)
.await
.map_err(|e| {
eprintln!("Error updating time: {e}");
e
});
match result {
Ok(row) => match row {
Some(r) => {
let last_login: time::OffsetDateTime = r
.try_get("last_login")
.map_err(|_e| sqlx::Error::RowNotFound)?;
Ok(last_login)
}
None => Err(sqlx::Error::RowNotFound),
},
Err(err) => Err(err),
}
}
pub async fn update_password(
pool: &sqlx::PgPool,
user: &schedtxt_models::user::User,
updated_hashed_password: &String,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
UPDATE "user" SET password = $1 WHERE id = $2
"#,
)
.bind(updated_hashed_password)
.bind(user.id)
.execute(pool)
.await
{
Ok(row) => {
if row.rows_affected() > 0 {
Ok(())
} else {
Err(sqlx::Error::RowNotFound)
}
}
Err(err) => Err(err),
}
}
pub async fn update_name(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
firstname: &str,
lastname: &str,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
UPDATE "user" SET firstname = $1, lastname = $2 WHERE id = $3
"#,
)
.bind(firstname)
.bind(lastname)
.bind(id)
.execute(pool)
.await
{
Ok(row) => {
if row.rows_affected() > 0 {
Ok(())
} else {
Err(sqlx::Error::RowNotFound)
}
}
Err(err) => Err(err),
}
}
pub async fn exists(pool: &sqlx::PgPool, username: &String) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT 1 FROM "user" WHERE username = $1
"#,
)
.bind(username)
.fetch_optional(pool)
.await;
match result {
Ok(r) => match r {
Some(row) => {
if row.is_empty() {
Ok(false)
} else {
Ok(true)
}
}
None => Ok(false),
},
Err(e) => {
eprintln!("What??");
eprintln!("Error: {e:?}");
Err(e)
}
}
}
pub async fn insert(
pool: &sqlx::PgPool,
user: &schedtxt_models::user::User,
) -> Result<(uuid::Uuid, std::option::Option<time::OffsetDateTime>), sqlx::Error> {
let row = sqlx::query(
r#"
INSERT INTO "user" (username, password, phone_number, firstname, lastname, salt_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created;
"#,
)
.bind(&user.username)
.bind(&user.password)
.bind(&user.phone_number)
.bind(&user.firstname)
.bind(&user.lastname)
.bind(user.salt_id)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting item: {e}");
e
})?;
let result = InsertedData {
id: row.try_get("id").map_err(|_e| sqlx::Error::RowNotFound)?,
date_created: row
.try_get("created")
.map_err(|_e| sqlx::Error::RowNotFound)?,
};
if result.id.is_nil() && result.date_created.is_none() {
Err(sqlx::Error::RowNotFound)
} else {
Ok((result.id, result.date_created))
}
}
}
pub mod salt {
use sqlx::Row;
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct InsertedData {
pub id: uuid::Uuid,
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::user::Salt, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, salt FROM "salt" WHERE id = $1
"#,
)
.bind(id)
.fetch_optional(pool)
.await;
match result {
Ok(r) => match r {
Some(r) => Ok(schedtxt_models::user::Salt {
id: r.try_get("id")?,
salt: r.try_get("salt")?,
}),
None => Err(sqlx::Error::RowNotFound),
},
Err(e) => Err(e),
}
}
pub async fn insert(
pool: &sqlx::PgPool,
salt: &schedtxt_models::user::Salt,
) -> Result<uuid::Uuid, sqlx::Error> {
let row = sqlx::query(
r#"
INSERT INTO "salt" (salt)
VALUES ($1)
RETURNING id;
"#,
)
.bind(&salt.salt)
.fetch_one(pool)
.await
.map_err(|e| {
eprintln!("Error inserting item: {e}");
e
})?;
let result = InsertedData {
id: row.try_get("id").map_err(|_e| sqlx::Error::RowNotFound)?,
};
if !result.id.is_nil() {
Ok(result.id)
} else {
Err(sqlx::Error::RowNotFound)
}
}
}
+235
View File
@@ -0,0 +1,235 @@
use sqlx::Row;
pub async fn valid_passphrase(
pool: &sqlx::PgPool,
passphrase: &String,
) -> Result<(uuid::Uuid, String, time::OffsetDateTime), sqlx::Error> {
let result = sqlx::query(
r#"
SELECT id, username, date_created FROM "passphrase" WHERE passphrase = $1
"#,
)
.bind(passphrase)
.fetch_one(pool)
.await;
match result {
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let username: String = row.try_get("username")?;
let date_created: Option<time::OffsetDateTime> = row.try_get("date_created")?;
Ok((id, username, date_created.unwrap()))
}
Err(err) => Err(err),
}
}
pub async fn get_passphrase(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<(String, String, time::OffsetDateTime), sqlx::Error> {
let result = sqlx::query(
r#"
SELECT username, passphrase, date_created FROM "passphrase" WHERE id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await;
match result {
Ok(row) => {
let username: String = row.try_get("username")?;
let passphrase: String = row.try_get("passphrase")?;
let date_created: time::OffsetDateTime = row.try_get("date_created")?;
Ok((username, passphrase, date_created))
}
Err(err) => Err(err),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::user::ServiceUser, sqlx::Error> {
match sqlx::query(
r#"SELECT id, username, passphrase, created, last_login, salt_id FROM "service_user" WHERE id = $1"#
).bind(id)
.fetch_one(pool).await {
Ok(row) => {
let last_login: Option<time::OffsetDateTime> = match row.try_get("last_login") {
Ok(login) => {
Some(login)
}
Err(err) => {
eprintln!("Error: {err:?}");
None
}
};
let service_user = schedtxt_models::user::ServiceUser {
id: row.try_get("id")?,
username: row.try_get("username")?,
passphrase: row.try_get("passphrase")?,
created: row.try_get("created")?,
last_login,
salt_id: row.try_get("salt_id")?,
};
Ok(service_user)
}
Err(err) => {
Err(err)
}
}
}
pub async fn get_with_username(
pool: &sqlx::PgPool,
username: &String,
) -> Result<schedtxt_models::user::ServiceUser, sqlx::Error> {
match sqlx::query(
r#"SELECT id, username, passphrase, created, last_login, salt_id FROM "service_user" WHERE username = $1"#
).bind(username)
.fetch_one(pool).await {
Ok(row) => {
let last_login: Option<time::OffsetDateTime> = match row.try_get("last_login") {
Ok(login) => {
Some(login)
}
Err(err) => {
eprintln!("Error: {err:?}");
None
}
};
let service_user = schedtxt_models::user::ServiceUser {
id: row.try_get("id")?,
username: row.try_get("username")?,
passphrase: row.try_get("passphrase")?,
created: row.try_get("created")?,
last_login,
salt_id: row.try_get("salt_id")?,
};
Ok(service_user)
}
Err(err) => {
Err(err)
}
}
}
pub async fn update_last_login(
pool: &sqlx::PgPool,
service_user: &schedtxt_models::user::ServiceUser,
time: &time::OffsetDateTime,
) -> Result<time::OffsetDateTime, sqlx::Error> {
let result = sqlx::query(
r#"
UPDATE "service_user" SET last_login = $1 WHERE id = $2 RETURNING last_login
"#,
)
.bind(time)
.bind(service_user.id)
.fetch_optional(pool)
.await
.map_err(|e| {
eprintln!("Error updating time: {e}");
e
});
match result {
Ok(row) => match row {
Some(r) => {
let last_login: time::OffsetDateTime = r
.try_get("last_login")
.map_err(|_e| sqlx::Error::RowNotFound)?;
Ok(last_login)
}
None => Err(sqlx::Error::RowNotFound),
},
Err(err) => Err(err),
}
}
pub async fn update_passphrase(
pool: &sqlx::PgPool,
user: &schedtxt_models::user::ServiceUser,
updated_hashed_passphrase: &String,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
UPDATE "service_user" SET passphrase = $1 WHERE id = $2
"#,
)
.bind(updated_hashed_passphrase)
.bind(user.id)
.execute(pool)
.await
{
Ok(row) => {
if row.rows_affected() > 0 {
Ok(())
} else {
Err(sqlx::Error::RowNotFound)
}
}
Err(err) => Err(err),
}
}
pub async fn insert(
pool: &sqlx::PgPool,
service_user: &schedtxt_models::user::ServiceUser,
) -> Result<(uuid::Uuid, time::OffsetDateTime), sqlx::Error> {
match sqlx::query(
r#"INSERT INTO "service_user" (username, passphrase, salt_id)
VALUES ($1, $2, $3)
RETURNING id, created
"#,
)
.bind(&service_user.username)
.bind(&service_user.passphrase)
.bind(service_user.salt_id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let created: time::OffsetDateTime = row.try_get("created")?;
Ok((id, created))
}
Err(err) => Err(err),
}
}
pub async fn exists(pool: &sqlx::PgPool, service_username: &String) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
r#"
SELECT 1 FROM "service_user" WHERE username = $1
"#,
)
.bind(service_username)
.fetch_optional(pool)
.await;
match result {
Ok(r) => match r {
Some(row) => {
if row.is_empty() {
Ok(false)
} else {
Ok(true)
}
}
None => Ok(false),
},
Err(e) => {
eprintln!("What??");
eprintln!("Error: {e:?}");
Err(e)
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use josekit::{
self,
jws::alg::hmac::HmacJwsAlgorithm::Hs256,
jwt::{self},
};
use time;
pub const KEY_ENV: &str = "SECRET_KEY";
pub const MESSAGE: &str = "Something random";
pub const ISSUER: &str = "schedtxt_auth";
pub const AUDIENCE: &str = "schedtxt";
pub fn get_expiration(issued: &time::OffsetDateTime) -> Result<time::OffsetDateTime, time::Error> {
let duration_expire = time::Duration::hours(4);
Ok(*issued + duration_expire)
}
pub fn create_token(
provided_key: &str,
id: &uuid::Uuid,
) -> Result<schedtxt_models::token::CreateTokenResult, josekit::JoseError> {
let resource = schedtxt_models::token::TokenResource {
message: String::from(MESSAGE),
issuer: String::from(ISSUER),
audiences: vec![String::from(AUDIENCE)],
user_id: *id,
};
schedtxt_models::token::create_token(provided_key, resource, time::Duration::hours(4))
}
pub fn create_service_token(
provided: &str,
id: &uuid::Uuid,
) -> Result<schedtxt_models::token::CreateTokenResult, josekit::JoseError> {
let resource = schedtxt_models::token::TokenResource {
message: String::from(SERVICE_SUBJECT),
issuer: String::from(ISSUER),
audiences: vec![String::from(AUDIENCE)],
user_id: *id,
};
schedtxt_models::token::create_token(provided, resource, time::Duration::hours(1))
}
pub fn create_service_refresh_token(
key: &str,
id: &uuid::Uuid,
) -> Result<schedtxt_models::token::CreateTokenResult, josekit::JoseError> {
let resource = schedtxt_models::token::TokenResource {
message: String::from(SERVICE_SUBJECT),
issuer: String::from(ISSUER),
audiences: vec![String::from(AUDIENCE)],
user_id: *id,
};
schedtxt_models::token::create_token(key, resource, time::Duration::hours(4))
}
pub fn verify_token(key: &str, token: &str) -> bool {
match get_payload(key, token) {
Ok((payload, _header)) => match payload.subject() {
Some(_sub) => true,
None => false,
},
Err(_err) => false,
}
}
pub fn extract_id_from_token(key: &str, token: &str) -> Result<uuid::Uuid, std::io::Error> {
match get_payload(key, token) {
Ok((payload, _header)) => match payload.claim("user_id") {
Some(id) => match uuid::Uuid::parse_str(id.as_str().unwrap()) {
Ok(extracted) => Ok(extracted),
Err(err) => Err(std::io::Error::other(err.to_string())),
},
None => Err(std::io::Error::other("No claim found")),
},
Err(err) => Err(std::io::Error::other(err.to_string())),
}
}
pub const APP_TOKEN_TYPE: &str = "Schedtxt_App";
pub const APP_SUBJECT: &str = "Something random";
pub const SERVICE_TOKEN_TYPE: &str = "Schedtxt_Service";
pub const SERVICE_SUBJECT: &str = "Service random";
pub fn get_token_type(key: &str, token: &str) -> Result<String, std::io::Error> {
match get_payload(key, token) {
Ok((payload, _header)) => match payload.subject() {
Some(subject) => {
if subject == APP_SUBJECT {
Ok(String::from(APP_TOKEN_TYPE))
} else if subject == SERVICE_SUBJECT {
Ok(String::from(SERVICE_TOKEN_TYPE))
} else {
Err(std::io::Error::other(String::from("Invalid subject")))
}
}
None => Err(std::io::Error::other(String::from("Invalid payload"))),
},
Err(err) => Err(std::io::Error::other(err.to_string())),
}
}
pub fn is_token_type_valid(token_type: &str) -> bool {
token_type == SERVICE_TOKEN_TYPE
}
fn get_payload(
key: &str,
token: &str,
) -> Result<(josekit::jwt::JwtPayload, josekit::jws::JwsHeader), josekit::JoseError> {
let ver = Hs256.verifier_from_bytes(key.as_bytes()).unwrap();
jwt::decode_with_verifier(token, &ver)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize() {
let special_key = schedtxt_models::envy::environment::get_secret_key();
let id = uuid::Uuid::new_v4();
match create_token(&special_key.value, &id) {
Ok(cst) => {
let result = verify_token(&special_key.value, &cst.access_token);
assert!(result, "Token not verified");
}
Err(err) => {
assert!(false, "Error: {:?}", err.to_string());
}
};
}
}
+1094
View File
File diff suppressed because it is too large Load Diff