Compare commits

..

282 Commits

Author SHA1 Message Date
ef20829b2e 🐛 Fix did not remove user from program if they didn't pay 2025-04-02 23:15:59 +08:00
e716f5700c 🐛 Fix check punishment expires 2025-04-02 01:33:45 +08:00
b327e08062 DirectAccess in users 2025-03-29 18:04:05 +08:00
6ada023cea :arrow_up Upgrade nexus to fix panic 2025-03-29 16:00:01 +08:00
7389f70128 🛂 Clear auth context cache when logout for security 2025-03-29 15:32:20 +08:00
cc7c96311f ⬆️ Upgrade nexus 2025-03-29 15:22:53 +08:00
d3d7c4b573 ⬆️ Upgrade nexus for bug fixes 2025-03-29 13:39:57 +08:00
3d58ff2391 🔊 Add auth context cache miss log 2025-03-29 13:34:37 +08:00
9cda542b01 ⬆️ Upgrade nexus sdk 2025-03-29 13:29:39 +08:00
e1d7b4e20b ♻️ Rebuilt the otp system with cache 2025-03-29 13:22:39 +08:00
a4c6e9a905 ♻️ Rebuilt cache with the new cache system from nexus 2025-03-29 13:15:20 +08:00
c8e8104d5d Punishments permission override 2025-03-25 23:29:39 +08:00
de28ae027f Notify punishments 2025-03-25 23:20:42 +08:00
a5033c0fb0 Punishment API 2025-03-25 23:10:14 +08:00
742edaa9e2 🐛 Fix set avatar cause group permission leaked to personal 2025-03-25 21:48:51 +08:00
9712119238 🐛 Fix charging for programs 2025-03-25 21:43:49 +08:00
f8a4924d56 🐛 Fix post leave program 2025-03-24 13:11:33 +08:00
fa3561ae58 🐛 Fix place transaction wrongly 2025-03-23 21:52:10 +08:00
548bd6984b 🐛 Wrong experience check 2025-03-23 21:43:41 +08:00
27e1196130 🐛 Fix wrong determine of experience 2025-03-23 21:39:35 +08:00
173ef92c01 Program member benifits 2025-03-23 19:57:19 +08:00
23e5ba432b Programs and members 2025-03-23 19:36:27 +08:00
2ead62ecce 👽 Support wallet multi currency 2025-03-23 17:59:49 +08:00
b18dd5ef82 Permission check 2025-03-23 17:25:38 +08:00
c4758c6217 🐛 Fix large JWT header 2025-03-23 00:07:32 +08:00
f9aa4e3c10 👔 Captcha usage in check in & register
🐛 Fix counting streak bugs etc
2025-03-22 20:00:05 +08:00
34c2d0d5e7 🚚 Remap get account page path 2025-03-21 00:06:03 +08:00
82fa877cad 🐛 Prevent duplicate device token be registered 2025-03-19 21:29:14 +08:00
73683d66e8 :bug [127;2u: Fix grpc handler 2025-03-16 12:14:28 +08:00
ecaace5616 List available realms supports with publics 2025-03-16 12:12:13 +08:00
db018b35f9 👔 Prevent user from deleting their primary contact 2025-03-16 11:54:28 +08:00
45dd50ccba User able to re-send the confirm register email 2025-03-15 22:20:33 +08:00
bcb2cd2f9c GeoIP 2025-03-15 18:06:20 +08:00
32e91e2601 ♻️ Refactored event audit system 2025-03-15 16:37:47 +08:00
35e5eadb05 🐛 Fix realm did not update attachment meta 2025-03-11 00:04:10 +08:00
651a738646 👽 Support new count usage API 2025-03-10 23:15:20 +08:00
e43b9c9341 Broadcast realm was deleted 2025-03-10 22:01:16 +08:00
dfc85c5913 🐛 Fix panic on get status from cache 2025-03-09 13:36:12 +08:00
879cd7991b 🐛 Bug fix on count streak wrongly 2025-03-09 12:24:48 +08:00
84c1a8f836 👔 Optimized check in reward expr 2025-03-09 12:21:45 +08:00
0f9bde2533 👔 Update check in result caluclate expr 2025-03-09 01:30:30 +08:00
1ed9816b12 🐛 Fix key exchange to original client 2025-03-04 20:56:07 +08:00
8ca5573b86 🐛 Fix key exchange wrong client 2025-03-04 13:12:22 +08:00
8c600daf86 🐛 Fix decoding websocket package issue 2025-03-04 00:50:31 +08:00
bdef101bc0 🐛 Fix key exchange missing client id 2025-03-04 00:44:30 +08:00
d6c364ceb3 🐛 Fix key exchange 2025-03-04 00:38:16 +08:00
0c0164c2e8 🎨 Change the api handler name of listing notifications 2025-03-03 20:54:53 +08:00
53d856c7a7 🐛 Fix activating badges 2025-03-02 21:04:24 +08:00
bc31a94c98 🐛 Fix account page did not do auto migrate 2025-03-02 12:48:11 +08:00
0fac34edfb Account page (aka detailed version description) 2025-03-02 12:47:29 +08:00
5979fd5b2c Enchance user profile
💥 Move description from account to profile
2025-03-02 12:00:29 +08:00
4616f7cc93 Wearing badge 2025-03-02 11:52:45 +08:00
378c60cef8 PaKex Key exchange 2025-03-01 15:22:51 +08:00
af2bc29068 🐛 Fix db cleaner 2025-03-01 14:14:00 +08:00
07d9335180 🗑️ Clean up code 2025-03-01 14:12:29 +08:00
e737d31fd9 🐛 Fix leave realm 2025-02-22 00:27:26 +08:00
701d9b1929 🐛 Trying to fix notify again 2025-02-21 23:11:50 +08:00
e2e0cdc9b0 🐛 Fix leave member 2025-02-21 23:08:41 +08:00
e65713a5b2 🐛 Fix router stacking issue 2025-02-21 22:51:48 +08:00
816ee07947 🐛 Fix cannot add notification subscription 2025-02-21 22:39:55 +08:00
6db14a71af 👔 Prevent user from creating account with name that only includes number 2025-02-20 23:04:03 +08:00
a649f468fc 👔 Change adding realm condition 2025-02-20 22:32:47 +08:00
2f541f2346 Auto handle increment of popularity on realm member changes 2025-02-20 21:58:07 +08:00
bac655c1cf 🗃️ Realm popularity field & sorting communities according to it 2025-02-20 21:42:19 +08:00
a2d033b56c 🐛 Fix delete account at least destroy auth tickets 2025-02-20 21:22:36 +08:00
76cc34c90a 🐛 Try to fix leave own realm bug 2025-02-20 00:03:12 +08:00
160fc79a07 🐛 Fix list user 2025-02-17 23:44:17 +08:00
59e1bf1ddf 🐛 Trying to fix some bugs 2025-02-17 23:36:07 +08:00
badae3aa0d ♻️ Get users in batch supports name 2025-02-17 23:09:46 +08:00
11ca7c0ba6 Introduce batch get user api 2025-02-17 22:30:07 +08:00
09dddf7721 Support localized notification 2025-02-16 23:33:48 +08:00
4fa155cafb 👔 Delete realm will auto delete realm members 2025-02-16 19:44:00 +08:00
1bb73c996b 👽 Upgrade nexus to provide language in userinfo 2025-02-16 18:42:32 +08:00
76682ab27f 🐛 Fix wrong type of notify OTP 2025-02-15 18:33:13 +08:00
64fd2f3cd9 🐛 Fix relationships 2025-02-15 18:32:32 +08:00
497c0692ca 🐛 Fix create duplicate relationship 2025-02-15 16:00:01 +08:00
a08372d4d2 👔 No longer return error when trying add a member who already in the realm 2025-02-10 18:01:32 +08:00
271e61510e 🐛 Fix inconsistent remove member behaviour with messaging 2025-02-10 18:00:00 +08:00
4bf28e5298 Rollback skip push notify 2025-02-04 22:06:30 +08:00
7ff93f1344 🐛 Bug fixes pushing notification skip stragry 2025-02-04 14:07:29 +08:00
a40ab9efdb 🐛 Skip pushing notification when ws is available 2025-02-03 16:44:58 +08:00
bf7c10d195 Support get client by alias 2025-02-02 21:22:53 +08:00
17a99cce61 Third client query toolkit 2025-02-02 21:13:55 +08:00
7dbb552dd2 Third client query grpc endpoint
⬆️ Upgrade protobuf
2025-02-02 21:03:58 +08:00
7d1165b87b 🐛 Fix use wrong config key 2025-02-02 14:33:00 +08:00
eaa8fb5225 ♻️ Replace i18n services with nexus one 2025-02-02 14:28:03 +08:00
ec0048042a 👔 Limit max auth steps to 2 for normal users 2025-02-01 19:00:34 +08:00
62a3168190 Register with preferred language 2025-02-01 18:59:50 +08:00
79844c414b 🐛 Fix email html rendering 2025-02-01 18:27:57 +08:00
cd01657857 🐛 Bug fixes on localization 2025-02-01 18:21:54 +08:00
b68aed0284 🌐 Fully localized notification 2025-02-01 17:59:58 +08:00
d7ee87433f Push email & notification localization 2025-02-01 17:53:21 +08:00
820d96f6b0 Account preferred language 2025-02-01 16:39:14 +08:00
509390b38a Contact API 2025-02-01 14:20:03 +08:00
78d624697c 🐛 Bug fixes 2025-01-31 18:13:42 +08:00
5b1c0784ec 🐛 Fix notify one user api 2025-01-31 18:04:38 +08:00
e115374589 👔 Did not block unconfirmed user login, give them no permission instead 2025-01-29 19:45:10 +08:00
09010d5867 Assocation with Wallet to give daily rewards 2025-01-29 02:37:57 +08:00
867a556204 🐛 Fix wrong type const 2025-01-28 19:41:22 +08:00
71e959805f 🐛 Bug fixes on factor config 2025-01-28 19:33:26 +08:00
fabde24734 🐛 Trying to fix factor respond with null config 2025-01-28 19:27:48 +08:00
123342b13b Impl for totp code, and in app notify factor 2025-01-28 18:20:18 +08:00
dd9a44d126 ♻️ Refactor the get userinfo endpoint for oidc 2025-01-27 19:34:48 +08:00
dc2de65245 List, create & delete auth factor apis 2025-01-27 19:19:31 +08:00
a43e89a5a3 🐛 Bug fixes 2025-01-27 18:17:34 +08:00
0db2967214 🐛 Trying to fix ack message issue 2025-01-27 16:39:14 +08:00
0dbc30d53c 🐛 Unsubscribe the stream after used the stream to validate code 2025-01-27 16:31:15 +08:00
e4ba72725e 🐛 Trying to fix nats: subject does not match consumer 2025-01-27 16:12:09 +08:00
144219f0aa 🐛 Fix nats option durable set more than once 2025-01-27 15:53:34 +08:00
72d0cbb7be 🐛 Fix JetStream invalid stream name 2025-01-27 15:48:09 +08:00
1f75a9e64b ♻️ Use nats jetstream instead of database to store otp 2025-01-27 15:43:24 +08:00
2dac1759d9 Remove the profile field on userinfo 2025-01-25 01:24:55 +08:00
5b516094a0 🐛 Add compability on userinfo 2025-01-22 15:58:56 +08:00
6a4f2b6869 🐛 Fix openid configuration singing algorithm 2025-01-04 18:18:38 +08:00
5bdc59b4af 🚚 Move well known api path 2025-01-04 18:11:09 +08:00
5273498af1 👽 Update magic token callback url 2025-01-04 17:22:11 +08:00
fcd3b56f89 Mark all notification as read api 2025-01-01 11:16:54 +08:00
61976f6c47 👔 Remove auto enable two-step verification 2025-01-01 01:13:16 +08:00
9c0abdb46f Configurable auto signoff duration 2024-12-29 22:21:56 +08:00
f33497d002 Notify all users endpoint 2024-12-25 00:25:32 +08:00
94e55a6d4e 🐛 Fix abuse report have no default status 2024-12-12 00:32:43 +08:00
afc2ec80e9 🐛 Fix list user relative return wrong data 2024-12-11 23:58:19 +08:00
573ccc0478 🐛 Bug fixes on notifications 2024-12-09 23:34:21 +08:00
fdf2d28f51 A separate API to only get notification count 2024-12-09 23:24:12 +08:00
0115e6723a 🐛 Fix missing api endpoint 2024-12-08 22:18:45 +08:00
02f122328a Add cache into querying user 2024-12-08 20:21:40 +08:00
77c543f88e 🐛 Fix random panic 2024-12-05 13:09:52 +08:00
50498976af 🐛 Bug fix directory service wasn't registered 2024-12-03 22:12:10 +08:00
bede9716f1 🔊 Verbose logging at receive broadcasting event 2024-12-03 21:49:41 +08:00
3074cd93d8 🔊 Verbose logging at setting last seen at 2024-12-03 21:24:16 +08:00
87c3b81ad4 🐛 Fix check user exists in realm bug 2024-12-01 12:17:49 +08:00
20ff541ab4 💥 Pagination fetching realm member api
🐛 Fix remove realm member has no memberId path param
2024-12-01 11:32:54 +08:00
e0c9646c98 🐛 Prevent user adding a user twice into a realm 2024-12-01 02:04:57 +08:00
d3a1382711 Add realm member support both account name and id 2024-12-01 01:57:05 +08:00
f8492ea1af 🐛 Hotfix previous commit compile issue 2024-12-01 01:27:42 +08:00
10e9813bea 💥 Move remove member api arguments from body to querystring just as messaging 2024-12-01 01:26:33 +08:00
192a03e9d1 🚚 Move make friendship api 2024-11-30 20:25:56 +08:00
08c639497d Better relationships stauts query 2024-11-30 17:31:35 +08:00
312cf820ab Better check in experience random algorithm 2024-11-30 01:01:54 +08:00
55b3a6f164 💥 Passing relationship api arguments in body instead of querystring 2024-11-30 00:35:11 +08:00
659548a544 Result modifiers in check-in 2024-11-27 22:03:09 +08:00
c16019341f 🚚 Rename daily-sign to check-in 2024-11-27 21:57:10 +08:00
1515e29d5b 🔇 Remove authenticate result logging 2024-11-24 21:57:58 +08:00
668d9805e2 🐛 Fix group permission didn't extend properly 2024-11-23 21:44:14 +08:00
7081cb8885 🔊 Add verbose permission logging 2024-11-23 21:35:57 +08:00
bc2e7df574 🐛 Fix unable get user full perm node 2024-11-23 20:51:57 +08:00
bc5a0ea966 🐛 Fix get user grpc method didn't extend group's permission 2024-11-23 20:24:11 +08:00
f89a2244ea ⬆️ Resum go.mod 2024-11-23 13:30:26 +08:00
bf733da247 Support jwks.json according OIDC stranded 2024-11-23 13:07:49 +08:00
ca9bd7ac14 🐛 Fix notification push batch emitted twice 2024-11-23 12:43:09 +08:00
3f4fe68105 🐛 Fix notifiable is empty when user do not set 2024-11-23 12:31:00 +08:00
7a2ce20460 🔊 Verbose notifying check logging 2024-11-23 12:16:34 +08:00
720f9690f6 🔊 Verbose notifying logging 2024-11-23 00:34:53 +08:00
663b563b39 💥 Remove deprecated subscription API 2024-11-23 00:28:42 +08:00
8e1ed67c48 🐛 Fix get user info query statement bug 2024-11-17 01:05:11 +08:00
4695a617d7 Allow access user info via numeric id 2024-11-17 00:17:56 +08:00
f91be512f3 Allow get realm by numeric auto increment id 2024-11-16 21:36:34 +08:00
40b64a3559 🐛 Fix auth cache 2024-11-03 21:31:32 +08:00
808e7998dd Authkit can get user 2024-11-03 01:47:50 +08:00
40a0403521 Authkit support check user related permission 2024-11-02 12:48:32 +08:00
b4da8ce78b ♻️ Replace nil context with context.Background in authkit 2024-11-02 12:48:17 +08:00
d1f1183beb 🐛 Fix grpc namespace conflict 2024-10-31 22:40:10 +08:00
c3619f6d25 Realm operations now available in authkit 2024-10-31 22:08:51 +08:00
2b4f71e732 Add list relative method into authkit 2024-10-31 21:35:21 +08:00
39ac016b46 🧑‍💻 Improve DX by extending authkit 2024-10-31 21:26:25 +08:00
8326c716e3 Support users related rpc calls 2024-10-31 21:07:53 +08:00
8ff2648e4c 🚚 Update package name from Hypdrogen to Hypernet 2024-10-31 20:38:50 +08:00
e6f6389eb0 🔀 Merge rolled back changes 2024-10-31 00:23:53 +08:00
2fcc784bc4 Revert "♻️ Move models.Account to sec.UserInfo" for a better solution
This reverts commit 8fbb7960
2024-10-31 00:17:53 +08:00
69c6ac6581 🚚 Auth kit and parser of nexus userinfo token 2024-10-31 00:15:25 +08:00
2d322f070a 🐛 Refresh token bug 2024-10-30 23:59:16 +08:00
d7b1913bc3 🐛 Fix check perm panic 2024-10-30 23:28:26 +08:00
8fbb79607b ♻️ Move models.Account to sec.UserInfo 2024-10-30 23:26:13 +08:00
14baee03fe 🚚 Move http server package 2024-10-27 12:50:07 +08:00
7a0756a5e8 ♻️ All parts into nexus 2024-10-27 00:07:59 +08:00
4c44af74b5 ♻️ Refactored more modules into nexus 2024-10-24 00:46:59 +08:00
b4fb7b53af ♻️ Remove most of the dealer deps and move to nexus 2024-10-24 00:13:16 +08:00
e412d5e742 🐛 Trying to fix panic 2024-10-18 23:10:33 +08:00
e83e92b054 🐛 Fix notification order... again 2024-10-16 21:07:53 +08:00
10d977ab23 🐛 Fix notification order 2024-10-16 20:57:08 +08:00
29629cead6 🐛 Bug fixes on compile time 2024-10-16 01:06:27 +08:00
df8058e794 👽 Support better metadata encoder / decoder 2024-10-16 01:01:39 +08:00
be32ad1522 🐛 Bug fixes in non-cached notification preferences 2024-10-14 23:45:28 +08:00
8e3ce09734 New login alert 2024-10-14 22:28:30 +08:00
974c5d9700 More event logs point 2024-10-14 22:24:27 +08:00
a4a139c56e Implement event recorder grpc 2024-10-14 22:09:45 +08:00
06bd632f37 🐛 Bug fixes on settings auth preferences 2024-10-13 14:07:49 +08:00
68b4390edf 🐛 Bug fixes on multi-factors based authentication 2024-10-13 14:02:48 +08:00
6ea4850459 ♻️ Improve notifications mark read system 2024-10-13 13:00:51 +08:00
bee9799415 Allow user view and remove notification subscriptions 2024-10-13 12:53:47 +08:00
821e0c3e60 🐛 Trying to prevent send same notification to the same user in batch 2024-10-13 12:46:14 +08:00
39c3799d82 ♻️ Single table to store auth preferences 2024-10-13 12:36:51 +08:00
9287e6c5cc Auth config to limit auth steps 2024-10-13 01:45:08 +08:00
0f18c6ff16 🐛 Hotfix panic when getting auth context from cache 2024-10-07 00:53:23 +08:00
94c15f58ec Allow user block user initially 2024-09-26 22:09:39 +08:00
cb3cc3f540 🐛 Fix check delete account issue 2024-09-25 20:18:11 +08:00
cec234bc53 🐛 Fix wrong condition on check auth context is cached 2024-09-22 14:32:55 +08:00
27b625e0c8 Bring cache into preference notification 2024-09-22 14:04:56 +08:00
bbceb65dbf ♻️ Refactored cache system 2024-09-22 13:13:05 +08:00
648f10b25a 🐛 Fix batch notification prefs querying issue 2024-09-22 01:27:57 +08:00
0a362dadec 🐛 Try to fix notifiable check 2024-09-21 23:47:04 +08:00
9174767d80 🐛 Fix notifiable check 2024-09-21 22:54:54 +08:00
53c6b2a636 List other user daily records 2024-09-21 22:50:01 +08:00
10e4a67835 👔 Modify report abuse condition 2024-09-21 19:30:06 +08:00
ba32925b58 🐛 Tryin' to fix notifications issue 2024-09-20 21:55:25 +08:00
3031f61ea4 Account deletion 2024-09-19 22:18:22 +08:00
02bffc062f Report status update with a message 2024-09-19 21:06:35 +08:00
817c60c4e0 Abuse report system 2024-09-19 21:02:21 +08:00
5bcce9d44e 🐛 Fix cannot determine correct notifiable in batch 2024-09-17 21:15:34 +08:00
7a9e31fd86 Supply access policy in grpc get realms 2024-09-17 16:54:45 +08:00
4b4eb3e581 New query method on querying realm members 2024-09-17 16:46:58 +08:00
5203234c4a 🐛 Fix bugs in updating notification preferences 2024-09-17 15:56:41 +08:00
f287e295e5 Notification preferences 2024-09-17 14:50:05 +08:00
df9fb0a92a ♻️ Better sign in flow 2024-09-16 02:37:02 +08:00
ea3d6c20d3 👽 Support new version dealer devkit 2024-09-11 23:04:14 +08:00
313f6b8d4f Realm avatar, banner and access policy 2024-09-11 22:31:30 +08:00
582e6cdb61 🐛 Fix daily sign random panic 2024-09-08 10:39:08 +08:00
d4ed047ed7 🐛 Fix daily sign batch list query issue 2024-09-07 19:12:42 +08:00
3ce5ef6617 🐛 Fix date formatting issue on daily signing 2024-09-02 22:48:06 +08:00
a23c51f483 🐛 Fix auto maintain range missing models 2024-09-02 22:12:25 +08:00
bfddfa2201 Can get today's daily sign record 2024-09-02 20:07:19 +08:00
bee5b676fa 🐛 Fix daily check issue 2024-09-01 16:51:13 +08:00
99f8e4c891 Daily signs 2024-09-01 16:38:09 +08:00
f240226563 🐛 Fix query statement column issue 2024-08-26 00:53:13 +08:00
d205a41614 🐛 Fix bot related key api issue 2024-08-26 00:44:10 +08:00
51a53a25da 🐛 Fix preloading issue 2024-08-26 00:33:57 +08:00
1bdeba58bc Preload api key's ticket 2024-08-25 23:43:15 +08:00
46c7b6dff1 🐛 Fix api key missing account id 2024-08-25 23:15:29 +08:00
b097dab9e1 🐛 Fix api key wasn't in auto maintain range 2024-08-25 23:12:33 +08:00
52c06d9826 🐛 Fix path parameters misplaced 2024-08-25 21:35:22 +08:00
4b0910f6eb 🐛 Fix bot related bot key apis path error 2024-08-25 21:34:54 +08:00
2b2e7b5a89 Bot related bot key apis 2024-08-25 20:51:58 +08:00
476ef57236 🐛 Fix compare perm node function 2024-08-25 18:36:19 +08:00
0f2b45352c 🐛 Fix compare perm node panic 2024-08-25 18:08:37 +08:00
a6b0170b1d 🐛 Fix query issue 2024-08-25 17:16:22 +08:00
876cfa9956 Pagination bots api 2024-08-25 17:03:06 +08:00
7a585ec5b9 Return affiliated to and automated by in userinfo grpc call 2024-08-25 00:08:06 +08:00
b22657d09f Bots aka. automated accounts 2024-08-24 23:49:19 +08:00
8f61253bd3 Bot token aka. API token 2024-08-24 20:28:10 +08:00
516f5593de 🗃️ Update modeling 2024-08-24 15:17:26 +08:00
5e76fa07b7 👽 Use hyper's NetworkPackage instead of own UnifiedCommand 2024-08-24 14:29:36 +08:00
31fcfa2bf5 👽 Use pointer user id in stream request
⬆️ Re-sum go mod
2024-08-23 19:42:30 +08:00
9d075f862c ⬆️ Upgrade dealer package 2024-08-23 19:36:30 +08:00
0285b82ca2 👽 Change avatar and banner id to string 2024-08-18 22:08:58 +08:00
dd3586b241 ⬆️ Implement list user relative grpc function 2024-08-14 22:50:03 +08:00
bdcc8de59b 🐛 Fix internal token audience update isn't fully applied 2024-08-13 15:16:02 +08:00
0ee3f1bc97 Use capital to deal with links 2024-08-13 10:58:59 +08:00
a97615b6c7 🗑️ Totally removed frontend 2024-08-12 21:16:48 +08:00
fe5e9c66b9 🔀 Merge pull request '♻️ Use Capital (the new portal website) instead of embed frontend)' (#6) from refactor/use-capital-as-frontend into master
Reviewed-on: Hydrogen/Passport#6
2024-08-12 13:07:13 +00:00
e4d73b1d31 ♻️ Separate application domain and token issuer 2024-08-12 20:58:20 +08:00
142e7c3434 🗑️ Clean up code 2024-08-12 20:53:03 +08:00
7a072988ce 🐛 Fix validate condition issue 2024-08-01 23:55:33 +08:00
817c573048 🐛 Fix batch get userinfo 2024-08-01 12:47:19 +08:00
c51af61820 Better account name validation 2024-08-01 12:21:34 +08:00
688d026d75 Support batch get userinfo by username 2024-08-01 12:11:57 +08:00
ed13d62110 👽 Fix avatar cannot be updated due to attachment api updated 2024-07-31 14:16:13 +08:00
d2beaa67b4 Able get multiple userinfo in one request 2024-07-30 20:28:38 +08:00
e97f3b6047 🐛 Fix search account query 2024-07-30 19:39:53 +08:00
57b4b314fe Search accounts w/ username or nick 2024-07-30 18:20:45 +08:00
7c09138ef7 Present nonce in id token 2024-07-28 22:30:51 +08:00
6ef46d984d Better id token in oidc 2024-07-28 20:04:22 +08:00
94aed49092 Present azp in token 2024-07-28 19:50:49 +08:00
ebbfd7450c 🐛 Fix user nick condtion doesn't match registering 2024-07-27 23:29:10 +08:00
c458123d9e 🐛 Fix permissions in groups 2024-07-24 18:09:18 +08:00
7737d9fd08 Default user group 2024-07-24 17:33:05 +08:00
f113719e79 Account groups 2024-07-24 17:23:44 +08:00
2d181bfdaf 🐛 Fix endpoint conflict again... 2024-07-24 00:04:21 +08:00
24395b698e 🐛 Fix conflict on endpoints 2024-07-24 00:02:30 +08:00
cb0fefecfb More relationship APIs 2024-07-23 23:50:05 +08:00
3cab122b76 🐛 Fix cannot clear notifications 2024-07-23 22:04:52 +08:00
e56c48362d 🚚 Update API endpoints 2024-07-23 21:25:13 +08:00
d08b7a273d 🐛 Fix push notification to wrong person 2024-07-21 22:52:24 +08:00
6350ec1e43 ♻️ Use dealer postman instead of built-in feature to deliver email and notify 2024-07-21 14:22:54 +08:00
27d501d7a7 Support stream controller event emit 2024-07-20 19:58:54 +08:00
6643d31b1a Show invisible is enabled in api 2024-07-20 15:14:50 +08:00
b3bb7cb582 Provide API to try out our newest notification feature 2024-07-19 23:35:15 +08:00
8fcaf99103 Notification with avatar(only in APNs) and picture 2024-07-19 23:27:58 +08:00
282a0891d0 Optimize batch notification speed 2024-07-17 14:04:55 +08:00
7436d4b2cc 🐛 Fix concurrent write and read auth context cache 2024-07-17 13:27:16 +08:00
7b9aebd5d7 🐛 Fix attachment url issue 2024-07-16 20:11:08 +08:00
cef0a552f4 ⬆️ Configured for support dealer 2024-07-16 18:09:18 +08:00
c912271610 Make log more quiet 2024-07-16 14:15:21 +08:00
222 changed files with 11345 additions and 18709 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
/dist
/uploads
/keys
geoip.mmdb
.DS_Store

12
.idea/Passport.iml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="animate.css" level="application" />
<orderEntry type="library" name="tailwindcss" level="application" />
<orderEntry type="library" name="@tailwindcss/typography" level="application" />
</component>
</module>

View File

@@ -1,59 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="GO-241.18034.61">
<data-source name="hy_passport@localhost" uuid="74bcf3ef-a2b9-435b-b9e5-f32902a33b25">
<database-info product="PostgreSQL" version="16.3 (Homebrew)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="16.3" exact-driver-version="42.6">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>postgres</user-name>
<schema-mapping>
<introspection-scope>
<node negative="1">
<node kind="database" qname="@">
<node kind="schema" qname="@" />
</node>
<node kind="database" qname="hy_passport" />
</node>
</introspection-scope>
</schema-mapping>
</data-source>
<data-source name="passport@id.solsynth.dev" uuid="723637bc-6ce3-4bbe-afb3-d88730c75a1b">
<database-info product="PostgreSQL" version="16.2 (Ubuntu 16.2-1.pgdg22.04+1)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="16.2" exact-driver-version="42.6">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>postgres</user-name>
<schema-mapping>
<introspection-scope>
<node kind="database" qname="@">
<node kind="schema" qname="@" />
</node>
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

19
.idea/dataSources.xml generated
View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hy_passport@localhost" uuid="74bcf3ef-a2b9-435b-b9e5-f32902a33b25">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hy_passport</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="passport@id.solsynth.dev" uuid="723637bc-6ce3-4bbe-afb3-d88730c75a1b">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://id.solsynth.dev:5432/passport</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
#n:public
!<md> [41831, 0, null, null, -2147483648, -2147483648]

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
#n:public
!<md> [10102, 0, null, null, -2147483648, -2147483648]

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{@tailwindcss/typography, animate.css, tailwindcss}" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Passport.iml" filepath="$PROJECT_DIR$/.idea/Passport.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

185
.idea/workspace.xml generated
View File

@@ -1,185 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":bug: Fix model relation issue">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Go File" />
</list>
</option>
</component>
<component name="GOROOT" url="file:///opt/homebrew/opt/go/libexec" />
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="master" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="ProjectErrors" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="2fLXu43fjlLYVIGNrhGhOgBFq2O" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
&quot;Go Build.Backend.executor&quot;: &quot;Run&quot;,
&quot;Go 构建.Backend.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.go.list.on.any.changes.was.set&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;refactor/dealer&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/src/components/admin&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
]
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/web/src/components/admin" />
<recent name="$PROJECT_DIR$/web/src/views" />
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
<recent name="$PROJECT_DIR$/web" />
<recent name="$PROJECT_DIR$/pkg/services" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/web/src/views/flow" />
<recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
<recent name="$PROJECT_DIR$/pkg/internal" />
<recent name="$PROJECT_DIR$/pkg" />
</key>
</component>
<component name="RunAnythingCache">
<myKeys>
<visibility group="Grunt" flag="true" />
<visibility group="Gulp" flag="true" />
<visibility group="HTTP Requests" flag="true" />
<visibility group="HTTP 请求" flag="true" />
<visibility group="Recent projects" flag="true" />
<visibility group="Run configurations" flag="true" />
<visibility group="npm" flag="true" />
<visibility group="yarn" flag="true" />
</myKeys>
</component>
<component name="RunManager">
<configuration name="Backend" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="Passport" />
<working_directory value="$PROJECT_DIR$" />
<kind value="FILE" />
<package value="git.solsynth.dev/hydrogen/passport" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/pkg/main.go" />
<output_directory value="$PROJECT_DIR$/dist" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-gosdk-33c477a475b1-e0158606a674-org.jetbrains.plugins.go.sharedIndexes.bundled-GO-241.18034.61" />
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-GO-241.18034.61" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnknownFeatures">
<option featureType="dependencySupport" implementationName="javascript:npm:prettier" />
<option featureType="dependencySupport" implementationName="executable:docker" />
<option featureType="dependencySupport" implementationName="javascript:npm:unocss" />
<option featureType="dependencySupport" implementationName="javascript:npm:vite" />
<option featureType="dependencySupport" implementationName="executable:kubectl" />
<option featureType="dependencySupport" implementationName="javascript:npm:vue" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="refactor/v2" />
</list>
</value>
</entry>
</map>
</option>
</State>
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value=":bug: Fix bugs in status" />
<MESSAGE value=":bug: Fix status query condition" />
<MESSAGE value=":bug: Fix disturbable condition" />
<MESSAGE value=":sparkles: Admin notify all API" />
<MESSAGE value=":bug: Fix request body validation" />
<MESSAGE value=":bug: Fix API mapping issue" />
<MESSAGE value=":recycle: Improve notify API" />
<MESSAGE value=":sparkles: Reset password APIs" />
<MESSAGE value=":sparkles: Password reset &amp; user lookup API" />
<MESSAGE value=":recycle: Optimized the initial permission system" />
<MESSAGE value=":zap: Optimized audit, event logging system&#10;:sparkles: Audit logs&#10;:sparkles: Admin edit user permissions" />
<MESSAGE value=":sparkles: Admin force confirm account" />
<MESSAGE value=":sparkles: Admin notify one user" />
<MESSAGE value=":sparkles: Admin check users' auth factor" />
<MESSAGE value=":sparkles: Admin panel &amp; users, users' permissions management" />
<MESSAGE value=":bug: Fix clear function doesn't real clear items in slice" />
<MESSAGE value=":sparkles: View auth factors" />
<MESSAGE value=":sparkles: View auth factors in admin panel" />
<MESSAGE value=":rotating_light: Fix ts lint issue" />
<MESSAGE value=":poop: Remove mis-imported cgo" />
<MESSAGE value=":bug: Bug fixes" />
<MESSAGE value=":bug: Fix oauth ticket need mfa" />
<MESSAGE value=":arrow_up: Support new auth api" />
<MESSAGE value=":recycle: Refactored relation system&#10;:arrow_up: Support new realm &amp; relation api" />
<MESSAGE value=":bug: Fix model relation issue" />
<option name="LAST_COMMIT_MESSAGE" value=":bug: Fix model relation issue" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>

View File

@@ -1,23 +1,16 @@
# Building Backend
FROM golang:alpine as passport-server
RUN apk add nodejs npm
WORKDIR /source
COPY . .
WORKDIR /source/web
RUN npm install
RUN npm run build
WORKDIR /source
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/main.go
# Runtime
FROM golang:alpine
COPY --from=passport-server /dist /passport/server
COPY --from=passport-server /source/web/dist /passport/web
COPY ./templates /templates
COPY ./locales /locales
EXPOSE 8444

158
go.mod
View File

@@ -1,126 +1,100 @@
module git.solsynth.dev/hydrogen/passport
module git.solsynth.dev/hypernet/passport
go 1.21.6
toolchain go1.22.1
go 1.23.2
require (
firebase.google.com/go v3.13.0+incompatible
git.solsynth.dev/hydrogen/dealer v0.0.0-20240716024524-cfb73fde1951
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240622051057-0f56dba45745
github.com/go-playground/validator/v10 v10.17.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-jwt/jwt/v5 v5.2.0
git.solsynth.dev/hypernet/nexus v0.0.0-20250330063116-4350d197f9c6
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886
github.com/fatih/color v1.18.0
github.com/go-playground/validator/v10 v10.22.1
github.com/goccy/go-json v0.10.3
github.com/gofiber/contrib/fiberzerolog v1.0.2
github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/json-iterator/go v1.1.12
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/oschwald/geoip2-golang v1.11.0
github.com/pquerna/otp v1.4.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0
github.com/sideshow/apns2 v0.23.0
github.com/spf13/viper v1.18.2
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/spf13/viper v1.19.0
github.com/sujit-baniya/flash v0.1.8
golang.org/x/crypto v0.24.0
google.golang.org/api v0.183.0
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.2
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.6
golang.org/x/crypto v0.33.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
gorm.io/datatypes v1.2.4
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
cloud.google.com/go v0.114.0 // indirect
cloud.google.com/go/auth v0.5.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/iam v1.1.8 // indirect
cloud.google.com/go/longrunning v0.5.7 // indirect
cloud.google.com/go/storage v1.41.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/form v3.1.4+incompatible // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/eko/gocache/lib/v4 v4.2.0 // indirect
github.com/eko/gocache/store/redis/v4 v4.2.2 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
github.com/hashicorp/consul/api v1.29.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mbobakov/grpc-consul-resolver v1.5.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/nats-io/nats.go v1.37.0 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.2 // indirect
gorm.io/driver/mysql v1.5.7 // indirect
)
replace git.solsynth.dev/hydrogen/bus => ../Bus

605
go.sum
View File

@@ -1,582 +1,303 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715120403-4e96bb2cd519 h1:HR0JxkVIeN8xyasy99a0i+Vfy46uW1KthPVamCjOTPg=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715120403-4e96bb2cd519/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715142037-d7f5053fc74b h1:7pwMvP+zfl5rn08+Wg1rtcSqq1MtEN72wEmluvhBYIA=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715142037-d7f5053fc74b/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715153944-44861dc01ffb h1:8/dP5qj47UPD9TZFkYH1syRCHSUo16TDutcuMK13eQY=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715153944-44861dc01ffb/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154151-74c9ce063b4e h1:mkV/bshMqDTA3jUwsbb3MxN0d+U0I38S/teeRMoPNsk=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154151-74c9ce063b4e/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154526-59bda7a4117a h1:ozV2DWlcSCLfq3941W6C7vVGlgIQ9PH2TXfnHvSms4o=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154526-59bda7a4117a/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154745-ab0b491f45ff h1:Bzcu+n1YFD09SGoGxuFnqT56u3kK66sQ/vF8PEhbPJQ=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240715154745-ab0b491f45ff/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240716024524-cfb73fde1951 h1:RgZJK4PXhrjhUX75BclBCg2xfetMkswcMwMqfTBtryQ=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240716024524-cfb73fde1951/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240622051057-0f56dba45745 h1:40BUsQMNXjqHyytkyF9py1HjTAWlRgO6R57YXUrHNy4=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240622051057-0f56dba45745/go.mod h1:FsQGSLTl0gvo+9Jmbot02S72suyF9tFTrzDj70Xhifo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072130-f113ae6cbaf7 h1:0OitkUQJ3hrobm71UHETLB9N6jTgm6jKTeGRJuBI/6E=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072130-f113ae6cbaf7/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072729-4a08fd8f1c46 h1:oH2jq7ZG5cslCULUMWqv4dS/YNvd+Xcuv4rBPj0uGA8=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072729-4a08fd8f1c46/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c h1:XgdTgJxSAQuCbiG15hN5pY6chzcz8sX3Onm2itS+Ufs=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/nexus v0.0.0-20250330063116-4350d197f9c6 h1:K7dYn7/rAXry3dSghFVd4aHOt2+8nTbhdav6DTW8sP8=
git.solsynth.dev/hypernet/nexus v0.0.0-20250330063116-4350d197f9c6/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47 h1:fvu+bNKPTNtQocssnKbEZ66MqR0iBfAxY3HwlqnmYyE=
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47/go.mod h1:jvxq2qftz2v72x+24+cTFJdQKr9eHQTdk3KVR7cx36s=
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88 h1:2HEENe9KUrdaJeNBzx9lsuXQGyzWqCgnLTKQnr8xFr8=
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88/go.mod h1:ildzMtLagNsLK0Rkw4Hgk2TrrwqZnjwJIUx0MNZwcDY=
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886 h1:rVssXF8jZ64ctAfzlCgIgF22NCT9VAPAVxrwlcItx3s=
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886/go.mod h1:rmomNGQ6RBSp8TpZGA8tFr5M54AL2NADJ/1n0MfrIRM=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI=
github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/contrib/fiberzerolog v1.0.2 h1:LMa/luarQVeINoRwZLHtLQYepLPDIwUNB5OmdZKk+s8=
github.com/gofiber/contrib/fiberzerolog v1.0.2/go.mod h1:aTPsgArSgxRWcUeJ/K6PiICz3mbQENR1QOR426QwOoQ=
github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc=
github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI=
github.com/hashicorp/consul/proto-public v0.6.1 h1:+uzH3olCrksXYWAYHKqK782CtK9scfqH+Unlw3UHhCg=
github.com/hashicorp/consul/proto-public v0.6.1/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mbobakov/grpc-consul-resolver v1.5.3 h1:xL7nJm8qCvxgHMqlnF4naXruBUoHqfUWORl3UmwKByU=
github.com/mbobakov/grpc-consul-resolver v1.5.3/go.mod h1:0wN8+McBocuk5mO9xlAfrmBSothm7sps43bFGubg0m4=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nicksnyder/go-i18n/v2 v2.5.0 h1:3wH1gpaekcgGuwzWdSu7JwJhH9Tk87k1ezt0i1p2/Is=
github.com/nicksnyder/go-i18n/v2 v2.5.0/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sideshow/apns2 v0.23.0 h1:lpkikaZ995GIcKk6AFsYzHyezCrsrfEDvUWcWkEGErY=
github.com/sideshow/apns2 v0.23.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/sujit-baniya/flash v0.1.8 h1:BwcrybCatPU30VMA9IBA5q3ZE0VSr5c7qTqwZrSvyRI=
github.com/sujit-baniya/flash v0.1.8/go.mod h1:kmlAIkLDMlLshEeeE6fETEW8kSOopKN5WA3KXLmS/U0=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240604185151-ef581f913117 h1:HCZ6DlkKtCDAtD8ForECsY3tKuaR+p4R3grlK80uCCc=
google.golang.org/genproto v0.0.0-20240604185151-ef581f913117/go.mod h1:lesfX/+9iA+3OdqeCpoDddJaNxVB1AB6tD7EfqMmprc=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

18
locales/en-US.json Normal file
View File

@@ -0,0 +1,18 @@
{
"subjectLoginOneTimePassword": "Login verification code",
"shortBodyLoginOneTimePassword": "%s is your login verification code. It will expires in 30 minutes.",
"subjectConfirmRegistration": "Confirm your registration",
"subjectResetPassword": "Reset your password",
"subjectDeleteAccount": "Confirm your account deletion",
"subjectLoginAlert": "Login alert",
"shortBodyLoginAlert": "Your account got logged in from %s. If it's not your device, please deal with it immediately.",
"subjectAbuseReportUpdated": "Abuse report status has been changed",
"shortBodyAbuseReportUpdated": "Report #%d has been changed to %s. Moderator message: %s",
"subtitlePunishment": "Case #%d Moderated by %s",
"subjectPunishmentCreated": "You have been punished",
"shortBodyPunishmentCreated": "You have been punished for %s. Learn more inside the app.",
"subjectPunishmentUpdated": "Your punishment has been updated",
"shortBodyPunishmentUpdated": "Your punishment #%s has been updated. Learn more inside the app.",
"subjectPunishmentDeleted": "Your punishment has been revoked",
"shortBodyPunishmentDeleted": "Your punishment #%s has been revoked."
}

18
locales/zh-CN.json Normal file
View File

@@ -0,0 +1,18 @@
{
"subjectLoginOneTimePassword": "您的验证码",
"shortBodyLoginOneTimePassword": "%s 是您的登录验证码,它将在 30 分钟后过期。",
"subjectConfirmRegistration": "确认您的注册",
"subjectResetPassword": "重置您的密码",
"subjectDeleteAccount": "确认您的帐户删除",
"subjectLoginAlert": "登陆提醒",
"shortBodyLoginAlert": "您的帐户在 %s 登录,若它不是你的设备,请立即处理。",
"subjectAbuseReportUpdated": "举报状态已更新",
"shortBodyAbuseReportUpdated": "举报 #%d 已更新为 %s。管理员回复%s",
"subtitlePunishment": "案件 #%d 由 %s 处理",
"subjectPunishmentCreated": "你收到了一份处分",
"shortBodyPunishmentCreated": "你因为 %s 而被处分,详情请在应用内查看。",
"subjectPunishmentUpdated": "你的处分已更新",
"shortBodyPunishmentUpdated": "你的处分 #%s 已更新。详情请在应用内查看。",
"subjectPunishmentDeleted": "你的处分已撤销",
"shortBodyPunishmentDeleted": "你的处分 #%s 已撤销。"
}

46
pkg/authkit/audit.go Normal file
View File

@@ -0,0 +1,46 @@
package authkit
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"github.com/gofiber/fiber/v2"
)
func AddEvent(nx *nex.Conn, userId uint, action string, meta map[string]any, ip, ua string) error {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return fmt.Errorf("failed to get auth service client: %v", err)
}
_, err = proto.NewAuditServiceClient(conn).RecordEvent(context.Background(), &proto.RecordEventRequest{
UserId: uint64(userId),
Action: action,
Metadata: nex.EncodeMap(meta),
Ip: ip,
UserAgent: ua,
})
return err
}
func AddEventExt(nx *nex.Conn, action string, meta map[string]any, c *fiber.Ctx) error {
user, ok := c.Locals("nex_user").(*sec.UserInfo)
if !ok {
return fmt.Errorf("failed to get user info, make sure you call this method behind the ContextMiddleware")
}
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return fmt.Errorf("failed to get auth service client: %v", err)
}
_, err = proto.NewAuditServiceClient(conn).RecordEvent(context.Background(), &proto.RecordEventRequest{
UserId: uint64(user.ID),
Action: action,
Metadata: nex.EncodeMap(meta),
Ip: c.IP(),
UserAgent: c.Get(fiber.HeaderUserAgent),
})
return err
}

26
pkg/authkit/auth.go Normal file
View File

@@ -0,0 +1,26 @@
package authkit
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/samber/lo"
)
func EnsureUserPermGranted(nx *nex.Conn, userId, otherId uint, key string, val any) error {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return fmt.Errorf("failed to get auth service client: %v", err)
}
resp, err := proto.NewAuthServiceClient(conn).EnsureUserPermGranted(context.Background(), &proto.CheckUserPermRequest{
UserId: uint64(userId),
OtherId: uint64(otherId),
Key: key,
Value: nex.EncodeMap(val),
})
if err != nil {
return err
}
return lo.Ternary(resp.GetIsValid(), nil, fmt.Errorf("missing permission: %v", key))
}

View File

@@ -0,0 +1,19 @@
package models
import "gorm.io/datatypes"
type AccountGroup struct {
BaseModel
Name string `json:"name"`
PermNodes datatypes.JSONMap `json:"perm_nodes"`
}
type AccountGroupMember struct {
BaseModel
Account Account `json:"account"`
Group AccountGroup `json:"group"`
AccountID uint `json:"account_id"`
GroupID uint `json:"group_id"`
}

View File

@@ -4,9 +4,12 @@ import (
"fmt"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"gorm.io/datatypes"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/datatypes"
)
type Account struct {
@@ -14,44 +17,44 @@ type Account struct {
Name string `json:"name" gorm:"uniqueIndex"`
Nick string `json:"nick"`
Description string `json:"description"`
Avatar *uint `json:"avatar"`
Banner *uint `json:"banner"`
Avatar *string `json:"avatar"`
Banner *string `json:"banner"`
ConfirmedAt *time.Time `json:"confirmed_at"`
SuspendedAt *time.Time `json:"suspended_at"`
PermNodes datatypes.JSONMap `json:"perm_nodes"`
Language string `json:"language"`
AutomatedBy *Account `json:"automated_by" gorm:"foreignKey:AutomatedID"`
AutomatedID *uint `json:"automated_id"`
AffiliatedTo *Realm `json:"affiliated_to" gorm:"foreignKey:AffiliatedID"`
AffiliatedID *uint `json:"affiliated_id"`
Profile AccountProfile `json:"profile,omitempty"`
Contacts []AccountContact `json:"contacts,omitempty"`
Statuses []Status `json:"statuses,omitempty"`
Badges []Badge `json:"badges,omitempty"`
Identities []RealmMember `json:"identities,omitempty"`
Tickets []AuthTicket `json:"tickets,omitempty"`
Factors []AuthFactor `json:"factors,omitempty"`
Events []ActionEvent `json:"events,omitempty"`
MagicTokens []MagicToken `json:"-"`
ThirdClients []ThirdClient `json:"clients,omitempty"`
Notifications []Notification `json:"notifications,omitempty"`
NotifySubscribers []NotificationSubscriber `json:"notify_subscribers,omitempty"`
Relations []AccountRelationship `json:"relations,omitempty" gorm:"foreignKey:AccountID"`
Punishments []Punishment `json:"punishments,omitempty"`
// Keep this for backward compability
Description string `json:"description" gorm:"-"`
}
func (v Account) GetAvatar() *string {
if v.Avatar != nil {
return lo.ToPtr(fmt.Sprintf("%s/api/attachments/%d", viper.GetString("content_endpoint"), *v.Avatar))
return lo.ToPtr(fmt.Sprintf("%s/%s", viper.GetString("content_endpoint"), *v.Avatar))
}
return nil
}
func (v Account) GetBanner() *string {
if v.Banner != nil {
return lo.ToPtr(fmt.Sprintf("%s/api/attachments/%d", viper.GetString("content_endpoint"), *v.Banner))
return lo.ToPtr(fmt.Sprintf("%s/%s", viper.GetString("content_endpoint"), *v.Banner))
}
return nil
}
@@ -63,6 +66,16 @@ func (v Account) GetPrimaryEmail() AccountContact {
return val
}
func (v Account) EncodeToUserInfo() *proto.UserInfo {
return &proto.UserInfo{
Id: uint64(v.ID),
Name: v.Name,
Language: v.Language,
PermNodes: nex.EncodeMap(v.PermNodes),
Metadata: nex.EncodeMap(v),
}
}
type AccountContactType = int8
const (

View File

@@ -0,0 +1,16 @@
package models
import "gorm.io/datatypes"
type AuditRecord struct {
BaseModel
Action string `json:"action"`
Metadata datatypes.JSONMap `json:"metadata"`
Location *string `json:"location"`
CoordinateX *float64 `json:"coordinate_x"`
CoordinateY *float64 `json:"coordinate_y"`
UserAgent string `json:"user_agent"`
IpAddress string `json:"ip_address"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,85 @@
package models
import (
"fmt"
"time"
"gorm.io/datatypes"
)
type AuthConfig struct {
AlwaysRisky bool `json:"always_risky"`
MaximumAuthSteps int `json:"maximum_auth_steps" validate:"required,min=1,max=99"`
}
type AuthFactorType = int8
const (
PasswordAuthFactor = AuthFactorType(iota)
EmailPasswordFactor
TimeOtpFactor
InAppNotifyFactor
)
type AuthFactor struct {
BaseModel
Type int8 `json:"type"`
Secret string `json:"-"`
Config datatypes.JSONMap `json:"config"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}
type AuthTicket struct {
BaseModel
Location *string `json:"location"`
CoordinateX *float64 `json:"coordinate_x"`
CoordinateY *float64 `json:"coordinate_y"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
StepRemain int `json:"step_remain"`
Claims datatypes.JSONSlice[string] `json:"claims"`
Audiences datatypes.JSONSlice[string] `json:"audiences"`
FactorTrail datatypes.JSONSlice[int] `json:"factor_trail"`
GrantToken *string `json:"grant_token"`
AccessToken *string `json:"access_token"`
RefreshToken *string `json:"refresh_token"`
ExpiredAt *time.Time `json:"expired_at"`
AvailableAt *time.Time `json:"available_at"`
LastGrantAt *time.Time `json:"last_grant_at"`
Nonce *string `json:"nonce"`
ClientID *uint `json:"client_id"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}
func (v AuthTicket) IsAvailable() error {
if v.StepRemain > 0 {
return fmt.Errorf("ticket isn't authenticated yet")
}
if v.AvailableAt != nil && time.Now().Unix() < v.AvailableAt.Unix() {
return fmt.Errorf("ticket isn't available yet")
}
if v.ExpiredAt != nil && time.Now().Unix() > v.ExpiredAt.Unix() {
return fmt.Errorf("ticket expired")
}
return nil
}
func (v AuthTicket) IsCanBeAvailble() error {
if v.StepRemain > 0 {
return fmt.Errorf("ticket isn't authenticated yet")
}
return nil
}
type AuthContext struct {
Ticket AuthTicket `json:"ticket"`
Account Account `json:"account"`
}

View File

@@ -7,5 +7,6 @@ type Badge struct {
Type string `json:"type"`
Metadata datatypes.JSONMap `json:"metadata"`
IsActive bool `json:"is_active"`
AccountID uint `json:"account_id"`
}

13
pkg/authkit/models/bot.go Normal file
View File

@@ -0,0 +1,13 @@
package models
type ApiKey struct {
BaseModel
Name string `json:"name"`
Description string `json:"description"`
Lifecycle *int64 `json:"lifecycle"`
Ticket AuthTicket `json:"ticket" gorm:"TicketID"`
TicketID uint `json:"ticket_id"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,21 @@
package models
import "gorm.io/datatypes"
type CheckInRecord struct {
BaseModel
ResultTier int `json:"result_tier"`
ResultExperience int `json:"result_experience"`
ResultCoin float64 `json:"result_coin"`
CurrentStreak int `json:"current_streak"`
// The result modifiers are some random tips that will show up in the client;
// This field is to use to make sure the tips will be the same when the client is reloaded.
// For now, this modifier slice will contain four random numbers from 0 to 1024.
// Client should mod this modifier by the length of total available tips.
ResultModifiers datatypes.JSONSlice[int] `json:"result_modifiers"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,18 @@
package models
import "gorm.io/datatypes"
type ActionEvent struct {
BaseModel
Type string `json:"type"`
Metadata datatypes.JSONMap `json:"metadata"`
Location *string `json:"location"`
CoordinateX *float64 `json:"coordinate_x"`
CoordinateY *float64 `json:"coordinate_y"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,65 @@
package models
import (
"time"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
"gorm.io/datatypes"
)
type Notification struct {
BaseModel
Topic string `json:"topic"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Body string `json:"body"`
Metadata datatypes.JSONMap `json:"metadata"`
Priority int `json:"priority"`
SenderID *uint `json:"sender_id"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
ReadAt *time.Time `json:"read_at"`
}
func (v Notification) EncodeToPushkit() pushkit.Notification {
return pushkit.Notification{
Topic: v.Topic,
Title: v.Title,
Subtitle: v.Subtitle,
Body: v.Body,
Metadata: v.Metadata,
Priority: v.Priority,
}
}
func NewNotificationFromPushkit(pk pushkit.Notification) Notification {
return Notification{
Topic: pk.Topic,
Title: pk.Title,
Subtitle: pk.Subtitle,
Body: pk.Body,
Metadata: pk.Metadata,
Priority: pk.Priority,
SenderID: nil,
}
}
const (
NotifySubscriberFirebase = "firebase"
NotifySubscriberAPNs = "apple"
)
type NotificationSubscriber struct {
BaseModel
UserAgent string `json:"user_agent"`
Provider string `json:"provider"`
DeviceID string `json:"device_id" gorm:"uniqueIndex"`
DeviceToken string `json:"device_token"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,19 @@
package models
import "gorm.io/datatypes"
type PreferenceAuth struct {
BaseModel
Config datatypes.JSONType[AuthConfig] `json:"config"`
AccountID uint `json:"account_id"`
Account Account `json:"account"`
}
type PreferenceNotification struct {
BaseModel
Config datatypes.JSONMap `json:"config"`
AccountID uint `json:"account_id"`
Account Account `json:"account"`
}

View File

@@ -0,0 +1,31 @@
package models
import (
"time"
"gorm.io/datatypes"
)
type AccountProfile struct {
BaseModel
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Description string `json:"description"`
TimeZone string `json:"time_zone"`
Location string `json:"location"`
Pronouns string `json:"pronouns"`
Gender string `json:"gender"`
Links datatypes.JSONMap `json:"links"`
Experience uint64 `json:"experience"`
LastSeenAt *time.Time `json:"last_seen_at"`
Birthday *time.Time `json:"birthday"`
AccountID uint `json:"account_id"`
}
type AccountPage struct {
BaseModel
Content string `json:"content"`
AccountID uint `json:"account_id"`
}

View File

@@ -0,0 +1,44 @@
package models
import (
"time"
"gorm.io/datatypes"
)
type ProgramPrice struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
}
type ProgramBadge struct {
Type string `json:"type"`
Metadata map[string]any `json:"metadata"`
}
type ProgramGroup struct {
ID uint `json:"id"`
}
type Program struct {
BaseModel
Name string `json:"name"`
Description string `json:"description"`
Alias string `json:"alias" gorm:"uniqueIndex"`
ExpRequirement int64 `json:"exp_requirement"`
Price datatypes.JSONType[ProgramPrice] `json:"price"`
Badge datatypes.JSONType[ProgramBadge] `json:"badge"`
Group datatypes.JSONType[ProgramGroup] `json:"group"`
Appearance datatypes.JSONMap `json:"appearance"`
}
type ProgramMember struct {
BaseModel
LastPaid *time.Time `json:"last_paid"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
Program Program `json:"program"`
ProgramID uint `json:"program_id"`
}

View File

@@ -0,0 +1,26 @@
package models
import (
"time"
"gorm.io/datatypes"
)
const (
PunishmentTypeStrike = iota
PunishmentTypeLimited
PunishmentTypeDisabled
)
type Punishment struct {
BaseModel
Reason string `json:"reason"`
Type int `json:"type"`
PermNodes datatypes.JSONMap `json:"perm_nodes"`
ExpiredAt *time.Time `json:"expired_at"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
Moderator *Account `json:"moderator"`
ModeratorID *uint `json:"moderator_id"`
}

View File

@@ -0,0 +1,66 @@
package models
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"gorm.io/datatypes"
)
const (
RealmPopularityMemberFactor = 5
RealmPopularityPostFactor = 10
RealmPopularityChatFactor = 2
)
type Realm struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"`
Name string `json:"name"`
Description string `json:"description"`
Members []RealmMember `json:"members"`
Avatar *string `json:"avatar"`
Banner *string `json:"banner"`
Popularity int `json:"popularity"`
AccessPolicy datatypes.JSONMap `json:"access_policy"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
AccountID uint `json:"account_id"`
}
func NewRealmFromProto(proto *proto.RealmInfo) Realm {
return Realm{
BaseModel: BaseModel{
ID: uint(proto.GetId()),
},
Alias: proto.GetAlias(),
Name: proto.GetName(),
Description: proto.GetDescription(),
Avatar: &proto.Avatar,
Banner: &proto.Banner,
IsPublic: proto.GetIsPublic(),
IsCommunity: proto.GetIsCommunity(),
AccessPolicy: nex.DecodeMap(proto.GetAccessPolicy()),
}
}
type RealmMember struct {
BaseModel
RealmID uint `json:"realm_id"`
AccountID uint `json:"account_id"`
Realm Realm `json:"realm"`
Account Account `json:"account"`
PowerLevel int `json:"power_level"`
}
func NewRealmMemberFromProto(proto *proto.RealmMemberInfo) RealmMember {
return RealmMember{
BaseModel: BaseModel{
ID: uint(proto.GetId()),
},
RealmID: uint(proto.GetRealmId()),
AccountID: uint(proto.GetUserId()),
PowerLevel: int(proto.GetPowerLevel()),
}
}

View File

@@ -8,6 +8,7 @@ const (
RelationshipPending = RelationshipStatus(iota)
RelationshipFriend
RelationshipBlocked
RelationshipWaiting
)
type AccountRelationship struct {

View File

@@ -0,0 +1,19 @@
package models
const (
ReportStatusPending = "pending"
ReportStatusReviewing = "reviewing"
ReportStatusConfirmed = "confirmed"
ReportStatusRejected = "rejected"
ReportStatusProcessed = "processed"
)
type AbuseReport struct {
BaseModel
Resource string `json:"resource"`
Reason string `json:"reason"`
Status string `json:"status"`
AccountID uint `json:"account_id"`
Account Account `json:"account"`
}

View File

@@ -0,0 +1,22 @@
package models
import "time"
type MagicTokenType = int8
const (
ConfirmMagicToken = MagicTokenType(iota)
RegistrationMagicToken
ResetPasswordMagicToken
DeleteAccountMagicToken
)
type MagicToken struct {
BaseModel
Code string `json:"code"`
Type int8 `json:"type"`
AccountID *uint `json:"account_id"`
ExpiredAt *time.Time `json:"expired_at"`
LastNotifiedAt *time.Time `json:"last_notified_at"`
}

48
pkg/authkit/notify.go Normal file
View File

@@ -0,0 +1,48 @@
package authkit
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
"github.com/goccy/go-json"
)
func NotifyUser(nx *nex.Conn, userId uint64, notify pushkit.Notification, unsaved ...bool) error {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return fmt.Errorf("failed to get auth service client: %v", err)
}
raw, _ := json.Marshal(notify)
if len(unsaved) == 0 {
unsaved = append(unsaved, false)
}
_, err = proto.NewNotifyServiceClient(conn).NotifyUser(context.Background(), &proto.NotifyUserRequest{
UserId: userId,
Notify: &proto.NotifyInfoPayload{
Unsaved: unsaved[0],
Data: raw,
},
})
return err
}
func NotifyUserBatch(nx *nex.Conn, userId []uint64, notify pushkit.Notification, unsaved ...bool) error {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return fmt.Errorf("failed to get auth service client: %v", err)
}
raw, _ := json.Marshal(notify)
if len(unsaved) == 0 {
unsaved = append(unsaved, false)
}
_, err = proto.NewNotifyServiceClient(conn).NotifyUserBatch(context.Background(), &proto.NotifyUserBatchRequest{
UserId: userId,
Notify: &proto.NotifyInfoPayload{
Unsaved: unsaved[0],
Data: raw,
},
})
return err
}

27
pkg/authkit/parser.go Normal file
View File

@@ -0,0 +1,27 @@
package authkit
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
)
// GetAccountFromUserInfo returns the account from the user info
// This method will not to query the database, it will parse the token and get the subject of the userinfo token
func GetAccountFromUserInfo(info *sec.UserInfo) models.Account {
raw, _ := json.Marshal(info.Metadata)
// We assume the token is signed by the same version of service
// So directly read the data out of the metadata
var out models.Account
_ = json.Unmarshal(raw, &out)
return out
}
func ParseAccountMiddleware(c *fiber.Ctx) error {
if info, ok := c.Locals("nex_user").(*sec.UserInfo); ok {
c.Locals("user", GetAccountFromUserInfo(info))
}
return c.Next()
}

109
pkg/authkit/realm.go Normal file
View File

@@ -0,0 +1,109 @@
package authkit
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"github.com/samber/lo"
)
func GetRealm(nx *nex.Conn, id uint) (models.Realm, error) {
var realm models.Realm
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return realm, err
}
resp, err := proto.NewRealmServiceClient(conn).GetRealm(context.Background(), &proto.LookupRealmRequest{
Id: lo.ToPtr(uint64(id)),
})
if err != nil {
return realm, err
}
return models.NewRealmFromProto(resp), nil
}
func GetRealmByAlias(nx *nex.Conn, alias string) (models.Realm, error) {
var realm models.Realm
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return realm, err
}
resp, err := proto.NewRealmServiceClient(conn).GetRealm(context.Background(), &proto.LookupRealmRequest{
Alias: &alias,
})
if err != nil {
return realm, err
}
return models.NewRealmFromProto(resp), nil
}
func ListRealm(nx *nex.Conn, id []uint) ([]models.Realm, error) {
var realms []models.Realm
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return realms, err
}
resp, err := proto.NewRealmServiceClient(conn).ListRealm(context.Background(), &proto.ListRealmRequest{
Id: lo.Map(id, func(item uint, _ int) uint64 {
return uint64(item)
}),
})
if err != nil {
return realms, err
}
for _, realm := range resp.GetData() {
realms = append(realms, models.NewRealmFromProto(realm))
}
return realms, nil
}
func GetRealmMember(nx *nex.Conn, realmID, userID uint) (models.RealmMember, error) {
var member models.RealmMember
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return member, err
}
resp, err := proto.NewRealmServiceClient(conn).GetRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
RealmId: lo.ToPtr(uint64(realmID)),
UserId: lo.ToPtr(uint64(userID)),
})
if err != nil {
return member, err
}
return models.NewRealmMemberFromProto(resp), nil
}
func ListRealmMember(nx *nex.Conn, realmID uint) ([]models.RealmMember, error) {
var members []models.RealmMember
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return members, err
}
resp, err := proto.NewRealmServiceClient(conn).ListRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
RealmId: lo.ToPtr(uint64(realmID)),
})
if err != nil {
return members, err
}
for _, member := range resp.GetData() {
members = append(members, models.NewRealmMemberFromProto(member))
}
return members, nil
}
func CheckRealmMemberPerm(nx *nex.Conn, realmID uint, userID, power int) bool {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return false
}
resp, err := proto.NewRealmServiceClient(conn).CheckRealmMemberPerm(context.Background(), &proto.CheckRealmPermRequest{
RealmId: uint64(realmID),
UserId: uint64(userID),
PowerLevel: int32(power),
})
if err != nil {
return false
}
return resp.GetIsSuccess()
}

23
pkg/authkit/relative.go Normal file
View File

@@ -0,0 +1,23 @@
package authkit
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
)
func ListRelative(nx *nex.Conn, userId uint, status int32, isRelated bool) ([]*proto.UserInfo, error) {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return nil, err
}
resp, err := proto.NewUserServiceClient(conn).ListUserRelative(context.Background(), &proto.ListUserRelativeRequest{
UserId: uint64(userId),
Status: status,
IsRelated: isRelated,
})
if err != nil {
return nil, err
}
return resp.GetData(), err
}

View File

@@ -0,0 +1,65 @@
package authkit
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"github.com/samber/lo"
)
func GetThirdClient(nx *nex.Conn, id uint, secret *string) (*models.ThirdClient, error) {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return nil, fmt.Errorf("failed to get auth service client: %v", err)
}
resp, err := proto.NewThirdClientServiceClient(conn).
GetThirdClient(context.Background(), &proto.GetThirdClientRequest{
Id: lo.ToPtr(uint64(id)),
Secret: secret,
})
if err != nil {
return nil, err
}
return &models.ThirdClient{
Alias: resp.GetInfo().GetAlias(),
Name: resp.GetInfo().GetName(),
Description: resp.GetInfo().GetDescription(),
IsDraft: resp.GetInfo().GetIsDraft(),
AccountID: lo.TernaryF(resp.GetInfo().AccountId != nil, func() *uint {
return lo.ToPtr(uint(resp.GetInfo().GetAccountId()))
}, func() *uint {
return nil
}),
}, nil
}
func GetThirdClientByAlias(nx *nex.Conn, alias string, secret *string) (*models.ThirdClient, error) {
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return nil, fmt.Errorf("failed to get auth service client: %v", err)
}
resp, err := proto.NewThirdClientServiceClient(conn).
GetThirdClient(context.Background(), &proto.GetThirdClientRequest{
Alias: &alias,
Secret: secret,
})
if err != nil {
return nil, err
}
return &models.ThirdClient{
Alias: resp.GetInfo().GetAlias(),
Name: resp.GetInfo().GetName(),
Description: resp.GetInfo().GetDescription(),
IsDraft: resp.GetInfo().GetIsDraft(),
AccountID: lo.TernaryF(resp.GetInfo().AccountId != nil, func() *uint {
return lo.ToPtr(uint(resp.GetInfo().GetAccountId()))
}, func() *uint {
return nil
}),
}, nil
}

118
pkg/authkit/user.go Normal file
View File

@@ -0,0 +1,118 @@
package authkit
import (
"context"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"github.com/samber/lo"
)
func GetUser(nx *nex.Conn, userId uint) (models.Account, error) {
cacheConn, err := cachekit.NewConn(nx, 3*time.Second)
if err == nil {
key := cachekit.FKey(cachekit.DAAttachment, userId)
if user, err := cachekit.Get[models.Account](cacheConn, key); err == nil {
return user, nil
}
}
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return models.Account{}, err
}
raw, _ := proto.NewUserServiceClient(conn).GetUser(context.Background(), &proto.GetUserRequest{
UserId: lo.ToPtr(uint64(userId)),
})
return GetAccountFromUserInfo(&sec.UserInfo{
ID: uint(raw.GetId()),
Name: raw.GetName(),
PermNodes: nex.DecodeMap(raw.GetPermNodes()),
Metadata: nex.DecodeMap(raw.GetMetadata()),
}), nil
}
func GetUserByName(nx *nex.Conn, name string) (models.Account, error) {
cacheConn, err := cachekit.NewConn(nx, 3*time.Second)
if err == nil {
key := cachekit.FKey(cachekit.DAAttachment, name)
if user, err := cachekit.Get[models.Account](cacheConn, key); err == nil {
return user, nil
}
}
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return models.Account{}, err
}
raw, _ := proto.NewUserServiceClient(conn).GetUser(context.Background(), &proto.GetUserRequest{
Name: &name,
})
return GetAccountFromUserInfo(&sec.UserInfo{
ID: uint(raw.GetId()),
Name: raw.GetName(),
PermNodes: nex.DecodeMap(raw.GetPermNodes()),
Metadata: nex.DecodeMap(raw.GetMetadata()),
}), nil
}
func ListUser(nx *nex.Conn, userIds []uint) ([]models.Account, error) {
var accounts []models.Account
var missingId []uint
cachedUsers := make(map[uint]models.Account)
// Try to get users from cache
cacheConn, err := cachekit.NewConn(nx, 3*time.Second)
if err == nil {
for _, userId := range userIds {
key := cachekit.FKey(cachekit.DAAttachment, userId)
if user, err := cachekit.Get[models.Account](cacheConn, key); err == nil {
cachedUsers[userId] = user
} else {
missingId = append(missingId, userId)
}
}
}
// If all users are found in cache, return them
if len(missingId) == 0 {
for _, account := range cachedUsers {
accounts = append(accounts, account)
}
return accounts, nil
}
// Fetch missing users from the gRPC service
conn, err := nx.GetClientGrpcConn(nex.ServiceTypeAuth)
if err != nil {
return nil, err
}
raw, _ := proto.NewUserServiceClient(conn).ListUser(context.Background(), &proto.ListUserRequest{
UserId: lo.Map(missingId, func(item uint, index int) uint64 {
return uint64(item)
}),
})
// Convert fetched users and add to the result
for _, item := range raw.GetData() {
account := GetAccountFromUserInfo(&sec.UserInfo{
ID: uint(item.GetId()),
Name: item.GetName(),
PermNodes: nex.DecodeMap(item.GetPermNodes()),
Metadata: nex.DecodeMap(item.GetMetadata()),
})
accounts = append(accounts, account)
}
// Merge cached and fetched results
for _, account := range cachedUsers {
accounts = append(accounts, account)
}
return accounts, nil
}

View File

@@ -1,14 +1,17 @@
package database
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"gorm.io/gorm"
)
var AutoMaintainRange = []any{
&models.Account{},
&models.AccountGroup{},
&models.AccountGroupMember{},
&models.AuthFactor{},
&models.AccountProfile{},
&models.AccountPage{},
&models.AccountContact{},
&models.AccountRelationship{},
&models.Status{},
@@ -22,6 +25,14 @@ var AutoMaintainRange = []any{
&models.Notification{},
&models.NotificationSubscriber{},
&models.AuditRecord{},
&models.ApiKey{},
&models.CheckInRecord{},
&models.PreferenceNotification{},
&models.PreferenceAuth{},
&models.AbuseReport{},
&models.Program{},
&models.ProgramMember{},
&models.Punishment{},
}
func RunMigration(source *gorm.DB) error {

View File

@@ -1,24 +1,28 @@
package database
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"github.com/oschwald/geoip2-golang"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var C *gorm.DB
func NewGorm() error {
var err error
dsn, err := cruda.NewCrudaConn(gap.Nx).AllocDatabase("passport")
if err != nil {
return fmt.Errorf("failed to alloc database from nexus: %v", err)
}
dialector := postgres.Open(viper.GetString("database.dsn"))
C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: viper.GetString("database.prefix"),
}, Logger: logger.New(&log.Logger, logger.Config{
C, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.New(&log.Logger, logger.Config{
Colorful: true,
IgnoreRecordNotFoundError: true,
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
@@ -26,3 +30,14 @@ func NewGorm() error {
return err
}
var Gc *geoip2.Reader
func NewGeoDB() error {
conn, err := geoip2.Open(viper.GetString("geoip_db"))
if err != nil {
return fmt.Errorf("failed to open geoip database: %v", err)
}
Gc = conn
return nil
}

View File

@@ -1,15 +0,0 @@
package gap
import "net"
func GetOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "1.1.1.1:80")
if err != nil {
return nil, err
} else {
defer conn.Close()
}
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}

View File

@@ -2,41 +2,76 @@ package gap
import (
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"github.com/rs/zerolog/log"
"strings"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/rx"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit/pushcon"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
)
var H *hyper.HyperConn
var (
Nx *nex.Conn
Px *pushcon.Conn
Rx *rx.MqConn
Ca *cachekit.Conn
)
func RegisterService() error {
const (
FactorOtpPrefix = "auth-otp"
)
func InitializeToNexus() error {
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
outboundIp, _ := GetOutboundIP()
outboundIp, _ := nex.GetOutboundIP()
grpcOutbound := fmt.Sprintf("%s:%s", outboundIp, grpcBind[1])
httpOutbound := fmt.Sprintf("%s:%s", outboundIp, httpBind[1])
var err error
H, err = hyper.NewHyperConn(viper.GetString("dealer.addr"), &proto.ServiceInfo{
Nx, err = nex.NewNexusConn(viper.GetString("nexus_addr"), &proto.ServiceInfo{
Id: viper.GetString("id"),
Type: hyper.ServiceTypeAuthProvider,
Type: nex.ServiceTypeAuth,
Label: "Passport",
GrpcAddr: grpcOutbound,
HttpAddr: &httpOutbound,
HttpAddr: lo.ToPtr("http://" + httpOutbound + "/api"),
})
if err == nil {
go func() {
err := H.KeepRegisterService()
err := Nx.RunRegistering()
if err != nil {
log.Error().Err(err).Msg("An error occurred while registering service...")
}
}()
}
Px, err = pushcon.NewConn(Nx)
if err != nil {
return fmt.Errorf("error during initialize pushcon: %v", err)
}
Rx, err = rx.NewMqConn(Nx)
if err != nil {
return fmt.Errorf("error during initialize nexus rx module: %v", err)
}
Ca, err = cachekit.NewConn(Nx, time.Second*3)
if err != nil {
return fmt.Errorf("error during initialize nexus cache module: %v", err)
}
return err
}
func LoadLocalization() error {
return localize.LoadLocalization(viper.GetString("locales_dir"), viper.GetString("templates_dir"))
}

View File

@@ -2,57 +2,41 @@ package grpc
import (
"context"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
)
func (v *Server) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.AuthReply, error) {
ctx, perms, atk, rtk, err := services.Authenticate(in.GetAccessToken(), in.GetRefreshToken(), 0)
func (v *App) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.AuthReply, error) {
ticket, perms, err := services.Authenticate(uint(in.GetSessionId()))
if err != nil {
return &proto.AuthReply{
IsValid: false,
}, nil
} else {
user := ctx.Account
rawPerms, _ := jsoniter.Marshal(perms)
user := ticket.Account
userinfo := &proto.UserInfo{
Id: uint64(user.ID),
Name: user.Name,
Nick: user.Nick,
Email: user.GetPrimaryEmail().Content,
Description: &user.Description,
}
if user.Avatar != nil {
userinfo.Avatar = *user.GetAvatar()
}
if user.Banner != nil {
userinfo.Banner = *user.GetBanner()
Id: uint64(user.ID),
Name: user.Name,
PermNodes: nex.EncodeMap(perms),
Metadata: nex.EncodeMap(user),
}
return &proto.AuthReply{
IsValid: true,
Info: &proto.AuthInfo{
NewAccessToken: &atk,
NewRefreshToken: &rtk,
Permissions: rawPerms,
TicketId: uint64(ctx.Ticket.ID),
Info: userinfo,
SessionId: uint64(ticket.ID),
Info: userinfo,
},
}, nil
}
}
func (v *Server) EnsurePermGranted(_ context.Context, in *proto.CheckPermRequest) (*proto.CheckPermResponse, error) {
claims, err := services.DecodeJwt(in.GetToken())
if err != nil {
return nil, err
}
ctx, err := services.GetAuthContext(claims.ID)
func (v *App) EnsurePermGranted(_ context.Context, in *proto.CheckPermRequest) (*proto.CheckPermResponse, error) {
ctx, err := services.GetAuthContext(uint(in.GetSessionId()))
if err != nil {
return nil, err
}
@@ -63,7 +47,7 @@ func (v *Server) EnsurePermGranted(_ context.Context, in *proto.CheckPermRequest
var value any
_ = jsoniter.Unmarshal(in.GetValue(), &value)
perms := services.FilterPermNodes(heldPerms, ctx.Ticket.Claims)
perms := services.FilterPermNodes(heldPerms, ctx.Claims)
valid := services.HasPermNode(perms, in.GetKey(), value)
return &proto.CheckPermResponse{
@@ -71,7 +55,7 @@ func (v *Server) EnsurePermGranted(_ context.Context, in *proto.CheckPermRequest
}, nil
}
func (v *Server) EnsureUserPermGranted(_ context.Context, in *proto.CheckUserPermRequest) (*proto.CheckUserPermResponse, error) {
func (v *App) EnsureUserPermGranted(_ context.Context, in *proto.CheckUserPermRequest) (*proto.CheckUserPermResponse, error) {
relation, err := services.GetRelationWithTwoNode(uint(in.GetUserId()), uint(in.GetOtherId()))
if err != nil {
return &proto.CheckUserPermResponse{

View File

@@ -0,0 +1,21 @@
package grpc
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/passport/pkg/proto"
)
func (v *App) RecordEvent(ctx context.Context, request *proto.RecordEventRequest) (*proto.RecordEventResponse, error) {
services.AddEvent(
uint(request.GetUserId()),
request.GetAction(),
nex.DecodeMap(request.GetMetadata()),
request.GetIp(),
request.GetUserAgent(),
)
return &proto.RecordEventResponse{IsSuccess: true}, nil
}

View File

@@ -6,13 +6,13 @@ import (
"time"
)
func (v *Server) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
func (v *App) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
return &health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}, nil
}
func (v *Server) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
func (v *App) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
for {
if server.Send(&health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,

View File

@@ -1,47 +0,0 @@
package grpc
import (
"context"
"fmt"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
)
func (v *Server) NotifyUser(_ context.Context, in *proto.NotifyUserRequest) (*proto.NotifyResponse, error) {
var err error
var user models.Account
if user, err = services.GetAccount(uint(in.GetUserId())); err != nil {
return nil, fmt.Errorf("unable to get account: %v", err)
}
var metadata map[string]any
_ = jsoniter.Unmarshal(in.GetNotify().GetMetadata(), &metadata)
notification := models.Notification{
Topic: in.GetNotify().GetTopic(),
Title: in.GetNotify().GetTitle(),
Subtitle: in.GetNotify().Subtitle,
Body: in.GetNotify().GetBody(),
Metadata: metadata,
IsRealtime: in.GetNotify().GetIsRealtime(),
IsForcePush: in.GetNotify().GetIsForcePush(),
AccountID: user.ID,
}
if notification.IsRealtime {
if err := services.PushNotification(notification); err != nil {
return nil, err
}
} else {
if err := services.NewNotification(notification); err != nil {
return nil, err
}
}
return &proto.NotifyResponse{
IsSuccess: true,
}, nil
}

140
pkg/internal/grpc/notify.go Normal file
View File

@@ -0,0 +1,140 @@
package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
"github.com/goccy/go-json"
"github.com/rs/zerolog/log"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/passport/pkg/proto"
)
func (v *App) NotifyUser(_ context.Context, in *proto.NotifyUserRequest) (*proto.NotifyResponse, error) {
var err error
var user models.Account
if user, err = services.GetAccount(uint(in.GetUserId())); err != nil {
return nil, fmt.Errorf("unable to get account: %v", err)
}
var nty pushkit.Notification
if err = json.Unmarshal(in.GetNotify().GetData(), &nty); err != nil {
return nil, fmt.Errorf("unable to unmarshal notification: %v", err)
}
notification := models.NewNotificationFromPushkit(nty)
notification.Account = user
notification.AccountID = user.ID
log.Debug().Str("topic", notification.Topic).Uint("uid", notification.AccountID).Msg("Notifying user...")
if in.GetNotify().GetUnsaved() {
if err := services.PushNotification(notification); err != nil {
return nil, err
}
} else {
if err := services.NewNotification(notification); err != nil {
return nil, err
}
}
return &proto.NotifyResponse{
IsSuccess: true,
}, nil
}
func (v *App) NotifyUserBatch(_ context.Context, in *proto.NotifyUserBatchRequest) (*proto.NotifyResponse, error) {
var err error
var users []models.Account
if users, err = services.GetAccountList(lo.Map(in.GetUserId(), func(item uint64, index int) uint {
return uint(item)
})); err != nil {
return nil, fmt.Errorf("unable to get account: %v", err)
}
var nty pushkit.Notification
if err = json.Unmarshal(in.GetNotify().GetData(), &nty); err != nil {
return nil, fmt.Errorf("unable to unmarshal notification: %v", err)
}
var checklist = make(map[uint]bool, len(users))
var notifications []models.Notification
for _, user := range users {
if _, ok := checklist[user.ID]; ok {
continue
}
notification := models.NewNotificationFromPushkit(nty)
notification.Account = user
notification.AccountID = user.ID
checklist[user.ID] = true
notifications = append(notifications, notification)
}
if len(notifications) == 0 {
return &proto.NotifyResponse{
IsSuccess: true,
}, nil
}
log.Debug().Str("topic", notifications[0].Topic).Any("uid", lo.Keys(checklist)).Msg("Notifying users...")
if in.GetNotify().GetUnsaved() {
services.PushNotificationBatch(notifications)
} else {
if err := services.NewNotificationBatch(notifications); err != nil {
return nil, err
}
}
return &proto.NotifyResponse{
IsSuccess: true,
}, nil
}
func (v *App) NotifyAllUser(_ context.Context, in *proto.NotifyInfoPayload) (*proto.NotifyResponse, error) {
var users []models.Account
if err := database.C.Find(&users).Error; err != nil {
return nil, fmt.Errorf("unable to get account: %v", err)
}
var nty pushkit.Notification
if err := json.Unmarshal(in.GetData(), &nty); err != nil {
return nil, fmt.Errorf("unable to unmarshal notification: %v", err)
}
var checklist = make(map[uint]bool, len(users))
var notifications []models.Notification
for _, user := range users {
if checklist[user.ID] {
continue
}
notification := models.NewNotificationFromPushkit(nty)
notification.Account = user
notification.AccountID = user.ID
checklist[user.ID] = true
notifications = append(notifications, notification)
}
log.Debug().Str("topic", notifications[0].Topic).Any("uid", lo.Keys(checklist)).Msg("Notifying users...")
if in.GetUnsaved() {
services.PushNotificationBatch(notifications)
} else {
if err := services.NewNotificationBatch(notifications); err != nil {
return nil, err
}
}
return &proto.NotifyResponse{
IsSuccess: true,
}, nil
}

View File

@@ -3,58 +3,49 @@ package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"github.com/samber/lo"
)
func (v *Server) ListCommunityRealm(ctx context.Context, empty *proto.ListRealmRequest) (*proto.ListRealmResponse, error) {
realms, err := services.ListCommunityRealm()
if err != nil {
return nil, err
}
return &proto.ListRealmResponse{
Data: lo.Map(realms, func(item models.Realm, index int) *proto.RealmInfo {
return &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
}
}),
}, nil
}
func (v *Server) ListAvailableRealm(ctx context.Context, request *proto.LookupUserRealmRequest) (*proto.ListRealmResponse, error) {
func (v *App) ListAvailableRealm(ctx context.Context, request *proto.LookupUserRealmRequest) (*proto.ListRealmResponse, error) {
account, err := services.GetAccount(uint(request.GetUserId()))
if err != nil {
return nil, fmt.Errorf("unable to find target account: %v", err)
}
realms, err := services.ListAvailableRealm(account)
realms, err := services.ListAvailableRealm(account, request.GetIncludePublic())
if err != nil {
return nil, err
}
return &proto.ListRealmResponse{
Data: lo.Map(realms, func(item models.Realm, index int) *proto.RealmInfo {
return &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
info := &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
AccessPolicy: nex.EncodeMap(item.AccessPolicy),
}
if item.Avatar != nil {
info.Avatar = *item.Avatar
}
if item.Banner != nil {
info.Banner = *item.Banner
}
return info
}),
}, nil
}
func (v *Server) ListOwnedRealm(ctx context.Context, request *proto.LookupUserRealmRequest) (*proto.ListRealmResponse, error) {
func (v *App) ListOwnedRealm(ctx context.Context, request *proto.LookupUserRealmRequest) (*proto.ListRealmResponse, error) {
account, err := services.GetAccount(uint(request.GetUserId()))
if err != nil {
return nil, fmt.Errorf("unable to find target account: %v", err)
@@ -66,19 +57,55 @@ func (v *Server) ListOwnedRealm(ctx context.Context, request *proto.LookupUserRe
return &proto.ListRealmResponse{
Data: lo.Map(realms, func(item models.Realm, index int) *proto.RealmInfo {
return &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
info := &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
AccessPolicy: nex.EncodeMap(item.AccessPolicy),
}
if item.Avatar != nil {
info.Avatar = *item.Avatar
}
if item.Banner != nil {
info.Banner = *item.Banner
}
return info
}),
}, nil
}
func (v *Server) GetRealm(ctx context.Context, request *proto.LookupRealmRequest) (*proto.RealmInfo, error) {
func (v *App) ListRealm(ctx context.Context, request *proto.ListRealmRequest) (*proto.ListRealmResponse, error) {
var realms []models.Realm
if err := database.C.Where("id IN ?", request.GetId()).Find(&realms).Error; err != nil {
return nil, err
}
return &proto.ListRealmResponse{
Data: lo.Map(realms, func(item models.Realm, index int) *proto.RealmInfo {
info := &proto.RealmInfo{
Id: uint64(item.ID),
Alias: item.Alias,
Name: item.Name,
Description: item.Description,
IsPublic: item.IsPublic,
IsCommunity: item.IsCommunity,
AccessPolicy: nex.EncodeMap(item.AccessPolicy),
}
if item.Avatar != nil {
info.Avatar = *item.Avatar
}
if item.Banner != nil {
info.Banner = *item.Banner
}
return info
}),
}, nil
}
func (v *App) GetRealm(ctx context.Context, request *proto.LookupRealmRequest) (*proto.RealmInfo, error) {
var realm models.Realm
tx := database.C.Model(&models.Realm{})
@@ -99,19 +126,33 @@ func (v *Server) GetRealm(ctx context.Context, request *proto.LookupRealmRequest
return nil, err
}
return &proto.RealmInfo{
Id: uint64(realm.ID),
Alias: realm.Alias,
Name: realm.Name,
Description: realm.Description,
IsPublic: realm.IsPublic,
IsCommunity: realm.IsCommunity,
}, nil
info := &proto.RealmInfo{
Id: uint64(realm.ID),
Alias: realm.Alias,
Name: realm.Name,
Description: realm.Description,
IsPublic: realm.IsPublic,
IsCommunity: realm.IsCommunity,
AccessPolicy: nex.EncodeMap(realm.AccessPolicy),
}
if realm.Avatar != nil {
info.Avatar = *realm.Avatar
}
if realm.Banner != nil {
info.Banner = *realm.Banner
}
return info, nil
}
func (v *Server) ListRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.ListRealmMemberResponse, error) {
func (v *App) ListRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.ListRealmMemberResponse, error) {
var members []models.RealmMember
tx := database.C.Where("realm_id = ?", request.GetRealmId())
if request.UserId == nil && request.RealmId == nil {
return nil, fmt.Errorf("either user id or realm id must be provided")
}
tx := database.C
if request.RealmId != nil {
tx = tx.Where("realm_id = ?", request.GetRealmId())
}
if request.UserId != nil {
tx = tx.Where("account_id = ?", request.GetUserId())
}
@@ -123,6 +164,7 @@ func (v *Server) ListRealmMember(ctx context.Context, request *proto.RealmMember
return &proto.ListRealmMemberResponse{
Data: lo.Map(members, func(item models.RealmMember, index int) *proto.RealmMemberInfo {
return &proto.RealmMemberInfo{
Id: uint64(item.ID),
RealmId: uint64(item.RealmID),
UserId: uint64(item.AccountID),
PowerLevel: int32(item.PowerLevel),
@@ -131,9 +173,15 @@ func (v *Server) ListRealmMember(ctx context.Context, request *proto.RealmMember
}, nil
}
func (v *Server) GetRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.RealmMemberInfo, error) {
func (v *App) GetRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.RealmMemberInfo, error) {
var member models.RealmMember
tx := database.C.Where("realm_id = ?", request.GetRealmId())
if request.UserId == nil && request.RealmId == nil {
return nil, fmt.Errorf("either user id or realm id must be provided")
}
tx := database.C
if request.RealmId != nil {
tx = tx.Where("realm_id = ?", request.GetRealmId())
}
if request.UserId != nil {
tx = tx.Where("account_id = ?", request.GetUserId())
}
@@ -143,13 +191,14 @@ func (v *Server) GetRealmMember(ctx context.Context, request *proto.RealmMemberL
}
return &proto.RealmMemberInfo{
Id: uint64(member.ID),
RealmId: uint64(member.RealmID),
UserId: uint64(member.AccountID),
PowerLevel: int32(member.PowerLevel),
}, nil
}
func (v *Server) CheckRealmMemberPerm(ctx context.Context, request *proto.CheckRealmPermRequest) (*proto.CheckRealmPermResponse, error) {
func (v *App) CheckRealmMemberPerm(ctx context.Context, request *proto.CheckRealmPermRequest) (*proto.CheckRealmPermResponse, error) {
var member models.RealmMember
tx := database.C.
Where("realm_id = ?", request.GetRealmId()).

View File

@@ -1,33 +1,45 @@
package grpc
import (
"google.golang.org/grpc/reflection"
"net"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"google.golang.org/grpc/reflection"
nroto "git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"github.com/spf13/viper"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
)
import health "google.golang.org/grpc/health/grpc_health_v1"
type Server struct {
proto.UnimplementedAuthServer
proto.UnimplementedNotifierServer
proto.UnimplementedRealmServer
type App struct {
nroto.UnimplementedAuthServiceServer
nroto.UnimplementedDirectoryServiceServer
nroto.UnimplementedUserServiceServer
nroto.UnimplementedStreamServiceServer
proto.UnimplementedRealmServiceServer
proto.UnimplementedAuditServiceServer
proto.UnimplementedNotifyServiceServer
proto.UnimplementedThirdClientServiceServer
health.UnimplementedHealthServer
srv *grpc.Server
}
func NewServer() *Server {
server := &Server{
func NewServer() *App {
server := &App{
srv: grpc.NewServer(),
}
proto.RegisterAuthServer(server.srv, server)
proto.RegisterNotifierServer(server.srv, server)
proto.RegisterRealmServer(server.srv, server)
nroto.RegisterAuthServiceServer(server.srv, server)
nroto.RegisterUserServiceServer(server.srv, server)
nroto.RegisterDirectoryServiceServer(server.srv, server)
nroto.RegisterStreamServiceServer(server.srv, server)
proto.RegisterNotifyServiceServer(server.srv, server)
proto.RegisterRealmServiceServer(server.srv, server)
proto.RegisterAuditServiceServer(server.srv, server)
proto.RegisterThirdClientServiceServer(server.srv, server)
health.RegisterHealthServer(server.srv, server)
reflection.Register(server.srv)
@@ -35,7 +47,7 @@ func NewServer() *Server {
return server
}
func (v *Server) Listen() error {
func (v *App) Listen() error {
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
if err != nil {
return err

125
pkg/internal/grpc/stream.go Normal file
View File

@@ -0,0 +1,125 @@
package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
)
func (v *App) BroadcastEvent(ctx context.Context, request *proto.EventInfo) (*proto.EventResponse, error) {
log.Debug().Str("event", request.GetEvent()).
Msg("Got a broadcasting event...")
switch request.GetEvent() {
// Last seen at
case "ws.client.register":
// No longer need update user online status
// Based on realtime sever connection status
break
case "ws.client.unregister":
// Update user last seen at
data := nex.DecodeMap(request.GetData())
err := services.SetAccountLastSeen(uint(data["user"].(float64)))
log.Debug().Err(err).Any("event", data).Msg("Setting account last seen...")
}
return &proto.EventResponse{}, nil
}
func (v *App) PushStream(_ context.Context, request *proto.PushStreamRequest) (*proto.PushStreamResponse, error) {
sc := proto.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn())
var in nex.WebSocketPackage
if err := jsoniter.Unmarshal(request.GetBody(), &in); err != nil {
return nil, err
}
switch in.Action {
// PaKex (Key Exchange)
case "kex.ask":
var data struct {
UserID uint `json:"user_id" validate:"required"`
KeypairID string `json:"keypair_id" validate:"required"`
ClientID string `json:"client_id" validate:"required"`
}
err := jsoniter.Unmarshal(in.RawPayload(), &data)
if request.ClientId != nil {
data.ClientID = *request.ClientId
}
if err == nil {
err = exts.ValidateStruct(data)
}
if err != nil {
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
ClientId: request.ClientId,
Body: nex.WebSocketPackage{
Action: "error",
Message: fmt.Sprintf("unable parse payload: %v", err),
}.Marshal(),
})
break
}
// Forward ask request
sc.PushStream(context.Background(), &proto.PushStreamRequest{
UserId: lo.ToPtr(uint64(data.UserID)),
Body: nex.WebSocketPackage{
Action: "kex.ask",
Payload: data,
}.Marshal(),
})
case "kex.ack":
var data struct {
UserID uint `json:"user_id" validate:"required"`
KeypairID string `json:"keypair_id" validate:"required"`
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
ClientID string `json:"client_id" validate:"required"`
}
err := jsoniter.Unmarshal(in.RawPayload(), &data)
if err == nil {
err = exts.ValidateStruct(data)
}
if err != nil {
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
ClientId: request.ClientId,
Body: nex.WebSocketPackage{
Action: "error",
Message: fmt.Sprintf("unable parse payload: %v", err),
}.Marshal(),
})
break
}
if len(data.PublicKey) == 0 && len(data.PrivateKey) == 0 {
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
ClientId: request.ClientId,
Body: nex.WebSocketPackage{
Action: "error",
Message: "one of public key and private key is required",
}.Marshal(),
})
break
}
// Forward ack request
sc.PushStream(context.Background(), &proto.PushStreamRequest{
ClientId: &data.ClientID,
Body: nex.WebSocketPackage{
Action: "kex.ack",
Payload: data,
}.Marshal(),
})
}
return &proto.PushStreamResponse{}, nil
}

View File

@@ -0,0 +1,42 @@
package grpc
import (
"context"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (v *App) GetThirdClient(ctx context.Context, request *proto.GetThirdClientRequest) (*proto.GetThirdClientResponse, error) {
tx := database.C
if request.Id == nil && request.Alias == nil {
return nil, status.Error(codes.InvalidArgument, "either id or alias must be specified")
}
if request.Id != nil {
tx = tx.Where("id = ?", request.Id)
} else if request.Alias != nil {
tx = tx.Where("alias = ?", request.Alias)
}
var client models.ThirdClient
if err := tx.First(&client).Error; err != nil {
return nil, status.Errorf(codes.NotFound, "requested client was not found")
}
if request.Secret != nil {
if client.Secret != request.GetSecret() {
return nil, status.Errorf(codes.PermissionDenied, "invalid secret")
}
}
return &proto.GetThirdClientResponse{
Info: &proto.ThirdClientInfo{
Id: uint64(client.ID),
Name: client.Name,
Description: client.Description,
},
}, nil
}

77
pkg/internal/grpc/user.go Normal file
View File

@@ -0,0 +1,77 @@
package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
"github.com/samber/lo"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (v *App) GetUser(ctx context.Context, request *proto.GetUserRequest) (*proto.UserInfo, error) {
var account models.Account
var err error
if request.UserId != nil {
account, err = services.GetAccountForEnd(uint(request.GetUserId()))
} else if request.Name != nil {
account, err = services.GetAccountForEnd(request.GetName())
}
if err != nil {
return nil, status.Errorf(codes.NotFound, fmt.Sprintf("unable to get account punishments: %v", err))
}
return account.EncodeToUserInfo(), nil
}
func (v *App) ListUser(ctx context.Context, request *proto.ListUserRequest) (*proto.MultipleUserInfo, error) {
var accounts []models.Account
if err := database.C.
Where("id IN ?", lo.Map(request.GetUserId(), func(id uint64, _ int) interface{} { return id })).
Find(&accounts).Error; err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to list users: %v", err))
}
return &proto.MultipleUserInfo{
Data: lo.Map(request.GetUserId(), func(item uint64, index int) *proto.UserInfo {
val, ok := lo.Find(accounts, func(x models.Account) bool {
return uint(item) == x.ID
})
if !ok {
return nil
}
return val.EncodeToUserInfo()
}),
}, nil
}
func (v *App) ListUserRelative(ctx context.Context, request *proto.ListUserRelativeRequest) (*proto.ListUserRelativeResponse, error) {
tx := database.C.Preload("Account").Preload("Related").Where("status = ?", request.GetStatus())
if request.GetIsRelated() {
tx = tx.Where("related_id = ?", request.GetUserId())
} else {
tx = tx.Where("account_id = ?", request.GetUserId())
}
var data []models.AccountRelationship
if err := tx.Find(&data).Error; err != nil {
return nil, err
}
return &proto.ListUserRelativeResponse{
Data: lo.Map(data, func(item models.AccountRelationship, index int) *proto.UserInfo {
account := lo.Ternary(request.GetIsRelated(), item.Account, item.Related)
val := &proto.UserInfo{
Id: uint64(account.ID),
Name: account.Name,
}
return val
}),
}, nil
}

View File

@@ -1,13 +0,0 @@
package models
import "gorm.io/datatypes"
type AuditRecord struct {
BaseModel
Action string `json:"action"`
Metadata datatypes.JSONMap `json:"metadata"`
UserAgent string `json:"user_agent"`
IpAddress string `json:"ip_address"`
AccountID uint `json:"account_id"`
}

View File

@@ -1,64 +0,0 @@
package models
import (
"fmt"
"time"
"gorm.io/datatypes"
)
type AuthFactorType = int8
const (
PasswordAuthFactor = AuthFactorType(iota)
EmailPasswordFactor
)
type AuthFactor struct {
BaseModel
Type int8 `json:"type"`
Secret string `json:"-"`
Config JSONMap `json:"config"`
AccountID uint `json:"account_id"`
}
type AuthTicket struct {
BaseModel
Location string `json:"location"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
RequireMFA bool `json:"require_mfa"`
RequireAuthenticate bool `json:"require_authenticate"`
Claims datatypes.JSONSlice[string] `json:"claims"`
Audiences datatypes.JSONSlice[string] `json:"audiences"`
GrantToken *string `json:"grant_token"`
AccessToken *string `json:"access_token"`
RefreshToken *string `json:"refresh_token"`
ExpiredAt *time.Time `json:"expired_at"`
AvailableAt *time.Time `json:"available_at"`
LastGrantAt *time.Time `json:"last_grant_at"`
ClientID *uint `json:"client_id"`
AccountID uint `json:"account_id"`
}
func (v AuthTicket) IsAvailable() error {
if v.RequireMFA || v.RequireAuthenticate {
return fmt.Errorf("ticket isn't authenticated yet")
}
if v.AvailableAt != nil && time.Now().Unix() < v.AvailableAt.Unix() {
return fmt.Errorf("ticket isn't available yet")
}
if v.ExpiredAt != nil && time.Now().Unix() > v.ExpiredAt.Unix() {
return fmt.Errorf("ticket expired")
}
return nil
}
type AuthContext struct {
Ticket AuthTicket `json:"ticket"`
Account Account `json:"account"`
LastUsedAt time.Time `json:"last_used_at"`
}

View File

@@ -1,12 +0,0 @@
package models
type ActionEvent struct {
BaseModel
Type string `json:"type"`
Target string `json:"target"`
Location string `json:"location"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
AccountID uint `json:"account_id"`
}

View File

@@ -1,41 +0,0 @@
package models
import (
"gorm.io/datatypes"
)
type Notification struct {
BaseModel
Topic string `json:"topic"`
Title string `json:"title"`
Subtitle *string `json:"subtitle"`
Body string `json:"body"`
Metadata datatypes.JSONMap `json:"metadata"`
AccountID uint `json:"account_id"`
SenderID *uint `json:"sender_id"`
IsRealtime bool `json:"is_realtime" gorm:"-"`
IsForcePush bool `json:"is_force_push" gorm:"-"`
}
// NotificationLink Used to embed into notify and render actions
type NotificationLink struct {
Label string `json:"label"`
Url string `json:"url"`
}
const (
NotifySubscriberFirebase = "firebase"
NotifySubscriberAPNs = "apple"
)
type NotificationSubscriber struct {
BaseModel
UserAgent string `json:"user_agent"`
Provider string `json:"provider"`
DeviceID string `json:"device_id" gorm:"uniqueIndex"`
DeviceToken string `json:"device_token"`
AccountID uint `json:"account_id"`
}

View File

@@ -1,16 +0,0 @@
package models
import (
"time"
)
type AccountProfile struct {
BaseModel
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Experience uint64 `json:"experience"`
LastSeenAt *time.Time `json:"last_seen_at"`
Birthday *time.Time `json:"birthday"`
AccountID uint `json:"account_id"`
}

View File

@@ -1,23 +0,0 @@
package models
type Realm struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"`
Name string `json:"name"`
Description string `json:"description"`
Members []RealmMember `json:"members"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
AccountID uint `json:"account_id"`
}
type RealmMember struct {
BaseModel
RealmID uint `json:"realm_id"`
AccountID uint `json:"account_id"`
Realm Realm `json:"realm"`
Account Account `json:"account"`
PowerLevel int `json:"power_level"`
}

View File

@@ -1,20 +0,0 @@
package models
import "time"
type MagicTokenType = int8
const (
ConfirmMagicToken = MagicTokenType(iota)
RegistrationMagicToken
ResetPasswordMagicToken
)
type MagicToken struct {
BaseModel
Code string `json:"code"`
Type int8 `json:"type"`
AccountID *uint `json:"account_id"`
ExpiredAt *time.Time `json:"expired_at"`
}

View File

@@ -1,21 +0,0 @@
package models
import jsoniter "github.com/json-iterator/go"
type UnifiedCommand struct {
Action string `json:"w"`
Message string `json:"m"`
Payload any `json:"p"`
}
func UnifiedCommandFromError(err error) UnifiedCommand {
return UnifiedCommand{
Action: "error",
Message: err.Error(),
}
}
func (v UnifiedCommand) Marshal() []byte {
data, _ := jsoniter.Marshal(v)
return data
}

View File

@@ -1,187 +0,0 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"strconv"
"time"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper"
)
func lookupAccount(c *fiber.Ctx) error {
probe := c.Query("probe")
if len(probe) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "you must provide a probe")
}
user, err := services.LookupAccount(probe)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(user)
}
func getUserinfo(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
Preload("Contacts").
Preload("Badges").
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var resp fiber.Map
raw, _ := jsoniter.Marshal(data)
jsoniter.Unmarshal(raw, &resp)
resp["sub"] = strconv.Itoa(int(data.ID))
resp["family_name"] = data.Profile.FirstName
resp["given_name"] = data.Profile.LastName
resp["name"] = data.Name
resp["email"] = data.GetPrimaryEmail().Content
resp["preferred_username"] = data.Nick
if data.Avatar != nil {
resp["picture"] = *data.GetAvatar()
}
return c.JSON(resp)
}
func getEvents(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var count int64
var events []models.ActionEvent
if err := database.C.
Where(&models.ActionEvent{AccountID: user.ID}).
Model(&models.ActionEvent{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := database.C.
Order("created_at desc").
Where(&models.ActionEvent{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&events).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": events,
})
}
func editUserinfo(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
Nick string `json:"nick" validate:"required,min=4,max=24"`
Description string `json:"description"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Birthday time.Time `json:"birthday"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var account models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
account.Nick = data.Nick
account.Description = data.Description
account.Profile.FirstName = data.FirstName
account.Profile.LastName = data.LastName
account.Profile.Birthday = &data.Birthday
if err := database.C.Save(&account).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if err := database.C.Save(&account.Profile).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
services.InvalidAuthCacheWithUser(account.ID)
return c.SendStatus(fiber.StatusOK)
}
func doRegister(c *fiber.Ctx) error {
var data struct {
Name string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
Nick string `json:"nick" validate:"required,min=4,max=24"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=4,max=32"`
MagicToken string `json:"magic_token"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 {
return fmt.Errorf("missing magic token in request")
} else if viper.GetBool("use_registration_magic_token") {
if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil {
return err
} else {
database.C.Delete(&tk)
}
}
if user, err := services.CreateAccount(
data.Name,
data.Nick,
data.Email,
data.Password,
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(user)
}
}
func doRegisterConfirm(c *fiber.Ctx) error {
var data struct {
Code string `json:"code" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
if err := services.ConfirmAccount(data.Code); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,113 +0,0 @@
package api
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/gap"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func setAvatar(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
AttachmentID uint `json:"attachment" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pc, err := gap.H.GetServiceGrpcConn(hyper.ServiceTypeFileProvider)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "attachments services was not available")
}
if _, err := proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{
Id: lo.ToPtr(uint64(data.AttachmentID)),
Usage: lo.ToPtr("p.avatar"),
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("avatar was not found in repository: %v", err))
}
user.Avatar = &data.AttachmentID
if err := database.C.Save(&user).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
services.InvalidAuthCacheWithUser(user.ID)
}
return c.SendStatus(fiber.StatusOK)
}
func setBanner(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
AttachmentID uint `json:"attachment" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pc, err := gap.H.GetServiceGrpcConn(hyper.ServiceTypeFileProvider)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "attachments services was not available")
}
if _, err := proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{
Id: lo.ToPtr(uint64(data.AttachmentID)),
Usage: lo.ToPtr("p.banner"),
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("banner was not found in repository: %v", err))
}
user.Banner = &data.AttachmentID
if err := database.C.Save(&user).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
services.InvalidAuthCacheWithUser(user.ID)
}
return c.SendStatus(fiber.StatusOK)
}
func getAvatar(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if content := user.GetAvatar(); content == nil {
return c.SendStatus(fiber.StatusNotFound)
} else {
return c.Redirect(*content, fiber.StatusFound)
}
}
func getBanner(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if content := user.GetBanner(); content == nil {
return c.SendStatus(fiber.StatusNotFound)
} else {
return c.Redirect(*content, fiber.StatusFound)
}
}

View File

@@ -1,104 +0,0 @@
package api
import (
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App) {
app.Get("/.well-known/openid-configuration", getOidcConfiguration)
api := app.Group("/api").Name("API")
{
notify := api.Group("/notifications").Name("Notifications API")
{
notify.Get("/", getNotifications)
notify.Post("/subscribe", addNotifySubscriber)
notify.Put("/batch/read", markNotificationReadBatch)
notify.Put("/:notificationId/read", markNotificationRead)
}
api.Get("/users/lookup", lookupAccount)
me := api.Group("/users/me").Name("Myself Operations")
{
me.Get("/avatar", getAvatar)
me.Get("/banner", getBanner)
me.Put("/avatar", setAvatar)
me.Put("/banner", setBanner)
me.Get("/", getUserinfo)
me.Put("/", editUserinfo)
me.Get("/events", getEvents)
me.Get("/tickets", getTickets)
me.Delete("/tickets/:ticketId", killTicket)
me.Post("/confirm", doRegisterConfirm)
me.Post("/password-reset", requestResetPassword)
me.Patch("/password-reset", confirmResetPassword)
me.Get("/status", getMyselfStatus)
me.Post("/status", setStatus)
me.Put("/status", editStatus)
me.Delete("/status", clearStatus)
friends := me.Group("/relations").Name("Relations")
{
friends.Get("/", listRelationship)
friends.Get("/:relatedId", getRelationship)
friends.Post("/", makeFriendship)
friends.Post("/:relatedId", makeFriendship)
friends.Put("/:relatedId", editRelationship)
friends.Delete("/:relatedId", deleteRelationship)
}
}
directory := api.Group("/users/:alias").Name("User Directory")
{
directory.Get("/", getOtherUserinfo)
directory.Get("/status", getStatus)
}
api.Post("/users", doRegister)
auth := api.Group("/auth").Name("Auth")
{
auth.Post("/", doAuthenticate)
auth.Post("/mfa", doMultiFactorAuthenticate)
auth.Post("/token", getToken)
auth.Get("/tickets/:ticketId", getTicket)
auth.Get("/factors", getAvailableFactors)
auth.Post("/factors/:factorId", requestFactorToken)
auth.Get("/o/authorize", tryAuthorizeThirdClient)
auth.Post("/o/authorize", authorizeThirdClient)
}
realms := api.Group("/realms").Name("Realms API")
{
realms.Get("/", listCommunityRealm)
realms.Get("/me", listOwnedRealm)
realms.Get("/me/available", listAvailableRealm)
realms.Get("/:realm", getRealm)
realms.Get("/:realm/members", listRealmMembers)
realms.Get("/:realm/members/me", getMyRealmMember)
realms.Post("/", createRealm)
realms.Put("/:realmId", editRealm)
realms.Delete("/:realmId", deleteRealm)
realms.Post("/:realm/members", addRealmMember)
realms.Delete("/:realm/members", removeRealmMember)
realms.Delete("/:realm/members/me", leaveRealm)
}
developers := api.Group("/dev").Name("Developers API")
{
developers.Post("/notify", notifyUser)
}
api.All("/*", func(c *fiber.Ctx) error {
return fiber.ErrNotFound
})
}
}

View File

@@ -1,128 +0,0 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func getNotifications(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
tx := database.C.Where(&models.Notification{AccountID: user.ID}).Model(&models.Notification{})
var count int64
var notifications []models.Notification
if err := tx.
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := tx.
Limit(take).
Offset(offset).
Find(&notifications).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": notifications,
})
}
func markNotificationRead(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
id, _ := c.ParamsInt("notificationId", 0)
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
var notify models.Notification
if err := database.C.Where(&models.Notification{
BaseModel: models.BaseModel{ID: uint(id)},
AccountID: user.ID,
}).First(&notify).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := database.C.Delete(&notify).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}
func markNotificationReadBatch(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
MessageIDs []uint `json:"messages"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if err := database.C.Model(&models.Notification{}).
Where("recipient_id = ? AND id IN ?", user.ID, data.MessageIDs).
Delete(&models.Notification{}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}
func addNotifySubscriber(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
Provider string `json:"provider" validate:"required"`
DeviceToken string `json:"device_token" validate:"required"`
DeviceID string `json:"device_id" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var count int64
if err := database.C.Where(&models.NotificationSubscriber{
DeviceID: data.DeviceID,
DeviceToken: data.DeviceToken,
AccountID: user.ID,
}).Model(&models.NotificationSubscriber{}).Count(&count).Error; err != nil || count > 0 {
return c.SendStatus(fiber.StatusOK)
}
subscriber, err := services.AddNotifySubscriber(
user,
data.Provider,
data.DeviceID,
data.DeviceToken,
c.Get(fiber.HeaderUserAgent),
)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(subscriber)
}

View File

@@ -1,60 +0,0 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func notifyUser(c *fiber.Ctx) error {
var data struct {
ClientID string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
Topic string `json:"type" validate:"required"`
Title string `json:"subject" validate:"required,max=1024"`
Subtitle *string `json:"subtitle" validate:"max=1024"`
Body string `json:"content" validate:"required,max=4096"`
Metadata map[string]any `json:"metadata"`
IsForcePush bool `json:"is_force_push"`
IsRealtime bool `json:"is_realtime"`
UserID uint `json:"user_id" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
client, err := services.GetThirdClientWithSecret(data.ClientID, data.ClientSecret)
if err != nil {
return fiber.NewError(fiber.StatusForbidden, err.Error())
}
var user models.Account
if user, err = services.GetAccount(data.UserID); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
notification := models.Notification{
Topic: data.Topic,
Subtitle: data.Subtitle,
Title: data.Title,
Body: data.Body,
IsRealtime: data.IsRealtime,
IsForcePush: data.IsForcePush,
AccountID: user.ID,
SenderID: &client.ID,
}
if data.IsRealtime {
if err := services.PushNotification(notification); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
} else {
if err := services.NewNotification(notification); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
}
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,135 +0,0 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listRelationship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
status := c.QueryInt("status", -1)
var err error
var friends []models.AccountRelationship
if status < 0 {
if friends, err = services.ListAllRelationship(user); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
} else {
if friends, err = services.ListRelationshipWithFilter(user, models.RelationshipStatus(status)); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
}
return c.JSON(friends)
}
func getRelationship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
relatedId, _ := c.ParamsInt("relatedId", 0)
related, err := services.GetAccount(uint(relatedId))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if friend, err := services.GetRelationWithTwoNode(user.ID, related.ID); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else {
return c.JSON(friend)
}
}
func makeFriendship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
relatedName := c.Query("related")
relatedId, _ := c.ParamsInt("relatedId", 0)
var err error
var related models.Account
if relatedId > 0 {
related, err = services.GetAccount(uint(relatedId))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
} else if len(relatedName) > 0 {
related, err = services.LookupAccount(relatedName)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
} else {
return fiber.NewError(fiber.StatusBadRequest, "must one of username or user id")
}
friend, err := services.NewFriend(user, related)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(friend)
}
}
func editRelationship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
relatedId, _ := c.ParamsInt("relatedId", 0)
var data struct {
Status uint8 `json:"status"`
PermNodes map[string]any `json:"perm_nodes"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
relationship, err := services.GetRelationWithTwoNode(user.ID, uint(relatedId))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
relationship.Status = models.RelationshipStatus(data.Status)
relationship.PermNodes = data.PermNodes
if friendship, err := services.EditRelationship(relationship); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(friendship)
}
}
func deleteRelationship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
relatedId, _ := c.ParamsInt("relatedId", 0)
related, err := services.GetAccount(uint(relatedId))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
relationship, err := services.GetRelationWithTwoNode(user.ID, related.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeleteRelationship(relationship); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(relationship)
}
}

View File

@@ -1,23 +0,0 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"github.com/gofiber/fiber/v2"
)
func getOtherUserinfo(c *fiber.Ctx) error {
alias := c.Params("alias")
var account models.Account
if err := database.C.
Where(&models.Account{Name: alias}).
Omit("tickets", "challenges", "factors", "events", "clients", "notifications", "notify_subscribers").
Preload("Profile").
Preload("Badges").
First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(account)
}

View File

@@ -1,56 +0,0 @@
package exts
import (
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"strings"
)
func AuthMiddleware(c *fiber.Ctx) error {
var atk string
if cookie := c.Cookies(hyper.CookieAtk); len(cookie) > 0 {
atk = cookie
}
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
tk := strings.Replace(header, "Bearer", "", 1)
atk = strings.TrimSpace(tk)
}
if tk := c.Query("tk"); len(tk) > 0 {
atk = strings.TrimSpace(tk)
}
c.Locals("p_token", atk)
rtk := c.Cookies(hyper.CookieRtk)
if ctx, perms, newAtk, newRtk, err := services.Authenticate(atk, rtk, 0); err == nil {
if newAtk != atk {
SetAuthCookies(c, newAtk, newRtk)
}
c.Locals("permissions", perms)
c.Locals("user", ctx.Account)
}
return c.Next()
}
func EnsureAuthenticated(c *fiber.Ctx) error {
if _, ok := c.Locals("user").(models.Account); !ok {
return fiber.NewError(fiber.StatusUnauthorized)
}
return nil
}
func EnsureGrantedPerm(c *fiber.Ctx, key string, val any) error {
if err := EnsureAuthenticated(c); err != nil {
return err
}
perms := c.Locals("permissions").(map[string]any)
if !services.HasPermNode(perms, key, val) {
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("missing permission: %s", key))
}
return nil
}

View File

@@ -1,27 +0,0 @@
package exts
import (
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
"time"
)
func SetAuthCookies(c *fiber.Ctx, atk, rtk string) {
c.Cookie(&fiber.Cookie{
Name: hyper.CookieAtk,
Value: atk,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(60 * time.Minute),
Path: "/",
})
c.Cookie(&fiber.Cookie{
Name: hyper.CookieRtk,
Value: rtk,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(24 * 30 * time.Hour),
Path: "/",
})
}

View File

@@ -1,84 +0,0 @@
package server
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/admin"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/api"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"net/http"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/favicon"
"github.com/gofiber/fiber/v2/middleware/idempotency"
"github.com/gofiber/fiber/v2/middleware/logger"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type HTTPApp struct {
app *fiber.App
}
func NewServer() *HTTPApp {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hydrogen.Passport",
AppName: "Hydrogen.Passport",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
app.Use(idempotency.New())
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowMethods: strings.Join([]string{
fiber.MethodGet,
fiber.MethodPost,
fiber.MethodHead,
fiber.MethodOptions,
fiber.MethodPut,
fiber.MethodDelete,
fiber.MethodPatch,
}, ","),
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
app.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
app.Use(exts.AuthMiddleware)
admin.MapAdminAPIs(app)
api.MapAPIs(app)
app.Use(filesystem.New(filesystem.Config{
Root: http.Dir(viper.GetString("frontend_app")),
Index: "index.html",
NotFoundFile: "index.html",
MaxAge: 3600,
}))
app.Use(favicon.New(favicon.Config{
File: filepath.Join(viper.GetString("frontend_app"), "favicon.png"),
URL: "/favicon.png",
}))
return &HTTPApp{app}
}
func (v *HTTPApp) Listen() {
if err := v.app.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}
}

View File

@@ -0,0 +1,25 @@
package services
import (
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/samber/lo"
)
func GetUserAccountGroup(user models.Account) ([]models.AccountGroup, error) {
var members []models.AccountGroupMember
if err := database.C.Where(&models.AccountGroupMember{
AccountID: user.ID,
}).Find(&members).Error; err != nil {
return nil, err
}
var groups []models.AccountGroup
if err := database.C.Where("id IN ?", lo.Map(members, func(item models.AccountGroupMember, index int) uint {
return item.GroupID
})).Find(&groups).Error; err != nil {
return nil, err
}
return groups, nil
}

View File

@@ -1,19 +1,115 @@
package services
import (
"context"
"fmt"
"maps"
"time"
"unicode"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"gorm.io/datatypes"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"github.com/google/uuid"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/samber/lo"
)
func KgAccountCache(query any) string {
return cachekit.FKey(cachekit.DAUser, query)
}
func CacheAccount(account models.Account) {
cachekit.Set[models.Account](
gap.Ca,
KgAccountCache(account.Name),
account,
60*time.Minute,
fmt.Sprintf("user#%d", account.ID),
)
cachekit.Set[models.Account](
gap.Ca,
KgAccountCache(account.ID),
account,
60*time.Minute,
fmt.Sprintf("user#%d", account.ID),
)
}
func ValidateAccountName(val string, min, max int) bool {
actualLength := 0
for _, r := range val {
if unicode.Is(unicode.Han, r) || unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) || unicode.Is(unicode.Hangul, r) {
actualLength += 2
} else {
actualLength += 1
}
}
return actualLength >= min && max >= actualLength
}
func GetAccountForEnd(id any) (models.Account, error) {
if val, err := cachekit.Get[models.Account](gap.Ca, KgAccountCache(id)); err == nil {
return val, err
}
var account models.Account
tx := database.C
switch id.(type) {
case uint:
tx = tx.Where("id = ?", id)
case string:
tx = tx.Where("name = ?", id)
default:
return account, fmt.Errorf("invalid account id type")
}
if err := tx.
Preload("Profile").
Preload("Badges", func(db *gorm.DB) *gorm.DB {
return db.Order("badges.is_active DESC, badges.type DESC")
}).
First(&account).Error; err != nil {
return account, fmt.Errorf("requested user with id %d was not found", id)
}
groups, err := GetUserAccountGroup(account)
if err != nil {
return account, fmt.Errorf("unable to get account groups: %v", err)
}
for _, group := range groups {
for k, v := range group.PermNodes {
if _, ok := account.PermNodes[k]; !ok {
account.PermNodes[k] = v
}
}
}
punishments, err := ListPunishments(account)
if err != nil {
return account, fmt.Errorf("unable to get account punishments: %v", err)
}
account.Punishments = punishments
for _, punishment := range punishments {
if punishment.Type == models.PunishmentTypeLimited && len(punishment.PermNodes) > 0 {
maps.Copy(account.PermNodes, punishment.PermNodes)
}
}
CacheAccount(account)
return account, nil
}
func GetAccount(id uint) (models.Account, error) {
var account models.Account
if err := database.C.Where(models.Account{
@@ -25,6 +121,15 @@ func GetAccount(id uint) (models.Account, error) {
return account, nil
}
func GetAccountList(id []uint) ([]models.Account, error) {
var accounts []models.Account
if err := database.C.Where("id IN ?", id).Find(&accounts).Error; err != nil {
return accounts, err
}
return accounts, nil
}
func GetAccountWithName(alias string) (models.Account, error) {
var account models.Account
if err := database.C.Where(models.Account{
@@ -55,7 +160,16 @@ func LookupAccount(probe string) (models.Account, error) {
return account, fmt.Errorf("account was not found")
}
func CreateAccount(name, nick, email, password string) (models.Account, error) {
func SearchAccount(probe string) ([]models.Account, error) {
probe = "%" + probe + "%"
var accounts []models.Account
if err := database.C.Where("name LIKE ? OR nick LIKE ?", probe, probe).Find(&accounts).Error; err != nil {
return accounts, err
}
return accounts, nil
}
func CreateAccount(name, nick, email, password, lang string) (models.Account, error) {
user := models.Account{
Name: name,
Nick: nick,
@@ -67,10 +181,6 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
Type: models.PasswordAuthFactor,
Secret: HashPassword(password),
},
{
Type: models.EmailPasswordFactor,
Secret: uuid.NewString()[:8],
},
},
Contacts: []models.AccountContact{
{
@@ -80,13 +190,15 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
VerifiedAt: nil,
},
},
PermNodes: datatypes.JSONMap(viper.GetStringMap("permissions.default")),
Language: lang,
PermNodes: datatypes.JSONMap{},
ConfirmedAt: nil,
}
if err := database.C.Create(&user).Error; err != nil {
return user, err
}
// Only gave user permission group after they confiremd the registeration
if tk, err := NewMagicToken(models.ConfirmMagicToken, &user, nil); err != nil {
return user, err
@@ -124,19 +236,77 @@ func ConfirmAccount(code string) error {
func ForceConfirmAccount(user models.Account) error {
user.ConfirmedAt = lo.ToPtr(time.Now())
for k, v := range viper.GetStringMap("permissions.verified") {
if val, ok := user.PermNodes[k]; !ok {
user.PermNodes[k] = v
} else {
user.PermNodes[k] = val
}
if viper.GetInt("default_user_group") > 0 {
database.C.Create(&models.AccountGroupMember{
AccountID: user.ID,
GroupID: uint(viper.GetInt("default_user_group")),
})
}
_ = database.C.Model(&models.AccountContact{}).Where("account_id = ?", user.ID).Updates(&models.AccountContact{
VerifiedAt: lo.ToPtr(time.Now()),
})
if err := database.C.Save(&user).Error; err != nil {
return err
}
InvalidAuthCacheWithUser(user.ID)
InvalidUserAuthCache(user.ID)
return nil
}
func CheckAbleToDeleteAccount(user models.Account) error {
if user.AutomatedID != nil {
return fmt.Errorf("bot cannot request delete account, head to developer portal and dispose bot")
}
var count int64
if err := database.C.
Where("account_id = ?", user.ID).
Where("expired_at < ?", time.Now()).
Where("type = ?", models.DeleteAccountMagicToken).
Model(&models.MagicToken{}).
Count(&count).Error; err != nil {
return fmt.Errorf("unable to check delete account ability: %v", err)
} else if count > 0 {
return fmt.Errorf("you requested delete account recently")
}
return nil
}
func RequestDeleteAccount(user models.Account) error {
if tk, err := NewMagicToken(
models.DeleteAccountMagicToken,
&user,
lo.ToPtr(time.Now().Add(24*time.Hour)),
); err != nil {
return err
} else if err := NotifyMagicToken(tk); err != nil {
log.Error().
Err(err).
Str("code", tk.Code).
Uint("user", user.ID).
Msg("Failed to notify delete account magic token...")
}
return nil
}
func ConfirmDeleteAccount(code string) error {
token, err := ValidateMagicToken(code, models.DeleteAccountMagicToken)
if err != nil {
return err
} else if token.AccountID == nil {
return fmt.Errorf("magic token didn't assign a valid account")
}
if err := DeleteAccount(*token.AccountID); err != nil {
return err
} else {
database.C.Delete(&token)
}
return nil
}
@@ -194,45 +364,49 @@ func ConfirmResetPassword(code, newPassword string) error {
factor.Secret = HashPassword(newPassword)
}
return database.C.Save(&factor).Error
if err = database.C.Save(&factor).Error; err != nil {
return err
} else {
database.C.Delete(&token)
}
return nil
}
func DeleteAccount(id uint) error {
tx := database.C.Begin()
for _, model := range []any{
&models.Badge{},
&models.RealmMember{},
&models.AccountContact{},
&models.AuthFactor{},
&models.AuthTicket{},
&models.MagicToken{},
&models.ThirdClient{},
&models.NotificationSubscriber{},
&models.AccountRelationship{},
} {
if err := tx.Delete(model, "account_id = ?", id).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Delete(&models.AuthTicket{}, "account_id = ?", id).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Delete(&models.Notification{}, "recipient_id = ?", id).Error; err != nil {
if err := tx.Select(clause.Associations).Delete(&models.Account{}, "id = ?", id).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Delete(&models.Account{}, "id = ?", id).Error; err != nil {
tx.Rollback()
if err := tx.Commit().Error; err != nil {
return err
} else {
InvalidUserAuthCache(id)
conn := gap.Nx.GetNexusGrpcConn()
_, _ = proto.NewDirectoryServiceClient(conn).BroadcastEvent(context.Background(), &proto.EventInfo{
Event: "deletion",
Data: nex.EncodeMap(map[string]any{
"type": "account",
"id": id,
}),
})
}
return tx.Commit().Error
return nil
}
func RecycleUnConfirmAccount() {
deadline := time.Now().Add(-24 * time.Hour)
var hitList []models.Account
if err := database.C.Where("confirmed_at IS NULL").Find(&hitList).Error; err != nil {
if err := database.C.Where("confirmed_at IS NULL AND created_at <= ?", deadline).Find(&hitList).Error; err != nil {
log.Error().Err(err).Msg("An error occurred while recycling accounts...")
return
}

View File

@@ -2,45 +2,27 @@ package services
import (
"fmt"
"sync"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
var (
authContextMutex sync.Mutex
authContextCache = make(map[string]models.AuthContext)
)
func Authenticate(atk, rtk string, rty int) (ctx models.AuthContext, perms map[string]any, newAtk, newRtk string, err error) {
var claims PayloadClaims
claims, err = DecodeJwt(atk)
if err != nil {
if len(rtk) > 0 && rty < 1 {
// Auto refresh and retry
newAtk, newRtk, err = RefreshToken(rtk)
if err == nil {
return Authenticate(newAtk, newRtk, rty+1)
}
}
err = fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid auth key: %v", err))
return
}
newAtk = atk
newRtk = rtk
if ctx, err = GetAuthContext(claims.ID); err == nil {
func Authenticate(sessionId uint) (ctx models.AuthTicket, perms map[string]any, err error) {
if ctx, err = GetAuthContext(sessionId); err == nil {
var heldPerms map[string]any
rawHeldPerms, _ := jsoniter.Marshal(ctx.Account.PermNodes)
_ = jsoniter.Unmarshal(rawHeldPerms, &heldPerms)
perms = FilterPermNodes(heldPerms, ctx.Ticket.Claims)
perms = FilterPermNodes(heldPerms, ctx.Claims)
ctx.Account.PermNodes = perms
return
}
@@ -48,78 +30,76 @@ func Authenticate(atk, rtk string, rty int) (ctx models.AuthContext, perms map[s
return
}
func GetAuthContext(jti string) (models.AuthContext, error) {
var err error
var ctx models.AuthContext
func KgAuthContextCache(sessionId uint) string {
return cachekit.FKey("auth-context", sessionId)
}
if val, ok := authContextCache[jti]; ok {
func GetAuthContext(sessionId uint) (models.AuthTicket, error) {
var err error
var ctx models.AuthTicket
key := KgAuthContextCache(sessionId)
if val, err := cachekit.Get[models.AuthTicket](gap.Ca, key); err == nil {
ctx = val
ctx.LastUsedAt = time.Now()
authContextMutex.Lock()
authContextCache[jti] = ctx
authContextMutex.Unlock()
log.Debug().Str("jti", jti).Msg("Used an auth context cache")
} else {
ctx, err = CacheAuthContext(jti)
log.Debug().Str("jti", jti).Msg("Created a new auth context cache")
log.Error().Err(err).Msg("Unable to get auth context cache")
ctx, err = CacheAuthContext(sessionId)
if err != nil {
log.Error().Err(err).Msg("Unable to cache auth context")
} else {
log.Debug().Uint("session", sessionId).Msg("Created a new auth context cache")
}
}
return ctx, err
}
func CacheAuthContext(jti string) (models.AuthContext, error) {
var ctx models.AuthContext
func CacheAuthContext(sessionId uint) (models.AuthTicket, error) {
// Query data from primary database
ticket, err := GetTicketWithToken(jti)
if err != nil {
return ctx, fmt.Errorf("invalid auth ticket: %v", err)
var ticket models.AuthTicket
if err := database.C.
Where("id = ?", sessionId).
First(&ticket).Error; err != nil {
return ticket, fmt.Errorf("invalid auth ticket: %v", err)
} else if err := ticket.IsAvailable(); err != nil {
return ctx, fmt.Errorf("unavailable auth ticket: %v", err)
return ticket, fmt.Errorf("unavailable auth ticket: %v", err)
}
user, err := GetAccount(ticket.AccountID)
if err != nil {
return ctx, fmt.Errorf("invalid account: %v", err)
return ticket, fmt.Errorf("invalid account: %v", err)
}
groups, err := GetUserAccountGroup(user)
if err != nil {
return ticket, fmt.Errorf("unable to get account groups: %v", err)
}
ctx = models.AuthContext{
Ticket: ticket,
Account: user,
LastUsedAt: time.Now(),
}
// Put the data into memory for cache
authContextMutex.Lock()
authContextCache[jti] = ctx
authContextMutex.Unlock()
return ctx, nil
}
func RecycleAuthContext() {
if len(authContextCache) == 0 {
return
}
affected := 0
for key, val := range authContextCache {
if val.LastUsedAt.Add(60*time.Second).Unix() < time.Now().Unix() {
affected++
authContextMutex.Lock()
delete(authContextCache, key)
authContextMutex.Unlock()
for _, group := range groups {
for k, v := range group.PermNodes {
if _, ok := user.PermNodes[k]; !ok {
user.PermNodes[k] = v
}
}
}
log.Debug().Int("affected", affected).Msg("Recycled auth context...")
ticket.Account = user
// Put the data into the cache
key := KgAuthContextCache(sessionId)
err = cachekit.Set[models.AuthTicket](
gap.Ca,
key,
ticket,
time.Minute*10,
"auth-context",
fmt.Sprintf("user#%d", user.ID),
)
if err != nil {
log.Error().Err(err).Msg("Unable to cache auth context...")
}
return ticket, err
}
func InvalidAuthCacheWithUser(userId uint) {
for key, val := range authContextCache {
if val.Account.ID == userId {
authContextMutex.Lock()
delete(authContextCache, key)
authContextMutex.Unlock()
}
}
func InvalidUserAuthCache(uid uint) {
cachekit.DeleteByTags(gap.Ca, "auth-context", fmt.Sprintf("user#%d", uid))
}

View File

@@ -1,8 +1,8 @@
package services
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
)
func GrantBadge(user models.Account, badge models.Badge) error {
@@ -13,3 +13,23 @@ func GrantBadge(user models.Account, badge models.Badge) error {
func RevokeBadge(badge models.Badge) error {
return database.C.Delete(&badge).Error
}
func ActiveBadge(badge models.Badge) error {
accountId := badge.AccountID
tx := database.C.Begin()
if err := tx.Model(&models.Badge{}).Where("account_id = ?", accountId).Update("is_active", false).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Model(&models.Badge{}).Where("id = ?", badge.ID).Update("is_active", true).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,56 @@
package services
import (
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"time"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/google/uuid"
"github.com/samber/lo"
)
func NewApiKey(user models.Account, key models.ApiKey, ip, ua string, claims []string) (models.ApiKey, error) {
key.Account = user
key.AccountID = user.ID
var expiredAt *time.Time
if key.Lifecycle != nil {
expiredAt = lo.ToPtr(time.Now().Add(time.Duration(*key.Lifecycle) * time.Second))
}
key.Ticket = models.AuthTicket{
IpAddress: ip,
UserAgent: ua,
StepRemain: 0,
Claims: claims,
Audiences: []string{InternalTokenAudience},
GrantToken: lo.ToPtr(uuid.NewString()),
AccessToken: lo.ToPtr(uuid.NewString()),
RefreshToken: lo.ToPtr(uuid.NewString()),
AvailableAt: lo.ToPtr(time.Now()),
ExpiredAt: expiredAt,
Account: user,
AccountID: user.ID,
}
if err := database.C.Save(&key).Error; err != nil {
return key, err
}
return key, nil
}
func RollApiKey(key models.ApiKey) (models.ApiKey, error) {
var ticket models.AuthTicket
if err := database.C.Where("id = ?", key.TicketID).First(&ticket).Error; err != nil {
return key, err
}
ticket, err := RotateTicket(ticket, true)
if err != nil {
return key, err
} else {
key.Ticket = ticket
}
return key, nil
}

View File

@@ -0,0 +1,24 @@
package services
import (
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
)
func GetBotCount(user models.Account) (int64, error) {
var count int64
if err := database.C.Where("automated_id = ?", user.ID).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func NewBot(user models.Account, bot models.Account) (models.Account, error) {
bot.AutomatedBy = &user
bot.AutomatedID = &user.ID
if err := database.C.Save(&bot).Error; err != nil {
return bot, err
}
return bot, nil
}

View File

@@ -0,0 +1,139 @@
package services
import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"time"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"git.solsynth.dev/hypernet/wallet/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"gorm.io/gorm"
)
func CheckCanCheckIn(user models.Account) error {
var record models.CheckInRecord
if err := database.C.Where("account_id = ? AND created_at::date = CURRENT_DATE", user.ID).First(&record).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("unable get check in record: %v", err)
}
return fmt.Errorf("today's check in record exists")
}
func GetCheckInStreak(user models.Account) (int64, error) {
var streaks int64
if err := database.C.Raw(`WITH dates AS (
SELECT DISTINCT created_at::DATE AS created_date
FROM check_in_records
WHERE created_at::DATE <= CURRENT_DATE
AND account_id = ?
),
streak AS (
SELECT created_date,
created_date - INTERVAL '1 day' * (ROW_NUMBER() OVER (ORDER BY created_date)) AS grp
FROM dates
),
grouped_streaks AS (
SELECT grp, COUNT(*) AS streak_length, MAX(created_date) AS last_date
FROM streak
GROUP BY grp
),
last_streak AS (
SELECT streak_length
FROM grouped_streaks
WHERE last_date = (SELECT MAX(created_date) FROM dates)
)
SELECT COALESCE(streak_length, 0) FROM last_streak;`, user.ID).Scan(&streaks).Error; err != nil {
return streaks, err
}
return streaks, nil
}
func GetTodayCheckIn(user models.Account) (models.CheckInRecord, error) {
var record models.CheckInRecord
if err := database.C.Where("account_id = ? AND created_at::date = CURRENT_DATE", user.ID).First(&record).Error; err != nil {
return record, fmt.Errorf("unable get check in record: %v", err)
}
return record, nil
}
const CheckInResultModifiersLength = 4
func CheckIn(user models.Account) (models.CheckInRecord, error) {
var record models.CheckInRecord
if err := CheckCanCheckIn(user); err != nil {
return record, fmt.Errorf("today already signed")
}
tier := rand.Intn(5)
streak, _ := GetCheckInStreak(user)
expMin := 100
exp := expMin + int(math.Max(float64(streak)*5, 10*5))
coinMax := 10.0 * float64(tier+1)
coinMin := 10.0
rawCoins := coinMax + rand.Float64()*(coinMax-coinMin) + math.Max(float64(streak)*0.5, float64(100*0.5))
record = models.CheckInRecord{
ResultTier: tier,
ResultExperience: exp,
ResultCoin: float64(int(rawCoins*100)) / 100,
CurrentStreak: int(streak),
AccountID: user.ID,
}
modifiers := make([]int, CheckInResultModifiersLength)
for i := 0; i < CheckInResultModifiersLength; i++ {
modifiers[i] = rand.Intn(1025) // from 0 to 1024 as the comment said
}
record.ResultModifiers = modifiers
tx := database.C.Begin()
var profile models.AccountProfile
if err := database.C.Where("account_id = ?", user.ID).First(&profile).Error; err != nil {
return record, fmt.Errorf("unable get account profile: %v", err)
} else {
profile.Experience += uint64(record.ResultExperience)
if err := tx.Save(&profile).Error; err != nil {
tx.Rollback()
return record, fmt.Errorf("unable update account profile: %v", err)
}
}
conn, err := gap.Nx.GetClientGrpcConn("wa")
if err != nil {
log.Warn().Err(err).Msg("Unable to connect with wallet to send daily rewards")
record.ResultCoin = 0
}
wc := proto.NewPaymentServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err = wc.MakeTransactionWithAccount(ctx, &proto.MakeTransactionWithAccountRequest{
PayeeAccountId: lo.ToPtr(uint64(user.ID)),
Amount: record.ResultCoin,
Currency: "normal",
Remark: "Daily Check-In Rewards",
})
if err != nil {
log.Warn().Err(err).Msg("Unable to make transaction with account to send daily rewards")
record.ResultCoin = 0
}
if err := tx.Save(&record).Error; err != nil {
return record, fmt.Errorf("unable do check in: %v", err)
}
tx.Commit()
return record, nil
}

View File

@@ -1,7 +1,10 @@
package services
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"time"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/rs/zerolog/log"
)
@@ -9,13 +12,11 @@ func DoAutoDatabaseCleanup() {
log.Debug().Msg("Now cleaning up entire database...")
var count int64
for _, model := range database.AutoMaintainRange {
tx := database.C.Unscoped().Delete(model, "deleted_at IS NOT NULL")
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running cleaning up entire database...")
}
count += tx.RowsAffected
}
deadline := time.Now().Add(-30 * 24 * time.Hour)
seenDeadline := time.Now().Add(-7 * 24 * time.Hour)
tx := database.C.Unscoped().Where("created_at <= ? OR read_at <= ?", deadline, seenDeadline).Delete(&models.Notification{})
count += tx.RowsAffected
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
}

View File

@@ -2,9 +2,9 @@ package services
import (
"fmt"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
)
func GetThirdClient(id string) (models.ThirdClient, error) {
@@ -18,6 +18,18 @@ func GetThirdClient(id string) (models.ThirdClient, error) {
return client, nil
}
func GetThirdClientWithUser(id string, userId uint) (models.ThirdClient, error) {
var client models.ThirdClient
if err := database.C.Where(&models.ThirdClient{
Alias: id,
AccountID: &userId,
}).First(&client).Error; err != nil {
return client, err
}
return client, nil
}
func GetThirdClientWithSecret(id, secret string) (models.ThirdClient, error) {
client, err := GetThirdClient(id)
if err != nil {

View File

@@ -1,33 +1,73 @@
package services
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"net"
"strings"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
)
var writeEventQueue []models.ActionEvent
var writeAuditQueue []models.AuditRecord
var (
writeEventQueue []models.ActionEvent
writeAuditQueue []models.AuditRecord
)
// AddEvent to keep operation logs by user themselves clear to query
func AddEvent(user models.Account, event, target, ip, ua string) {
func AddEvent(user uint, event string, meta map[string]any, ip, ua string) {
var location *string
var coordinateX, coordinateY *float64
netIp := net.ParseIP(ip)
record, err := database.Gc.City(netIp)
if err == nil {
var locationNames []string
locationNames = append(locationNames, record.City.Names["en"])
for _, subs := range record.Subdivisions {
locationNames = append(locationNames, subs.Names["en"])
}
location = lo.ToPtr(strings.Join(locationNames, ", "))
coordinateX = &record.Location.Latitude
coordinateY = &record.Location.Longitude
}
writeEventQueue = append(writeEventQueue, models.ActionEvent{
Type: event,
Target: target,
IpAddress: ip,
UserAgent: ua,
AccountID: user.ID,
Type: event,
Metadata: meta,
IpAddress: ip,
UserAgent: ua,
Location: location,
CoordinateX: coordinateX,
CoordinateY: coordinateY,
AccountID: user,
})
}
// AddAuditRecord to keep logs to make administrators' operations clear to query
func AddAuditRecord(operator models.Account, act, ip, ua string, metadata map[string]any) {
var location *string
var coordinateX, coordinateY *float64
netIp := net.ParseIP(ip)
record, err := database.Gc.City(netIp)
if err == nil {
var locationNames []string
locationNames = append(locationNames, record.City.Names["en"])
for _, subs := range record.Subdivisions {
locationNames = append(locationNames, subs.Names["en"])
}
location = lo.ToPtr(strings.Join(locationNames, ", "))
coordinateX = &record.Location.Latitude
coordinateY = &record.Location.Longitude
}
writeAuditQueue = append(writeAuditQueue, models.AuditRecord{
Action: act,
Metadata: metadata,
IpAddress: ip,
UserAgent: ua,
AccountID: operator.ID,
Action: act,
Metadata: metadata,
IpAddress: ip,
UserAgent: ua,
Location: location,
CoordinateX: coordinateX,
CoordinateY: coordinateY,
AccountID: operator.ID,
})
}

View File

@@ -1,25 +0,0 @@
package services
import (
"github.com/sideshow/apns2"
"github.com/sideshow/apns2/token"
"github.com/spf13/viper"
)
// ExtAPNS is Apple Notification Services client
var ExtAPNS *apns2.Client
func SetupAPNS() error {
authKey, err := token.AuthKeyFromFile(viper.GetString("apns_credentials"))
if err != nil {
return err
}
ExtAPNS = apns2.NewTokenClient(&token.Token{
AuthKey: authKey,
KeyID: viper.GetString("apns_credentials_key"),
TeamID: viper.GetString("apns_credentials_team"),
}).Production()
return nil
}

View File

@@ -1,23 +0,0 @@
package services
import (
"context"
firebase "firebase.google.com/go"
"github.com/spf13/viper"
"google.golang.org/api/option"
)
// ExtFire is the firebase app client
var ExtFire *firebase.App
func SetupFirebase() error {
opt := option.WithCredentialsFile(viper.GetString("firebase_credentials"))
app, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
return err
} else {
ExtFire = app
}
return nil
}

View File

@@ -2,31 +2,23 @@ package services
import (
"fmt"
"strings"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
"github.com/google/uuid"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"strings"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"github.com/google/uuid"
"github.com/spf13/viper"
)
const EmailPasswordTemplate = `Dear %s,
We hope this message finds you well.
As part of our ongoing commitment to ensuring the security of your account, we require you to complete the login process by entering the verification code below:
Your Login Verification Code: %s
Please use the provided code within the next 2 hours to complete your login.
If you did not request this code, please update your information, maybe your username or email has been leak.
Thank you for your cooperation in helping us maintain the security of your account.
Best regards,
%s`
func GetPasswordTypeFactor(userId uint) (models.AuthFactor, error) {
var factor models.AuthFactor
err := database.C.Where(models.AuthFactor{
@@ -64,8 +56,39 @@ func CountUserFactor(userId uint) int64 {
return count
}
func GetFactorCode(factor models.AuthFactor) (bool, error) {
func GetFactorCode(factor models.AuthFactor, ip string) (bool, error) {
switch factor.Type {
case models.InAppNotifyFactor:
var user models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: factor.AccountID},
}).First(&user).Error; err != nil {
return true, err
}
secret := uuid.NewString()[:6]
identifier := fmt.Sprintf("%s#%d", gap.FactorOtpPrefix, factor.ID)
err := cachekit.Set(gap.Ca, identifier, secret, time.Minute*30, fmt.Sprintf("user#%d", factor.AccountID))
if err != nil {
return true, fmt.Errorf("error during creating otp: %v", err)
} else {
log.Info().Uint("factor", factor.ID).Str("secret", secret).Msg("Created one-time-password in cache...")
}
err = NewNotification(models.Notification{
Topic: "passport.security.otp",
Title: localize.L.GetLocalizedString("subjectLoginOneTimePassword", user.Language),
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyLoginOneTimePassword", user.Language), secret),
Account: user,
AccountID: user.ID,
Metadata: map[string]any{"secret": secret},
})
if err != nil {
log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via notify...")
return true, nil
}
return true, nil
case models.EmailPasswordFactor:
var user models.Account
if err := database.C.Where(&models.Account{
@@ -74,19 +97,37 @@ func GetFactorCode(factor models.AuthFactor) (bool, error) {
return true, err
}
factor.Secret = uuid.NewString()[:6]
if err := database.C.Save(&factor).Error; err != nil {
return true, err
secret := uuid.NewString()[:6]
identifier := fmt.Sprintf("%s#%d", gap.FactorOtpPrefix, factor.ID)
err := cachekit.Set(gap.Ca, identifier, secret, time.Minute*30, fmt.Sprintf("user#%d", factor.AccountID))
if err != nil {
return true, fmt.Errorf("error during creating otp: %v", err)
} else {
log.Info().Uint("factor", factor.ID).Str("secret", secret).Msg("Created one-time-password in cache...")
}
subject := fmt.Sprintf("[%s] Login verification code", viper.GetString("name"))
content := fmt.Sprintf(EmailPasswordTemplate, user.Name, factor.Secret, viper.GetString("maintainer"))
if err := SendMail(user.GetPrimaryEmail().Content, subject, content); err != nil {
subject := fmt.Sprintf("[%s] %s", viper.GetString("name"), localize.L.GetLocalizedString("subjectLoginOneTimePassword", user.Language))
content := localize.L.RenderLocalizedTemplateHTML("email-otp.tmpl", user.Language, map[string]any{
"Code": secret,
"User": user,
"IP": ip,
"Date": time.Now().Format(time.DateTime),
})
err = gap.Px.PushEmail(pushkit.EmailDeliverRequest{
To: user.GetPrimaryEmail().Content,
Email: pushkit.EmailData{
Subject: subject,
HTML: &content,
},
})
if err != nil {
log.Warn().Err(err).Uint("factor", factor.ID).Msg("Failed to delivery one-time-password via mail...")
return true, nil
}
return true, nil
default:
return false, nil
}
@@ -100,12 +141,29 @@ func CheckFactor(factor models.AuthFactor, code string) error {
nil,
fmt.Errorf("invalid password"),
)
case models.EmailPasswordFactor:
case models.TimeOtpFactor:
return lo.Ternary(
strings.ToUpper(code) == strings.ToUpper(factor.Secret),
totp.Validate(code, factor.Secret),
nil,
fmt.Errorf("invalid verification code"),
)
case models.InAppNotifyFactor:
case models.EmailPasswordFactor:
identifier := fmt.Sprintf("%s#%d", gap.FactorOtpPrefix, factor.ID)
val, err := cachekit.Get[string](gap.Ca, identifier)
if err != nil {
log.Error().Err(err).Msg("Error fetching message when validating factor code...")
return fmt.Errorf("one-time-password not found or expired")
}
if !strings.EqualFold(code, val) {
return fmt.Errorf("invalid verification code")
}
log.Info().Uint("factor", factor.ID).Str("secret", code).Msg("Verified one-time-password...")
if err := cachekit.Delete(gap.Ca, identifier); err != nil {
log.Error().Err(err).Msg("Error deleting the otp from cache...")
}
return nil
}
return nil

View File

@@ -1,18 +1,32 @@
package services
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
)
var EReader *sec.JwtReader
var EWriter *sec.JwtWriter
type PayloadClaims struct {
jwt.RegisteredClaims
// Internal Stuff
SessionID string `json:"sed"`
Type string `json:"typ"`
// ID Token Stuff
Name string `json:"name,omitempty"`
Nick string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
// Additional Stuff
AuthorizedParties string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"`
Type string `json:"typ"`
}
const (
@@ -20,39 +34,40 @@ const (
JwtRefreshType = "refresh"
)
func EncodeJwt(id string, typ, sub, sed string, aud []string, exp time.Time) (string, error) {
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
jwt.RegisteredClaims{
func EncodeJwt(id string, typ, sub, sed string, nonce *string, aud []string, exp time.Time, idTokenUser ...models.Account) (string, error) {
var azp string
for _, item := range aud {
if item != InternalTokenAudience {
azp = item
break
}
}
claims := PayloadClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: sub,
Audience: aud,
Issuer: fmt.Sprintf("https://%s", viper.GetString("domain")),
Issuer: viper.GetString("security.issuer"),
ExpiresAt: jwt.NewNumericDate(exp),
NotBefore: jwt.NewNumericDate(time.Now()),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: id,
},
sed,
typ,
})
return tk.SignedString([]byte(viper.GetString("secret")))
}
func DecodeJwt(str string) (PayloadClaims, error) {
var claims PayloadClaims
tk, err := jwt.ParseWithClaims(str, &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(viper.GetString("secret")), nil
})
if err != nil {
return claims, err
AuthorizedParties: azp,
SessionID: sed,
Type: typ,
}
if data, ok := tk.Claims.(*PayloadClaims); ok {
return *data, nil
} else {
return claims, fmt.Errorf("unexpected token payload: not payload claims type")
if len(idTokenUser) > 0 {
user := idTokenUser[0]
claims.Name = user.Name
claims.Nick = user.Nick
claims.Email = user.GetPrimaryEmail().Content
}
if nonce != nil {
claims.Nonce = *nonce
}
return sec.WriteJwt(EWriter, claims)
}

View File

@@ -1,51 +0,0 @@
package services
import (
"crypto/tls"
"fmt"
"net/smtp"
"net/textproto"
"github.com/jordan-wright/email"
"github.com/spf13/viper"
)
func SendMail(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
Text: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}
func SendMailHTML(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
HTML: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}

View File

@@ -1,18 +0,0 @@
package services
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
func GetFactorName(w models.AuthFactorType, localizer *i18n.Localizer) string {
unknown, _ := localizer.LocalizeMessage(&i18n.Message{ID: "unknown"})
mfaEmail, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaFactorEmail"})
switch w {
case models.EmailPasswordFactor:
return mfaEmail
default:
return unknown
}
}

View File

@@ -3,18 +3,20 @@ package services
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/gap"
"reflect"
"time"
"firebase.google.com/go/messaging"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
"github.com/rs/zerolog/log"
"github.com/sideshow/apns2"
payload2 "github.com/sideshow/apns2/payload"
"github.com/spf13/viper"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
)
func AddNotifySubscriber(user models.Account, provider, id, tk, ua string) (models.NotificationSubscriber, error) {
@@ -23,7 +25,10 @@ func AddNotifySubscriber(user models.Account, provider, id, tk, ua string) (mode
if err := database.C.Where(&models.NotificationSubscriber{
DeviceID: id,
AccountID: user.ID,
}); err != nil {
}).Or(&models.NotificationSubscriber{
DeviceToken: tk,
AccountID: user.ID,
}).First(&prev).Error; err != nil {
subscriber = models.NotificationSubscriber{
UserAgent: ua,
Provider: provider,
@@ -32,13 +37,12 @@ func AddNotifySubscriber(user models.Account, provider, id, tk, ua string) (mode
AccountID: user.ID,
}
} else {
prev = subscriber
subscriber = prev
subscriber.UserAgent = ua
subscriber.Provider = provider
subscriber.DeviceToken = tk
}
subscriber.UserAgent = ua
subscriber.Provider = provider
subscriber.DeviceToken = tk
var err error
if !reflect.DeepEqual(subscriber, prev) {
err = database.C.Save(&subscriber).Error
@@ -48,29 +52,59 @@ func AddNotifySubscriber(user models.Account, provider, id, tk, ua string) (mode
}
// NewNotification will create a notification and push via the push method it
// Pleases provide the notification with the account field is not empty
func NewNotification(notification models.Notification) error {
if ok := CheckNotificationNotifiable(notification.Account, notification.Topic); !ok {
log.Info().Str("topic", notification.Topic).Uint("uid", notification.AccountID).Msg("Notification dismissed by user...")
return nil
}
if err := database.C.Save(&notification).Error; err != nil {
return err
}
if err := PushNotification(notification); err != nil {
if err := PushNotification(notification, true); err != nil {
return err
}
return nil
}
// PushNotification will push the notification whatever it exists record in the
// database Recommend pushing another goroutine when you need to push a lot of
// notifications And just use a block statement when you just push one
// notification.
// The time of creating a new subprocess is much more than push notification.
func PushNotification(notification models.Notification) error {
func NewNotificationBatch(notifications []models.Notification) error {
if len(notifications) == 0 {
return nil
}
notifiable := CheckNotificationNotifiableBatch(lo.Map(notifications, func(item models.Notification, index int) models.Account {
return item.Account
}), notifications[0].Topic)
notifications = lo.Filter(notifications, func(item models.Notification, index int) bool {
return notifiable[index]
})
if err := database.C.CreateInBatches(notifications, 1000).Error; err != nil {
return err
}
PushNotificationBatch(notifications, true)
return nil
}
// PushNotification will push a notification to the user, via websocket, firebase, or APNs
// Please provide the notification with the account field is not empty
func PushNotification(notification models.Notification, skipNotifiableCheck ...bool) error {
if len(skipNotifiableCheck) == 0 || !skipNotifiableCheck[0] {
if ok := CheckNotificationNotifiable(notification.Account, notification.Topic); !ok {
log.Info().Str("topic", notification.Topic).Uint("uid", notification.AccountID).Msg("Notification dismissed by user...")
return nil
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := proto.NewStreamControllerClient(gap.H.GetDealerGrpcConn()).PushStream(ctx, &proto.PushStreamRequest{
UserId: uint64(notification.AccountID),
Body: models.UnifiedCommand{
_, err := proto.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn()).PushStream(ctx, &proto.PushStreamRequest{
UserId: lo.ToPtr(uint64(notification.AccountID)),
Body: nex.WebSocketPackage{
Action: "notifications.new",
Payload: notification,
}.Marshal(),
@@ -91,65 +125,111 @@ func PushNotification(notification models.Notification) error {
return err
}
var providers []string
var tokens []string
for _, subscriber := range subscribers {
switch subscriber.Provider {
case models.NotifySubscriberFirebase:
if ExtFire != nil {
ctx := context.Background()
client, err := ExtFire.Messaging(ctx)
if err != nil {
log.Warn().Err(err).Msg("An error occurred when creating FCM client...")
break
}
providers = append(providers, subscriber.Provider)
tokens = append(tokens, subscriber.DeviceToken)
}
message := &messaging.Message{
Notification: &messaging.Notification{
Title: notification.Title,
Body: notification.Body,
},
Token: subscriber.DeviceToken,
}
log.Debug().Str("topic", notification.Topic).Any("uid", notification.AccountID).Msg("Pushing notify to user...")
if response, err := client.Send(ctx, message); err != nil {
log.Warn().Err(err).Msg("An error occurred when notify subscriber via FCM...")
} else {
log.Debug().
Str("response", response).
Int("subscriber", int(subscriber.ID)).
Msg("Notified subscriber via FCM.")
}
}
case models.NotifySubscriberAPNs:
if ExtAPNS != nil {
data, err := payload2.
NewPayload().
AlertTitle(notification.Title).
AlertBody(notification.Body).
Sound("default").
Category(notification.Topic).
MarshalJSON()
if err != nil {
log.Warn().Err(err).Msg("An error occurred when preparing to notify subscriber via APNs...")
}
payload := &apns2.Notification{
ApnsID: subscriber.DeviceID,
DeviceToken: subscriber.DeviceToken,
Topic: viper.GetString("apns_topic"),
Payload: data,
}
err = gap.Px.PushNotifyBatch(pushkit.NotificationPushBatchRequest{
Lang: lo.Map(subscribers, func(item models.NotificationSubscriber, index int) string {
return notification.Account.Language
}),
Providers: providers,
Tokens: tokens,
Notification: notification.EncodeToPushkit(),
})
if err != nil {
log.Warn().Err(err).Str("topic", notification.Topic).Msg("Failed to push notification to Pusher")
}
if resp, err := ExtAPNS.Push(payload); err != nil {
log.Warn().Err(err).Msg("An error occurred when notify subscriber via APNs...")
} else {
log.Debug().
Str("reason", resp.Reason).
Int("status", resp.StatusCode).
Int("subscriber", int(subscriber.ID)).
Msg("Notified subscriber via APNs.")
}
}
return err
}
// PushNotificationBatch will push a notification to the user
// The notification should be the same for all users except the account id field
// For the notification push, the method will only use the first notification as template
func PushNotificationBatch(notifications []models.Notification, skipNotifiableCheck ...bool) {
if len(notifications) == 0 {
return
}
var accountIdx []uint
if len(skipNotifiableCheck) == 0 || !skipNotifiableCheck[0] {
notifiable := CheckNotificationNotifiableBatch(lo.Map(notifications, func(item models.Notification, index int) models.Account {
return item.Account
}), notifications[0].Topic)
accountIdx = lo.Map(
lo.Filter(notifications, func(item models.Notification, index int) bool {
return notifiable[index]
}),
func(item models.Notification, index int) uint {
return item.AccountID
},
)
} else {
accountIdx = lo.Map(
notifications,
func(item models.Notification, index int) uint {
return item.AccountID
},
)
}
log.Debug().Str("topic", notifications[0].Topic).Any("uid", accountIdx).Msg("Pushing notify to users...")
if len(accountIdx) == 0 {
return
}
var subscribers []models.NotificationSubscriber
if err := database.C.Where("account_id IN ?", accountIdx).Find(&subscribers).Error; err != nil {
log.Error().Err(err).Msg("Failed to fetch subscribers, unable to push notifications")
}
var providers []string
var tokens []string
stream := proto.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn())
for _, notification := range notifications {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, _ = stream.PushStream(ctx, &proto.PushStreamRequest{
UserId: lo.ToPtr(uint64(notification.AccountID)),
Body: nex.WebSocketPackage{
Action: "notifications.new",
Payload: notification,
}.Marshal(),
})
cancel()
// Skip push notification
if GetStatusDisturbable(notification.AccountID) != nil {
continue
}
for _, subscriber := range lo.Filter(subscribers, func(item models.NotificationSubscriber, index int) bool {
return item.AccountID == notification.AccountID
}) {
providers = append(providers, subscriber.Provider)
tokens = append(tokens, subscriber.DeviceToken)
}
}
return nil
if err := gap.Px.PushNotifyBatch(pushkit.NotificationPushBatchRequest{
Lang: lo.Map(subscribers, func(item models.NotificationSubscriber, index int) string {
for idx := 0; idx < len(notifications); idx++ {
if item.AccountID == notifications[idx].AccountID {
return notifications[idx].Account.Language
}
}
return "en-US"
}),
Providers: providers,
Tokens: tokens,
Notification: notifications[0].EncodeToPushkit(),
}); err != nil {
log.Warn().Err(err).Str("topic", notifications[0].Topic).Msg("Failed to push notification to Pusher")
}
}

Some files were not shown because too many files have changed in this diff Show More