Compare commits
512 Commits
e5641b8068
...
master
Author | SHA1 | Date | |
---|---|---|---|
ef20829b2e | |||
e716f5700c | |||
b327e08062 | |||
6ada023cea | |||
7389f70128 | |||
cc7c96311f | |||
d3d7c4b573 | |||
3d58ff2391 | |||
9cda542b01 | |||
e1d7b4e20b | |||
a4c6e9a905 | |||
c8e8104d5d | |||
de28ae027f | |||
a5033c0fb0 | |||
742edaa9e2 | |||
9712119238 | |||
f8a4924d56 | |||
fa3561ae58 | |||
548bd6984b | |||
27e1196130 | |||
173ef92c01 | |||
23e5ba432b | |||
2ead62ecce | |||
b18dd5ef82 | |||
c4758c6217 | |||
f9aa4e3c10 | |||
34c2d0d5e7 | |||
82fa877cad | |||
73683d66e8 | |||
ecaace5616 | |||
db018b35f9 | |||
45dd50ccba | |||
bcb2cd2f9c | |||
32e91e2601 | |||
35e5eadb05 | |||
651a738646 | |||
e43b9c9341 | |||
dfc85c5913 | |||
879cd7991b | |||
84c1a8f836 | |||
0f9bde2533 | |||
1ed9816b12 | |||
8ca5573b86 | |||
8c600daf86 | |||
bdef101bc0 | |||
d6c364ceb3 | |||
0c0164c2e8 | |||
53d856c7a7 | |||
bc31a94c98 | |||
0fac34edfb | |||
5979fd5b2c | |||
4616f7cc93 | |||
378c60cef8 | |||
af2bc29068 | |||
07d9335180 | |||
e737d31fd9 | |||
701d9b1929 | |||
e2e0cdc9b0 | |||
e65713a5b2 | |||
816ee07947 | |||
6db14a71af | |||
a649f468fc | |||
2f541f2346 | |||
bac655c1cf | |||
a2d033b56c | |||
76cc34c90a | |||
160fc79a07 | |||
59e1bf1ddf | |||
badae3aa0d | |||
11ca7c0ba6 | |||
09dddf7721 | |||
4fa155cafb | |||
1bb73c996b | |||
76682ab27f | |||
64fd2f3cd9 | |||
497c0692ca | |||
a08372d4d2 | |||
271e61510e | |||
4bf28e5298 | |||
7ff93f1344 | |||
a40ab9efdb | |||
bf7c10d195 | |||
17a99cce61 | |||
7dbb552dd2 | |||
7d1165b87b | |||
eaa8fb5225 | |||
ec0048042a | |||
62a3168190 | |||
79844c414b | |||
cd01657857 | |||
b68aed0284 | |||
d7ee87433f | |||
820d96f6b0 | |||
509390b38a | |||
78d624697c | |||
5b1c0784ec | |||
e115374589 | |||
09010d5867 | |||
867a556204 | |||
71e959805f | |||
fabde24734 | |||
123342b13b | |||
dd9a44d126 | |||
dc2de65245 | |||
a43e89a5a3 | |||
0db2967214 | |||
0dbc30d53c | |||
e4ba72725e | |||
144219f0aa | |||
72d0cbb7be | |||
1f75a9e64b | |||
2dac1759d9 | |||
5b516094a0 | |||
6a4f2b6869 | |||
5bdc59b4af | |||
5273498af1 | |||
fcd3b56f89 | |||
61976f6c47 | |||
9c0abdb46f | |||
f33497d002 | |||
94e55a6d4e | |||
afc2ec80e9 | |||
573ccc0478 | |||
fdf2d28f51 | |||
0115e6723a | |||
02f122328a | |||
77c543f88e | |||
50498976af | |||
bede9716f1 | |||
3074cd93d8 | |||
87c3b81ad4 | |||
20ff541ab4 | |||
e0c9646c98 | |||
d3a1382711 | |||
f8492ea1af | |||
10e9813bea | |||
192a03e9d1 | |||
08c639497d | |||
312cf820ab | |||
55b3a6f164 | |||
659548a544 | |||
c16019341f | |||
1515e29d5b | |||
668d9805e2 | |||
7081cb8885 | |||
bc2e7df574 | |||
bc5a0ea966 | |||
f89a2244ea | |||
bf733da247 | |||
ca9bd7ac14 | |||
3f4fe68105 | |||
7a2ce20460 | |||
720f9690f6 | |||
663b563b39 | |||
8e1ed67c48 | |||
4695a617d7 | |||
f91be512f3 | |||
40b64a3559 | |||
808e7998dd | |||
40a0403521 | |||
b4da8ce78b | |||
d1f1183beb | |||
c3619f6d25 | |||
2b4f71e732 | |||
39ac016b46 | |||
8326c716e3 | |||
8ff2648e4c | |||
e6f6389eb0 | |||
2fcc784bc4 | |||
69c6ac6581 | |||
2d322f070a | |||
d7b1913bc3 | |||
8fbb79607b | |||
14baee03fe | |||
7a0756a5e8 | |||
4c44af74b5 | |||
b4fb7b53af | |||
e412d5e742 | |||
e83e92b054 | |||
10d977ab23 | |||
29629cead6 | |||
df8058e794 | |||
be32ad1522 | |||
8e3ce09734 | |||
974c5d9700 | |||
a4a139c56e | |||
06bd632f37 | |||
68b4390edf | |||
6ea4850459 | |||
bee9799415 | |||
821e0c3e60 | |||
39c3799d82 | |||
9287e6c5cc | |||
0f18c6ff16 | |||
94c15f58ec | |||
cb3cc3f540 | |||
cec234bc53 | |||
27b625e0c8 | |||
bbceb65dbf | |||
648f10b25a | |||
0a362dadec | |||
9174767d80 | |||
53c6b2a636 | |||
10e4a67835 | |||
ba32925b58 | |||
3031f61ea4 | |||
02bffc062f | |||
817c60c4e0 | |||
5bcce9d44e | |||
7a9e31fd86 | |||
4b4eb3e581 | |||
5203234c4a | |||
f287e295e5 | |||
df9fb0a92a | |||
ea3d6c20d3 | |||
313f6b8d4f | |||
582e6cdb61 | |||
d4ed047ed7 | |||
3ce5ef6617 | |||
a23c51f483 | |||
bfddfa2201 | |||
bee5b676fa | |||
99f8e4c891 | |||
f240226563 | |||
d205a41614 | |||
51a53a25da | |||
1bdeba58bc | |||
46c7b6dff1 | |||
b097dab9e1 | |||
52c06d9826 | |||
4b0910f6eb | |||
2b2e7b5a89 | |||
476ef57236 | |||
0f2b45352c | |||
a6b0170b1d | |||
876cfa9956 | |||
7a585ec5b9 | |||
b22657d09f | |||
8f61253bd3 | |||
516f5593de | |||
5e76fa07b7 | |||
31fcfa2bf5 | |||
9d075f862c | |||
0285b82ca2 | |||
dd3586b241 | |||
bdcc8de59b | |||
0ee3f1bc97 | |||
a97615b6c7 | |||
fe5e9c66b9 | |||
e4d73b1d31 | |||
142e7c3434 | |||
7a072988ce | |||
817c573048 | |||
c51af61820 | |||
688d026d75 | |||
ed13d62110 | |||
d2beaa67b4 | |||
e97f3b6047 | |||
57b4b314fe | |||
7c09138ef7 | |||
6ef46d984d | |||
94aed49092 | |||
ebbfd7450c | |||
c458123d9e | |||
7737d9fd08 | |||
f113719e79 | |||
2d181bfdaf | |||
24395b698e | |||
cb0fefecfb | |||
3cab122b76 | |||
e56c48362d | |||
d08b7a273d | |||
6350ec1e43 | |||
27d501d7a7 | |||
6643d31b1a | |||
b3bb7cb582 | |||
8fcaf99103 | |||
282a0891d0 | |||
7436d4b2cc | |||
7b9aebd5d7 | |||
cef0a552f4 | |||
c912271610 | |||
1ce04f7067 | |||
14bcea02d0 | |||
4795da84f9 | |||
a8d919dc5b | |||
4143a7b2c8 | |||
69fb9531cb | |||
a60be78ce6 | |||
0e7e7e6d55 | |||
d405b34747 | |||
e049b2c566 | |||
681eb3cf7f | |||
e5bdb23c09 | |||
77c1a029bd | |||
f7063fc109 | |||
3216547d46 | |||
8c7f255473 | |||
13c26486b6 | |||
46468fae5f | |||
da15c72fb3 | |||
182a389180 | |||
74819c1c2b | |||
8c89d89382 | |||
5200e00c9b | |||
a4ccf12b7a | |||
e5d8f1ab3b | |||
7e334222ee | |||
133bbcc6bc | |||
659fc8375c | |||
78604db54e | |||
0276a9b0bf | |||
9f7205ba14 | |||
17e0fa3b18 | |||
7fed35dd04 | |||
b2b8aa467b | |||
3f0d3fc69e | |||
0bce0d45c8 | |||
203465f0bb | |||
4281952c87 | |||
e36b40614d | |||
f2b8401746 | |||
feabff16ec | |||
9519497887 | |||
b1f6cf8f6e | |||
8c2649e29d | |||
7c0713a12f | |||
132d301aeb | |||
38ee30f455 | |||
ebdb6f5688 | |||
ab5130de2a | |||
7aef153bf3 | |||
f69f1026d8 | |||
7e4ca530fa | |||
d540e86603 | |||
200a1f9dfc | |||
1c1b287650 | |||
ea33857afb | |||
880ed9a999 | |||
8044a1e5f2 | |||
6667bff188 | |||
21d3d71936 | |||
0d02eca76e | |||
3f64747839 | |||
1cf675b23a | |||
86b2cd8140 | |||
7ddbea8bcb | |||
b919e100e0 | |||
18ae5f12e2 | |||
4537bc9f28 | |||
d654931483 | |||
a4ec986b77 | |||
7007cda8f2 | |||
c37a55b88b | |||
d9aa478d10 | |||
5a3c6fcada | |||
d2fdb5a72b | |||
9c3b42f078 | |||
80ee964afa | |||
69fdee72f5 | |||
45048ea814 | |||
0695338fa1 | |||
ef055e1144 | |||
6d035c1147 | |||
5b06b2dccb | |||
3010d4f2a4 | |||
bcab6dfc51 | |||
daaac3667c | |||
b76e7d41cf | |||
533745e0e4 | |||
b44786ae9a | |||
332557778d | |||
b925d54000 | |||
6a37ee4487 | |||
f1ab0f203f | |||
1c36b429ea | |||
61eff6ea49 | |||
c7ba566c48 | |||
727b259ca7 | |||
4fb4abda89 | |||
31139e943f | |||
a502f11ebc | |||
ee4f929d6c | |||
fd5bbd732f | |||
ebef35a619 | |||
1e2d5e9f9d | |||
73b57164ab | |||
8ae6292bf0 | |||
4e4fbb8ba9 | |||
7d3b804516 | |||
0e89e325d4 | |||
07dbdd80ec | |||
bcfe1d3927 | |||
3ba152252e | |||
18a4321685 | |||
fe27b0bf1c | |||
35f9580499 | |||
ec911b828e | |||
ce0bf2b548 | |||
7c418a3cd3 | |||
fddcb86c9e | |||
db1849933f | |||
854a50f63f | |||
5de68bb9b9 | |||
29a33331e4 | |||
6ec48aaa8a | |||
3e9c84a284 | |||
e0d496cc47 | |||
58a4a367cf | |||
ec6ae26aba | |||
eda6ad9776 | |||
7e5fdb684e | |||
2f940d257e | |||
f2d7c8f93c | |||
447ea936e4 | |||
5bde44647b | |||
c25a1f5c82 | |||
8e315642a4 | |||
6b26cad796 | |||
ee6e7324b2 | |||
e79441dbc5 | |||
87cccefddb | |||
0d78f34535 | |||
e5d53ab77b | |||
d1f54099c4 | |||
44d2ec9c4a | |||
7f5183f83b | |||
ef55cbb32a | |||
bc688f61d3 | |||
e7e86fa7c6 | |||
3a323d5fd5 | |||
e50a338378 | |||
8dbb8c522a | |||
af615cebd7 | |||
6850f64fe3 | |||
0b436c0a1e | |||
e8aac7bb66 | |||
7873bafa4f | |||
11377c378b | |||
f88fdd7fe5 | |||
e7c84a91a2 | |||
31bf242587 | |||
b69ac44885 | |||
211959167a | |||
19f57e84b5 | |||
6ac77f3695 | |||
96ee0dc3a7 | |||
1bd0807d65 | |||
fdc252abc3 | |||
5087f00eb2 | |||
b84dca981f | |||
2288c001df | |||
2e74dcabd0 | |||
caf75166a0 | |||
97995e464d | |||
5a94ab1971 | |||
d59818e857 | |||
f3473aeb83 | |||
37a68eac28 | |||
fa59f87d3c | |||
d28a79fdd9 | |||
6b32f47758 | |||
f0e24f634b | |||
5a966a42d7 | |||
678d7f3afe | |||
7d11640ddd | |||
95c486b8f4 | |||
920efd3021 | |||
a2635a49f6 | |||
5962dd2e6a | |||
16c9eeba5c | |||
d80e755b4a | |||
95328f42c2 | |||
0a9369aba5 | |||
e4ace4324a | |||
14efa09486 | |||
14e87d96ce | |||
518b2f2503 | |||
bb65b11566 | |||
c169ffdacd | |||
3393f751a0 | |||
3b0cbbb6c9 | |||
cd5d0fc840 | |||
3da3a6dcf0 | |||
9a1ddc623b | |||
7e1d8fc91a | |||
b18cfd6741 | |||
7bdafe246f | |||
8d2eae4c9e | |||
272fe6f2c2 | |||
e429627ecf | |||
00028cfce8 | |||
cc2aa8ef40 | |||
55a20e18bb | |||
3437d32273 | |||
9edc5d77a1 | |||
c415105599 | |||
0bf71fba9c | |||
48ba6ac98a | |||
42c3770231 | |||
37f10e708e | |||
b620b176e2 | |||
07c6125cf3 | |||
e8b5b05755 | |||
c3636c23ff | |||
3264c85b39 | |||
775a3b8868 | |||
cc2fa06c72 | |||
3b43205583 | |||
37ac26378f | |||
922fa45fa7 | |||
902650f11d |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,2 +1,7 @@
|
||||
/dist
|
||||
/uploads
|
||||
/uploads
|
||||
/keys
|
||||
|
||||
geoip.mmdb
|
||||
|
||||
.DS_Store
|
||||
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
.idea/Passport.iml
generated
9
.idea/Passport.iml
generated
@ -1,9 +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" />
|
||||
</component>
|
||||
</module>
|
57
.idea/codeStyles/Project.xml
generated
57
.idea/codeStyles/Project.xml
generated
@ -1,57 +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="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="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<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="Remove" />
|
||||
<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>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
12
.idea/dataSources.xml
generated
12
.idea/dataSources.xml
generated
@ -1,12 +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="49a1c31c-500d-4f9f-bbf4-b4ddc9f3dc56">
|
||||
<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>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@ -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
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
12
Dockerfile
12
Dockerfile
@ -1,21 +1,17 @@
|
||||
# Building Backend
|
||||
FROM golang:alpine as passport-server
|
||||
|
||||
RUN apk add nodejs npm
|
||||
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
WORKDIR /source/pkg/view
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
WORKDIR /source
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/main.go
|
||||
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 ./templates /templates
|
||||
COPY ./locales /locales
|
||||
|
||||
EXPOSE 8444
|
||||
|
||||
CMD ["/passport/server"]
|
||||
CMD ["/passport/server"]
|
||||
|
111
go.mod
111
go.mod
@ -1,69 +1,100 @@
|
||||
module code.smartsheep.studio/hydrogen/passport
|
||||
module git.solsynth.dev/hypernet/passport
|
||||
|
||||
go 1.21.5
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
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/json-iterator/go v1.1.12
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/spf13/viper v1.18.1
|
||||
golang.org/x/crypto v0.17.0
|
||||
gorm.io/datatypes v1.2.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
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.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.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 (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // 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/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/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/klauspost/compress v1.17.0 // 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/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/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // 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.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.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.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-20231219180239-dc181d75b848 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // 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 code.smartsheep.studio/hydrogen/bus => ../Bus
|
||||
replace git.solsynth.dev/hydrogen/bus => ../Bus
|
||||
|
297
go.sum
297
go.sum
@ -1,77 +1,129 @@
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
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.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/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/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/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/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-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/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
|
||||
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
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/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.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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/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/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/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/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/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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
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.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/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.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
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/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
||||
@ -83,104 +135,151 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
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/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/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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
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/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/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.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/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/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.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=
|
||||
github.com/spf13/viper v1.18.1/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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/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/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.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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
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=
|
||||
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
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/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.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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
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.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-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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.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.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.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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-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-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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@ -189,16 +288,16 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
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=
|
||||
|
661
license
Normal file
661
license
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
18
locales/en-US.json
Normal file
18
locales/en-US.json
Normal 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
18
locales/zh-CN.json
Normal 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
46
pkg/authkit/audit.go
Normal 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
26
pkg/authkit/auth.go
Normal 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))
|
||||
}
|
19
pkg/authkit/models/account_groups.go
Normal file
19
pkg/authkit/models/account_groups.go
Normal 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"`
|
||||
}
|
94
pkg/authkit/models/accounts.go
Normal file
94
pkg/authkit/models/accounts.go
Normal file
@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
BaseModel
|
||||
|
||||
Name string `json:"name" gorm:"uniqueIndex"`
|
||||
Nick string `json:"nick"`
|
||||
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"`
|
||||
Badges []Badge `json:"badges,omitempty"`
|
||||
|
||||
Tickets []AuthTicket `json:"tickets,omitempty"`
|
||||
Factors []AuthFactor `json:"factors,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/%s", viper.GetString("content_endpoint"), *v.Avatar))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Account) GetBanner() *string {
|
||||
if v.Banner != nil {
|
||||
return lo.ToPtr(fmt.Sprintf("%s/%s", viper.GetString("content_endpoint"), *v.Banner))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Account) GetPrimaryEmail() AccountContact {
|
||||
val, _ := lo.Find(v.Contacts, func(item AccountContact) bool {
|
||||
return item.Type == EmailAccountContact && item.IsPrimary
|
||||
})
|
||||
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 (
|
||||
EmailAccountContact = AccountContactType(iota)
|
||||
)
|
||||
|
||||
type AccountContact struct {
|
||||
BaseModel
|
||||
|
||||
Type int8 `json:"type"`
|
||||
Content string `json:"content" gorm:"uniqueIndex"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
16
pkg/authkit/models/audit.go
Normal file
16
pkg/authkit/models/audit.go
Normal 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"`
|
||||
}
|
85
pkg/authkit/models/auth.go
Normal file
85
pkg/authkit/models/auth.go
Normal 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"`
|
||||
}
|
12
pkg/authkit/models/badges.go
Normal file
12
pkg/authkit/models/badges.go
Normal file
@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
type Badge struct {
|
||||
BaseModel
|
||||
|
||||
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
13
pkg/authkit/models/bot.go
Normal 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"`
|
||||
}
|
21
pkg/authkit/models/check_in.go
Normal file
21
pkg/authkit/models/check_in.go
Normal 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"`
|
||||
}
|
@ -11,7 +11,6 @@ type ThirdClient struct {
|
||||
Secret string `json:"secret"`
|
||||
Urls datatypes.JSONSlice[string] `json:"urls"`
|
||||
Callbacks datatypes.JSONSlice[string] `json:"callbacks"`
|
||||
Sessions []AuthSession `json:"sessions" gorm:"foreignKey:ClientID"`
|
||||
IsDraft bool `json:"is_draft"`
|
||||
AccountID *uint `json:"account_id"`
|
||||
}
|
18
pkg/authkit/models/events.go
Normal file
18
pkg/authkit/models/events.go
Normal 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"`
|
||||
}
|
65
pkg/authkit/models/notifications.go
Normal file
65
pkg/authkit/models/notifications.go
Normal 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"`
|
||||
}
|
19
pkg/authkit/models/preferences.go
Normal file
19
pkg/authkit/models/preferences.go
Normal 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"`
|
||||
}
|
31
pkg/authkit/models/profiles.go
Normal file
31
pkg/authkit/models/profiles.go
Normal 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"`
|
||||
}
|
44
pkg/authkit/models/programs.go
Normal file
44
pkg/authkit/models/programs.go
Normal 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"`
|
||||
}
|
26
pkg/authkit/models/punishments.go
Normal file
26
pkg/authkit/models/punishments.go
Normal 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"`
|
||||
}
|
66
pkg/authkit/models/realms.go
Normal file
66
pkg/authkit/models/realms.go
Normal 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()),
|
||||
}
|
||||
}
|
23
pkg/authkit/models/relationships.go
Normal file
23
pkg/authkit/models/relationships.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
type RelationshipStatus = int8
|
||||
|
||||
const (
|
||||
RelationshipPending = RelationshipStatus(iota)
|
||||
RelationshipFriend
|
||||
RelationshipBlocked
|
||||
RelationshipWaiting
|
||||
)
|
||||
|
||||
type AccountRelationship struct {
|
||||
BaseModel
|
||||
|
||||
AccountID uint `json:"account_id"`
|
||||
RelatedID uint `json:"related_id"`
|
||||
Account Account `json:"account"`
|
||||
Related Account `json:"related"`
|
||||
Status RelationshipStatus `json:"status"`
|
||||
PermNodes datatypes.JSONMap `json:"perm_nodes"`
|
||||
}
|
19
pkg/authkit/models/reports.go
Normal file
19
pkg/authkit/models/reports.go
Normal 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"`
|
||||
}
|
23
pkg/authkit/models/statuses.go
Normal file
23
pkg/authkit/models/statuses.go
Normal file
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type StatusAttitude = uint8
|
||||
|
||||
const (
|
||||
AttitudeNeutral = StatusAttitude(iota)
|
||||
AttitudePositive
|
||||
AttitudeNegative
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
BaseModel
|
||||
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Attitude StatusAttitude `json:"attitude"`
|
||||
IsNoDisturb bool `json:"is_no_disturb"`
|
||||
IsInvisible bool `json:"is_invisible"`
|
||||
ClearAt *time.Time `json:"clear_at"`
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
22
pkg/authkit/models/tokens.go
Normal file
22
pkg/authkit/models/tokens.go
Normal 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
48
pkg/authkit/notify.go
Normal 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
27
pkg/authkit/parser.go
Normal 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
109
pkg/authkit/realm.go
Normal 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
23
pkg/authkit/relative.go
Normal 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
|
||||
}
|
65
pkg/authkit/third_client.go
Normal file
65
pkg/authkit/third_client.go
Normal 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
118
pkg/authkit/user.go
Normal 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
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/passport/pkg/server"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
passport "code.smartsheep.studio/hydrogen/passport/pkg"
|
||||
"code.smartsheep.studio/hydrogen/passport/pkg/database"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Configure settings
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("..")
|
||||
viper.SetConfigName("settings")
|
||||
viper.SetConfigType("toml")
|
||||
|
||||
// Load settings
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Panic().Err(err).Msg("An error occurred when loading settings.")
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
if err := database.NewSource(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connect to database.")
|
||||
} else if err := database.RunMigration(database.C); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when running database auto migration.")
|
||||
}
|
||||
|
||||
// Server
|
||||
server.NewServer()
|
||||
go server.Listen()
|
||||
|
||||
// Messages
|
||||
log.Info().Msgf("Passport v%s is started...", passport.AppVersion)
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info().Msgf("Passport v%s is quitting...", passport.AppVersion)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/passport/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RunMigration(source *gorm.DB) error {
|
||||
if err := source.AutoMigrate(
|
||||
&models.Account{},
|
||||
&models.AuthFactor{},
|
||||
&models.AccountProfile{},
|
||||
&models.AccountContact{},
|
||||
&models.AuthSession{},
|
||||
&models.AuthChallenge{},
|
||||
&models.MagicToken{},
|
||||
&models.ThirdClient{},
|
||||
&models.ActionEvent{},
|
||||
&models.Notification{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"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 NewSource() error {
|
||||
var err error
|
||||
|
||||
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{
|
||||
Colorful: true,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
LogLevel: lo.Ternary(viper.GetBool("debug"), logger.Info, logger.Silent),
|
||||
})})
|
||||
|
||||
return err
|
||||
}
|
44
pkg/internal/database/migrator.go
Normal file
44
pkg/internal/database/migrator.go
Normal file
@ -0,0 +1,44 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"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{},
|
||||
&models.Badge{},
|
||||
&models.Realm{},
|
||||
&models.RealmMember{},
|
||||
&models.AuthTicket{},
|
||||
&models.MagicToken{},
|
||||
&models.ThirdClient{},
|
||||
&models.ActionEvent{},
|
||||
&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 {
|
||||
if err := source.AutoMigrate(AutoMaintainRange...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
43
pkg/internal/database/source.go
Normal file
43
pkg/internal/database/source.go
Normal file
@ -0,0 +1,43 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var C *gorm.DB
|
||||
|
||||
func NewGorm() error {
|
||||
dsn, err := cruda.NewCrudaConn(gap.Nx).AllocDatabase("passport")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to alloc database from nexus: %v", err)
|
||||
}
|
||||
|
||||
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),
|
||||
})})
|
||||
|
||||
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
|
||||
}
|
77
pkg/internal/gap/server.go
Normal file
77
pkg/internal/gap/server.go
Normal file
@ -0,0 +1,77 @@
|
||||
package gap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 (
|
||||
Nx *nex.Conn
|
||||
Px *pushcon.Conn
|
||||
Rx *rx.MqConn
|
||||
Ca *cachekit.Conn
|
||||
)
|
||||
|
||||
const (
|
||||
FactorOtpPrefix = "auth-otp"
|
||||
)
|
||||
|
||||
func InitializeToNexus() error {
|
||||
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
|
||||
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
|
||||
|
||||
outboundIp, _ := nex.GetOutboundIP()
|
||||
|
||||
grpcOutbound := fmt.Sprintf("%s:%s", outboundIp, grpcBind[1])
|
||||
httpOutbound := fmt.Sprintf("%s:%s", outboundIp, httpBind[1])
|
||||
|
||||
var err error
|
||||
Nx, err = nex.NewNexusConn(viper.GetString("nexus_addr"), &proto.ServiceInfo{
|
||||
Id: viper.GetString("id"),
|
||||
Type: nex.ServiceTypeAuth,
|
||||
Label: "Passport",
|
||||
GrpcAddr: grpcOutbound,
|
||||
HttpAddr: lo.ToPtr("http://" + httpOutbound + "/api"),
|
||||
})
|
||||
if err == nil {
|
||||
go func() {
|
||||
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"))
|
||||
}
|
75
pkg/internal/grpc/auth.go
Normal file
75
pkg/internal/grpc/auth.go
Normal file
@ -0,0 +1,75 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 *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 := ticket.Account
|
||||
userinfo := &proto.UserInfo{
|
||||
Id: uint64(user.ID),
|
||||
Name: user.Name,
|
||||
PermNodes: nex.EncodeMap(perms),
|
||||
Metadata: nex.EncodeMap(user),
|
||||
}
|
||||
|
||||
return &proto.AuthReply{
|
||||
IsValid: true,
|
||||
Info: &proto.AuthInfo{
|
||||
SessionId: uint64(ticket.ID),
|
||||
Info: userinfo,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var heldPerms map[string]any
|
||||
rawHeldPerms, _ := jsoniter.Marshal(ctx.Account.PermNodes)
|
||||
_ = jsoniter.Unmarshal(rawHeldPerms, &heldPerms)
|
||||
|
||||
var value any
|
||||
_ = jsoniter.Unmarshal(in.GetValue(), &value)
|
||||
perms := services.FilterPermNodes(heldPerms, ctx.Claims)
|
||||
valid := services.HasPermNode(perms, in.GetKey(), value)
|
||||
|
||||
return &proto.CheckPermResponse{
|
||||
IsValid: valid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
defaultPerm := relation.Status == models.RelationshipFriend
|
||||
|
||||
var value any
|
||||
_ = jsoniter.Unmarshal(in.GetValue(), &value)
|
||||
valid := services.HasPermNodeWithDefault(relation.PermNodes, in.GetKey(), value, defaultPerm)
|
||||
|
||||
return &proto.CheckUserPermResponse{
|
||||
IsValid: valid,
|
||||
}, nil
|
||||
}
|
21
pkg/internal/grpc/events.go
Normal file
21
pkg/internal/grpc/events.go
Normal 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
|
||||
}
|
26
pkg/internal/grpc/health.go
Normal file
26
pkg/internal/grpc/health.go
Normal file
@ -0,0 +1,26 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
health "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (v *App) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
|
||||
return &health.HealthCheckResponse{
|
||||
Status: health.HealthCheckResponse_SERVING,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *App) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
|
||||
for {
|
||||
if server.Send(&health.HealthCheckResponse{
|
||||
Status: health.HealthCheckResponse_SERVING,
|
||||
}) != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
140
pkg/internal/grpc/notify.go
Normal file
140
pkg/internal/grpc/notify.go
Normal 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
|
||||
}
|
216
pkg/internal/grpc/realms.go
Normal file
216
pkg/internal/grpc/realms.go
Normal file
@ -0,0 +1,216 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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 *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, request.GetIncludePublic())
|
||||
if 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) 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)
|
||||
}
|
||||
realms, err := services.ListOwnedRealm(account)
|
||||
if 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) 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{})
|
||||
if request.Id != nil {
|
||||
tx = tx.Where("id = ?", request.GetId())
|
||||
}
|
||||
if request.Alias != nil {
|
||||
tx = tx.Where("alias = ?", request.GetAlias())
|
||||
}
|
||||
if request.IsPublic != nil {
|
||||
tx = tx.Where("is_public = ?", request.GetIsPublic())
|
||||
}
|
||||
if request.IsCommunity != nil {
|
||||
tx = tx.Where("is_community = ?", request.GetIsCommunity())
|
||||
}
|
||||
|
||||
if err := tx.First(&realm).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 *App) ListRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.ListRealmMemberResponse, error) {
|
||||
var members []models.RealmMember
|
||||
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())
|
||||
}
|
||||
|
||||
if err := tx.Find(&members).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *App) GetRealmMember(ctx context.Context, request *proto.RealmMemberLookupRequest) (*proto.RealmMemberInfo, error) {
|
||||
var member models.RealmMember
|
||||
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())
|
||||
}
|
||||
|
||||
if err := tx.First(&member).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.RealmMemberInfo{
|
||||
Id: uint64(member.ID),
|
||||
RealmId: uint64(member.RealmID),
|
||||
UserId: uint64(member.AccountID),
|
||||
PowerLevel: int32(member.PowerLevel),
|
||||
}, nil
|
||||
}
|
||||
|
||||
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()).
|
||||
Where("account_id = ?", request.GetUserId())
|
||||
|
||||
if err := tx.First(&member).Error; err != nil {
|
||||
return &proto.CheckRealmPermResponse{
|
||||
IsSuccess: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &proto.CheckRealmPermResponse{
|
||||
IsSuccess: member.PowerLevel >= int(request.GetPowerLevel()),
|
||||
}, nil
|
||||
}
|
57
pkg/internal/grpc/server.go
Normal file
57
pkg/internal/grpc/server.go
Normal file
@ -0,0 +1,57 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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() *App {
|
||||
server := &App{
|
||||
srv: grpc.NewServer(),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (v *App) Listen() error {
|
||||
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.srv.Serve(listener)
|
||||
}
|
125
pkg/internal/grpc/stream.go
Normal file
125
pkg/internal/grpc/stream.go
Normal 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
|
||||
}
|
42
pkg/internal/grpc/third_client.go
Normal file
42
pkg/internal/grpc/third_client.go
Normal 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
77
pkg/internal/grpc/user.go
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package passport
|
||||
package pkg
|
||||
|
||||
const (
|
||||
AppVersion = "1.0.0"
|
25
pkg/internal/services/account_groups.go
Normal file
25
pkg/internal/services/account_groups.go
Normal 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
|
||||
}
|
433
pkg/internal/services/accounts.go
Normal file
433
pkg/internal/services/accounts.go
Normal file
@ -0,0 +1,433 @@
|
||||
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/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{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).First(&account).Error; err != nil {
|
||||
return account, err
|
||||
}
|
||||
|
||||
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{
|
||||
Name: alias,
|
||||
}).First(&account).Error; err != nil {
|
||||
return account, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func LookupAccount(probe string) (models.Account, error) {
|
||||
var account models.Account
|
||||
if err := database.C.Where(models.Account{Name: probe}).First(&account).Error; err == nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
var contact models.AccountContact
|
||||
if err := database.C.Where(models.AccountContact{Content: probe}).First(&contact).Error; err == nil {
|
||||
if err := database.C.
|
||||
Where(models.Account{
|
||||
BaseModel: models.BaseModel{ID: contact.AccountID},
|
||||
}).First(&account).Error; err == nil {
|
||||
return account, err
|
||||
}
|
||||
}
|
||||
|
||||
return account, fmt.Errorf("account was not found")
|
||||
}
|
||||
|
||||
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,
|
||||
Profile: models.AccountProfile{
|
||||
Experience: 100,
|
||||
},
|
||||
Factors: []models.AuthFactor{
|
||||
{
|
||||
Type: models.PasswordAuthFactor,
|
||||
Secret: HashPassword(password),
|
||||
},
|
||||
},
|
||||
Contacts: []models.AccountContact{
|
||||
{
|
||||
Type: models.EmailAccountContact,
|
||||
Content: email,
|
||||
IsPrimary: true,
|
||||
VerifiedAt: nil,
|
||||
},
|
||||
},
|
||||
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
|
||||
} else if err := NotifyMagicToken(tk); err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func ConfirmAccount(code string) error {
|
||||
token, err := ValidateMagicToken(code, models.ConfirmMagicToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if token.AccountID == nil {
|
||||
return fmt.Errorf("magic token didn't assign a valid account")
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
BaseModel: models.BaseModel{ID: *token.AccountID},
|
||||
}).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ForceConfirmAccount(user); err != nil {
|
||||
return err
|
||||
} else {
|
||||
database.C.Delete(&token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ForceConfirmAccount(user models.Account) error {
|
||||
user.ConfirmedAt = lo.ToPtr(time.Now())
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func CheckAbleToResetPassword(user models.Account) error {
|
||||
var count int64
|
||||
if err := database.C.
|
||||
Where("account_id = ?", user.ID).
|
||||
Where("expired_at < ?", time.Now()).
|
||||
Where("type = ?", models.ResetPasswordMagicToken).
|
||||
Model(&models.MagicToken{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("unable to check reset password ability: %v", err)
|
||||
} else if count > 0 {
|
||||
return fmt.Errorf("you requested reset password recently")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequestResetPassword(user models.Account) error {
|
||||
if tk, err := NewMagicToken(
|
||||
models.ResetPasswordMagicToken,
|
||||
&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 password reset magic token...")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfirmResetPassword(code, newPassword string) error {
|
||||
token, err := ValidateMagicToken(code, models.ResetPasswordMagicToken)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if token.AccountID == nil {
|
||||
return fmt.Errorf("magic token didn't assign a valid account")
|
||||
}
|
||||
|
||||
factor, err := GetPasswordTypeFactor(*token.AccountID)
|
||||
if err != nil {
|
||||
factor = models.AuthFactor{
|
||||
Type: models.PasswordAuthFactor,
|
||||
Secret: HashPassword(newPassword),
|
||||
AccountID: *token.AccountID,
|
||||
}
|
||||
} else {
|
||||
factor.Secret = HashPassword(newPassword)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if err := tx.Delete(&models.AuthTicket{}, "account_id = ?", id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Select(clause.Associations).Delete(&models.Account{}, "id = ?", id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
func RecycleUnConfirmAccount() {
|
||||
deadline := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
var hitList []models.Account
|
||||
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
|
||||
}
|
||||
|
||||
if len(hitList) > 0 {
|
||||
log.Info().Int("count", len(hitList)).Msg("Going to recycle those un-confirmed accounts...")
|
||||
for _, entry := range hitList {
|
||||
if err := DeleteAccount(entry.ID); err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred while recycling accounts...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetAccountLastSeen(uid uint) error {
|
||||
var profile models.AccountProfile
|
||||
if err := database.C.Where("account_id = ?", uid).First(&profile).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
profile.LastSeenAt = lo.ToPtr(time.Now())
|
||||
|
||||
return database.C.Save(&profile).Error
|
||||
}
|
105
pkg/internal/services/auth.go
Normal file
105
pkg/internal/services/auth.go
Normal file
@ -0,0 +1,105 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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.Claims)
|
||||
ctx.Account.PermNodes = perms
|
||||
return
|
||||
}
|
||||
|
||||
err = fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
func KgAuthContextCache(sessionId uint) string {
|
||||
return cachekit.FKey("auth-context", sessionId)
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
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(sessionId uint) (models.AuthTicket, error) {
|
||||
// Query data from primary database
|
||||
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 ticket, fmt.Errorf("unavailable auth ticket: %v", err)
|
||||
}
|
||||
|
||||
user, err := GetAccount(ticket.AccountID)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
for k, v := range group.PermNodes {
|
||||
if _, ok := user.PermNodes[k]; !ok {
|
||||
user.PermNodes[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
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 InvalidUserAuthCache(uid uint) {
|
||||
cachekit.DeleteByTags(gap.Ca, "auth-context", fmt.Sprintf("user#%d", uid))
|
||||
}
|
35
pkg/internal/services/badges.go
Normal file
35
pkg/internal/services/badges.go
Normal file
@ -0,0 +1,35 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"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 {
|
||||
badge.AccountID = user.ID
|
||||
return database.C.Save(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
|
||||
}
|
56
pkg/internal/services/bot_token.go
Normal file
56
pkg/internal/services/bot_token.go
Normal 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
|
||||
}
|
24
pkg/internal/services/bots.go
Normal file
24
pkg/internal/services/bots.go
Normal 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
|
||||
}
|
139
pkg/internal/services/check_in.go
Normal file
139
pkg/internal/services/check_in.go
Normal 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
|
||||
}
|
22
pkg/internal/services/cleaner.go
Normal file
22
pkg/internal/services/cleaner.go
Normal file
@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DoAutoDatabaseCleanup() {
|
||||
log.Debug().Msg("Now cleaning up entire database...")
|
||||
|
||||
var count int64
|
||||
|
||||
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.")
|
||||
}
|
44
pkg/internal/services/clients.go
Normal file
44
pkg/internal/services/clients.go
Normal file
@ -0,0 +1,44 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
)
|
||||
|
||||
func GetThirdClient(id string) (models.ThirdClient, error) {
|
||||
var client models.ThirdClient
|
||||
if err := database.C.Where(&models.ThirdClient{
|
||||
Alias: id,
|
||||
}).First(&client).Error; err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
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 {
|
||||
return client, err
|
||||
}
|
||||
|
||||
if client.Secret != secret {
|
||||
return client, fmt.Errorf("invalid client secret")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package security
|
||||
package services
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
88
pkg/internal/services/events.go
Normal file
88
pkg/internal/services/events.go
Normal file
@ -0,0 +1,88 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"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
|
||||
writeAuditQueue []models.AuditRecord
|
||||
)
|
||||
|
||||
// AddEvent to keep operation logs by user themselves clear to query
|
||||
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,
|
||||
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,
|
||||
Location: location,
|
||||
CoordinateX: coordinateX,
|
||||
CoordinateY: coordinateY,
|
||||
AccountID: operator.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveEventChanges runs every 60 seconds to save events / audits changes into the database
|
||||
func SaveEventChanges() {
|
||||
if len(writeEventQueue) > 0 {
|
||||
count := len(writeEventQueue)
|
||||
database.C.CreateInBatches(writeEventQueue, min(count, 1000))
|
||||
log.Info().Int("count", count).Msg("Saved action events changes into database...")
|
||||
writeEventQueue = nil
|
||||
}
|
||||
if len(writeAuditQueue) > 0 {
|
||||
count := len(writeAuditQueue)
|
||||
database.C.CreateInBatches(writeAuditQueue, min(count, 1000))
|
||||
log.Info().Int("count", count).Msg("Saved audit records changes into database...")
|
||||
writeAuditQueue = nil
|
||||
}
|
||||
}
|
170
pkg/internal/services/factors.go
Normal file
170
pkg/internal/services/factors.go
Normal file
@ -0,0 +1,170 @@
|
||||
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"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetPasswordTypeFactor(userId uint) (models.AuthFactor, error) {
|
||||
var factor models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
Type: models.PasswordAuthFactor,
|
||||
AccountID: userId,
|
||||
}).First(&factor).Error
|
||||
|
||||
return factor, err
|
||||
}
|
||||
|
||||
func GetFactor(id uint) (models.AuthFactor, error) {
|
||||
var factor models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).First(&factor).Error
|
||||
|
||||
return factor, err
|
||||
}
|
||||
|
||||
func ListUserFactor(userId uint) ([]models.AuthFactor, error) {
|
||||
var factors []models.AuthFactor
|
||||
err := database.C.Where(models.AuthFactor{
|
||||
AccountID: userId,
|
||||
}).Find(&factors).Error
|
||||
|
||||
return factors, err
|
||||
}
|
||||
|
||||
func CountUserFactor(userId uint) int64 {
|
||||
var count int64
|
||||
database.C.Where(models.AuthFactor{
|
||||
AccountID: userId,
|
||||
}).Model(&models.AuthFactor{}).Count(&count)
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
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{
|
||||
BaseModel: models.BaseModel{ID: factor.AccountID},
|
||||
}).Preload("Contacts").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...")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func CheckFactor(factor models.AuthFactor, code string) error {
|
||||
switch factor.Type {
|
||||
case models.PasswordAuthFactor:
|
||||
return lo.Ternary(
|
||||
VerifyPassword(code, factor.Secret),
|
||||
nil,
|
||||
fmt.Errorf("invalid password"),
|
||||
)
|
||||
case models.TimeOtpFactor:
|
||||
return lo.Ternary(
|
||||
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
|
||||
}
|
73
pkg/internal/services/jwt.go
Normal file
73
pkg/internal/services/jwt.go
Normal file
@ -0,0 +1,73 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"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"`
|
||||
|
||||
// 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 (
|
||||
JwtAccessType = "access"
|
||||
JwtRefreshType = "refresh"
|
||||
)
|
||||
|
||||
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: viper.GetString("security.issuer"),
|
||||
ExpiresAt: jwt.NewNumericDate(exp),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ID: id,
|
||||
},
|
||||
AuthorizedParties: azp,
|
||||
SessionID: sed,
|
||||
Type: typ,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
235
pkg/internal/services/notifications.go
Normal file
235
pkg/internal/services/notifications.go
Normal file
@ -0,0 +1,235 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"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/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) {
|
||||
var prev models.NotificationSubscriber
|
||||
var subscriber models.NotificationSubscriber
|
||||
if err := database.C.Where(&models.NotificationSubscriber{
|
||||
DeviceID: id,
|
||||
AccountID: user.ID,
|
||||
}).Or(&models.NotificationSubscriber{
|
||||
DeviceToken: tk,
|
||||
AccountID: user.ID,
|
||||
}).First(&prev).Error; err != nil {
|
||||
subscriber = models.NotificationSubscriber{
|
||||
UserAgent: ua,
|
||||
Provider: provider,
|
||||
DeviceID: id,
|
||||
DeviceToken: tk,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
} else {
|
||||
subscriber = prev
|
||||
subscriber.UserAgent = ua
|
||||
subscriber.Provider = provider
|
||||
subscriber.DeviceToken = tk
|
||||
}
|
||||
|
||||
var err error
|
||||
if !reflect.DeepEqual(subscriber, prev) {
|
||||
err = database.C.Save(&subscriber).Error
|
||||
}
|
||||
|
||||
return subscriber, err
|
||||
}
|
||||
|
||||
// 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(¬ification).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := PushNotification(notification, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn()).PushStream(ctx, &proto.PushStreamRequest{
|
||||
UserId: lo.ToPtr(uint64(notification.AccountID)),
|
||||
Body: nex.WebSocketPackage{
|
||||
Action: "notifications.new",
|
||||
Payload: notification,
|
||||
}.Marshal(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push via websocket: %v", err)
|
||||
}
|
||||
|
||||
// Skip push notification
|
||||
if GetStatusDisturbable(notification.AccountID) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var subscribers []models.NotificationSubscriber
|
||||
if err := database.C.Where(&models.NotificationSubscriber{
|
||||
AccountID: notification.AccountID,
|
||||
}).Find(&subscribers).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var providers []string
|
||||
var tokens []string
|
||||
for _, subscriber := range subscribers {
|
||||
providers = append(providers, subscriber.Provider)
|
||||
tokens = append(tokens, subscriber.DeviceToken)
|
||||
}
|
||||
|
||||
log.Debug().Str("topic", notification.Topic).Any("uid", notification.AccountID).Msg("Pushing notify to user...")
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
88
pkg/internal/services/perms.go
Normal file
88
pkg/internal/services/perms.go
Normal file
@ -0,0 +1,88 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func HasPermNode(perms map[string]any, requiredKey string, requiredValue any) bool {
|
||||
if heldValue, ok := perms[requiredKey]; ok {
|
||||
return ComparePermNode(heldValue, requiredValue)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasPermNodeWithDefault(perms map[string]any, requiredKey string, requiredValue any, defaultValue any) bool {
|
||||
if heldValue, ok := perms[requiredKey]; ok {
|
||||
return ComparePermNode(heldValue, requiredValue)
|
||||
}
|
||||
return ComparePermNode(defaultValue, requiredValue)
|
||||
}
|
||||
|
||||
func ComparePermNode(held any, required any) bool {
|
||||
isNumeric := func(val reflect.Value) bool {
|
||||
kind := val.Kind()
|
||||
return kind >= reflect.Int && kind <= reflect.Uint64 || kind >= reflect.Float32 && kind <= reflect.Float64
|
||||
}
|
||||
|
||||
toFloat64 := func(val reflect.Value) float64 {
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return float64(val.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return float64(val.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return val.Float()
|
||||
default:
|
||||
panic(fmt.Sprintf("non-numeric value of kind %s", val.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
heldValue := reflect.ValueOf(held)
|
||||
requiredValue := reflect.ValueOf(required)
|
||||
|
||||
switch heldValue.Kind() {
|
||||
case reflect.String:
|
||||
if heldValue.String() == requiredValue.String() {
|
||||
return true
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < heldValue.Len(); i++ {
|
||||
if reflect.DeepEqual(heldValue.Index(i).Interface(), required) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
default:
|
||||
if isNumeric(heldValue) && isNumeric(requiredValue) {
|
||||
return toFloat64(heldValue) >= toFloat64(requiredValue)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(held, required) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func FilterPermNodes(tree map[string]any, claims []string) map[string]any {
|
||||
filteredTree := make(map[string]any)
|
||||
|
||||
match := func(claim, permission string) bool {
|
||||
regex := strings.ReplaceAll(claim, "*", ".*")
|
||||
match, _ := regexp.MatchString(fmt.Sprintf("^%s$", regex), permission)
|
||||
return match
|
||||
}
|
||||
|
||||
for _, claim := range claims {
|
||||
for key, value := range tree {
|
||||
if match(claim, key) {
|
||||
filteredTree[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTree
|
||||
}
|
164
pkg/internal/services/preferences.go
Normal file
164
pkg/internal/services/preferences.go
Normal file
@ -0,0 +1,164 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
func GetAuthPreference(account models.Account) (models.PreferenceAuth, error) {
|
||||
var auth models.PreferenceAuth
|
||||
if err := database.C.Where("account_id = ?", account.ID).First(&auth).Error; err != nil {
|
||||
return auth, err
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func UpdateAuthPreference(account models.Account, config models.AuthConfig) (models.PreferenceAuth, error) {
|
||||
var auth models.PreferenceAuth
|
||||
var err error
|
||||
if auth, err = GetAuthPreference(account); err != nil {
|
||||
auth = models.PreferenceAuth{
|
||||
AccountID: account.ID,
|
||||
Config: datatypes.NewJSONType(config),
|
||||
}
|
||||
} else {
|
||||
auth.Config = datatypes.NewJSONType(config)
|
||||
}
|
||||
|
||||
err = database.C.Save(&auth).Error
|
||||
return auth, err
|
||||
}
|
||||
|
||||
func KgNotifyPreferenceCache(accountId uint) string {
|
||||
return fmt.Sprintf("notification-preference#%d", accountId)
|
||||
}
|
||||
|
||||
func GetNotifyPreference(account models.Account) (models.PreferenceNotification, error) {
|
||||
var notification models.PreferenceNotification
|
||||
if val, err := cachekit.Get[models.PreferenceNotification](
|
||||
gap.Ca,
|
||||
KgNotifyPreferenceCache(account.ID),
|
||||
); err == nil {
|
||||
return val, nil
|
||||
}
|
||||
if err := database.C.Where("account_id = ?", account.ID).First(¬ification).Error; err != nil {
|
||||
return notification, err
|
||||
}
|
||||
CacheNotifyPreference(notification)
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
func CacheNotifyPreference(prefs models.PreferenceNotification) {
|
||||
cachekit.Set[models.PreferenceNotification](
|
||||
gap.Ca,
|
||||
KgNotifyPreferenceCache(prefs.AccountID),
|
||||
prefs,
|
||||
time.Minute*60,
|
||||
fmt.Sprintf("user#%d", prefs.AccountID),
|
||||
)
|
||||
}
|
||||
|
||||
func UpdateNotifyPreference(account models.Account, config map[string]bool) (models.PreferenceNotification, error) {
|
||||
var notification models.PreferenceNotification
|
||||
var err error
|
||||
if notification, err = GetNotifyPreference(account); err != nil {
|
||||
notification = models.PreferenceNotification{
|
||||
AccountID: account.ID,
|
||||
Config: lo.MapValues(config, func(v bool, k string) any { return v }),
|
||||
}
|
||||
} else {
|
||||
notification.Config = lo.MapValues(config, func(v bool, k string) any { return v })
|
||||
}
|
||||
|
||||
err = database.C.Save(¬ification).Error
|
||||
if err == nil {
|
||||
CacheNotifyPreference(notification)
|
||||
}
|
||||
|
||||
return notification, err
|
||||
}
|
||||
|
||||
func CheckNotificationNotifiable(account models.Account, topic string) bool {
|
||||
var notification models.PreferenceNotification
|
||||
if val, err := cachekit.Get[models.PreferenceNotification](
|
||||
gap.Ca,
|
||||
KgNotifyPreferenceCache(account.ID),
|
||||
); err == nil {
|
||||
notification = val
|
||||
} else {
|
||||
if err := database.C.Where("account_id = ?", account.ID).First(¬ification).Error; err != nil {
|
||||
return true
|
||||
}
|
||||
CacheNotifyPreference(notification)
|
||||
}
|
||||
|
||||
if val, ok := notification.Config[topic]; ok {
|
||||
if status, ok := val.(bool); ok {
|
||||
return status
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CheckNotificationNotifiableBatch(accounts []models.Account, topic string) []bool {
|
||||
notifiable := make([]bool, len(accounts))
|
||||
var queryNeededIdx []uint
|
||||
notificationMap := make(map[uint]models.PreferenceNotification)
|
||||
|
||||
// Check cache for each account
|
||||
for _, account := range accounts {
|
||||
cacheKey := KgNotifyPreferenceCache(account.ID)
|
||||
if val, err := cachekit.Get[models.PreferenceNotification](gap.Ca, cacheKey); err == nil {
|
||||
notificationMap[account.ID] = val
|
||||
} else {
|
||||
// Add to the list of accounts that need to be queried
|
||||
queryNeededIdx = append(queryNeededIdx, account.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Query the database for missing account IDs
|
||||
if len(queryNeededIdx) > 0 {
|
||||
var dbNotifications []models.PreferenceNotification
|
||||
if err := database.C.Where("account_id IN ?", queryNeededIdx).Find(&dbNotifications).Error; err != nil {
|
||||
// Handle error by returning false for accounts without cached notifications
|
||||
return lo.Map(accounts, func(item models.Account, index int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Cache the newly fetched notifications and add to the notificationMap
|
||||
for _, notification := range dbNotifications {
|
||||
notificationMap[notification.AccountID] = notification
|
||||
CacheNotifyPreference(notification) // Cache the result
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Any("notifiable", notificationMap).Msg("Fetched notifiable status...")
|
||||
|
||||
// Process the notifiable status for the fetched notifications
|
||||
for idx, account := range accounts {
|
||||
if notification, exists := notificationMap[account.ID]; exists {
|
||||
if val, ok := notification.Config[topic]; ok {
|
||||
if status, ok := val.(bool); ok {
|
||||
notifiable[idx] = status
|
||||
continue
|
||||
}
|
||||
}
|
||||
notifiable[idx] = true
|
||||
} else {
|
||||
notifiable[idx] = true
|
||||
}
|
||||
}
|
||||
|
||||
return notifiable
|
||||
}
|
142
pkg/internal/services/programs.go
Normal file
142
pkg/internal/services/programs.go
Normal file
@ -0,0 +1,142 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/datatypes"
|
||||
)
|
||||
|
||||
func JoinProgram(user models.Account, program models.Program) (models.ProgramMember, error) {
|
||||
var member models.ProgramMember
|
||||
if err := database.C.Where("account_id = ? AND program_id = ?", user.ID, program.ID).First(&member).Error; err == nil {
|
||||
return member, fmt.Errorf("program member already exists")
|
||||
}
|
||||
var profile models.AccountProfile
|
||||
if err := database.C.Where("account_id = ?", user.ID).Select("experience").First(&profile).Error; err != nil {
|
||||
return member, err
|
||||
}
|
||||
if program.ExpRequirement > int64(profile.Experience) {
|
||||
return member, fmt.Errorf("insufficient experience")
|
||||
}
|
||||
member = models.ProgramMember{
|
||||
LastPaid: lo.ToPtr(time.Now()),
|
||||
Account: user,
|
||||
AccountID: user.ID,
|
||||
Program: program,
|
||||
ProgramID: program.ID,
|
||||
}
|
||||
if err := ChargeForProgram(member); err != nil {
|
||||
return member, err
|
||||
}
|
||||
if err := database.C.Create(&member).Error; err != nil {
|
||||
return member, err
|
||||
} else {
|
||||
PostJoinProgram(member)
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func LeaveProgram(user models.Account, program models.Program) error {
|
||||
var member models.ProgramMember
|
||||
if err := database.C.Where("account_id = ? AND program_id = ?", user.ID, program.ID).
|
||||
Preload("Program").
|
||||
First(&member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := database.C.Delete(&member).Error; err != nil {
|
||||
return err
|
||||
} else {
|
||||
PostLeaveProgram(member)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChargeForProgram(member models.ProgramMember) error {
|
||||
pricing := member.Program.Price.Data()
|
||||
if pricing.Amount == 0 {
|
||||
return nil
|
||||
}
|
||||
conn, err := gap.Nx.GetClientGrpcConn("wa")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wc := proto.NewPaymentServiceClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
_, err = wc.MakeTransactionWithAccount(ctx, &proto.MakeTransactionWithAccountRequest{
|
||||
PayerAccountId: lo.ToPtr(uint64(member.AccountID)),
|
||||
Amount: pricing.Amount,
|
||||
Currency: pricing.Currency,
|
||||
Remark: fmt.Sprintf("Program Membership: %s", member.Program.Name),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func PeriodicChargeProgramFee() {
|
||||
var members []models.ProgramMember
|
||||
// Every month paid once
|
||||
if err := database.C.Where("last_paid IS NULL OR last_paid < ?", time.Now().AddDate(0, 0, -30)).
|
||||
Preload("Program").Preload("Account").Find(&members).Error; err != nil {
|
||||
return
|
||||
}
|
||||
for _, member := range members {
|
||||
if err := ChargeForProgram(member); err == nil {
|
||||
database.C.Model(&member).Update("last_paid", time.Now())
|
||||
} else {
|
||||
LeaveProgram(member.Account, member.Program)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PostJoinProgram(member models.ProgramMember) error {
|
||||
badge := member.Program.Badge.Data()
|
||||
if len(badge.Type) > 0 {
|
||||
accountBadge := models.Badge{
|
||||
Type: badge.Type,
|
||||
AccountID: member.AccountID,
|
||||
Metadata: datatypes.JSONMap(badge.Metadata),
|
||||
}
|
||||
if err := database.C.Create(&accountBadge).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create badge for program member...")
|
||||
return err
|
||||
}
|
||||
}
|
||||
group := member.Program.Group.Data()
|
||||
if group.ID > 0 {
|
||||
accountGroup := models.AccountGroupMember{
|
||||
GroupID: group.ID,
|
||||
AccountID: member.AccountID,
|
||||
}
|
||||
if err := database.C.Create(&accountGroup).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create group for program member...")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostLeaveProgram(member models.ProgramMember) error {
|
||||
badge := member.Program.Badge.Data()
|
||||
if len(badge.Type) > 0 {
|
||||
if err := database.C.Where("account_id = ? AND type = ?", member.AccountID, badge.Type).Delete(&models.Badge{}).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete badge for program member...")
|
||||
return err
|
||||
}
|
||||
}
|
||||
group := member.Program.Group.Data()
|
||||
if group.ID > 0 {
|
||||
if err := database.C.Where("account_id = ? AND group_id = ?", member.AccountID, group.ID).Delete(&models.AccountGroupMember{}).Error; err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete group for program member...")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
203
pkg/internal/services/punishments.go
Normal file
203
pkg/internal/services/punishments.go
Normal file
@ -0,0 +1,203 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func NewPunishment(in models.Punishment, moderator ...models.Account) (models.Punishment, error) {
|
||||
if len(moderator) > 0 {
|
||||
in.Moderator = &moderator[0]
|
||||
in.ModeratorID = &moderator[0].ID
|
||||
}
|
||||
|
||||
// If user got more than 2 strikes, it will upgrade to limited
|
||||
if in.Type == models.PunishmentTypeStrike {
|
||||
var count int64
|
||||
if err := database.C.Model(&models.Punishment{}).
|
||||
Where("account_id = ? AND type = ?", in.AccountID, models.PunishmentTypeStrike).
|
||||
Count(&count).Error; err != nil {
|
||||
return in, err
|
||||
}
|
||||
if count > 2 {
|
||||
in.Type = models.PunishmentTypeLimited
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.C.Create(&in).Error; err != nil {
|
||||
return in, err
|
||||
} else {
|
||||
moderator := "System"
|
||||
if in.Moderator != nil {
|
||||
moderator = fmt.Sprintf("@%s", in.Moderator.Name)
|
||||
}
|
||||
err = NewNotification(models.Notification{
|
||||
Topic: "passport.punishments",
|
||||
Title: localize.L.GetLocalizedString("subjectPunishmentCreated", in.Account.Language),
|
||||
Subtitle: fmt.Sprintf(localize.L.GetLocalizedString("subtitlePunishment", in.Account.Language), in.ID, moderator),
|
||||
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyPunishmentCreated", in.Account.Language), in.Reason),
|
||||
Account: in.Account,
|
||||
AccountID: in.Account.ID,
|
||||
Metadata: map[string]any{"punishment": in},
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Uint("case", in.ID).Msg("Failed to delivery punishment via notify...")
|
||||
}
|
||||
}
|
||||
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func EditPunishment(in models.Punishment) (models.Punishment, error) {
|
||||
if err := database.C.Save(&in).Error; err != nil {
|
||||
return in, err
|
||||
} else {
|
||||
moderator := "System"
|
||||
if in.Moderator != nil {
|
||||
moderator = fmt.Sprintf("@%s", in.Moderator.Name)
|
||||
}
|
||||
err = NewNotification(models.Notification{
|
||||
Topic: "passport.punishments",
|
||||
Title: localize.L.GetLocalizedString("subjectPunishmentUpdated", in.Account.Language),
|
||||
Subtitle: fmt.Sprintf(localize.L.GetLocalizedString("subtitlePunishment", in.Account.Language), in.ID, moderator),
|
||||
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyPunishmentUpdated", in.Account.Language), in.ID),
|
||||
Account: in.Account,
|
||||
AccountID: in.Account.ID,
|
||||
Metadata: map[string]any{"punishment": in},
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Uint("case", in.ID).Msg("Failed to delivery punishment via notify...")
|
||||
}
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func DeletePunishment(in models.Punishment) error {
|
||||
if err := database.C.Delete(&in).Error; err != nil {
|
||||
return err
|
||||
} else {
|
||||
moderator := "System"
|
||||
if in.Moderator != nil {
|
||||
moderator = fmt.Sprintf("@%s", in.Moderator.Name)
|
||||
}
|
||||
err = NewNotification(models.Notification{
|
||||
Topic: "passport.punishments",
|
||||
Title: localize.L.GetLocalizedString("subjectPunishmentDeleted", in.Account.Language),
|
||||
Subtitle: fmt.Sprintf(localize.L.GetLocalizedString("subtitlePunishment", in.Account.Language), in.ID, moderator),
|
||||
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyPunishmentDeleted", in.Account.Language), in.ID),
|
||||
Account: in.Account,
|
||||
AccountID: in.Account.ID,
|
||||
Metadata: map[string]any{"punishment": in},
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Uint("case", in.ID).Msg("Failed to delivery punishment via notify...")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPunishment(id uint, preload ...bool) (models.Punishment, error) {
|
||||
tx := database.C
|
||||
if len(preload) > 0 && preload[0] {
|
||||
tx = tx.Preload("Moderator").Preload("Account")
|
||||
}
|
||||
|
||||
var punishment models.Punishment
|
||||
if err := tx.First(&punishment, id).Error; err != nil {
|
||||
return punishment, err
|
||||
}
|
||||
return punishment, nil
|
||||
}
|
||||
|
||||
func GetMadePunishment(id uint, moderator models.Account) (models.Punishment, error) {
|
||||
var punishment models.Punishment
|
||||
if err := database.C.Where("id = ? AND moderator_id = ?", id, moderator.ID).First(&punishment).Error; err != nil {
|
||||
return punishment, err
|
||||
}
|
||||
return punishment, nil
|
||||
}
|
||||
|
||||
func ListPunishments(user models.Account) ([]models.Punishment, error) {
|
||||
var punishments []models.Punishment
|
||||
if err := database.C.
|
||||
Where("account_id = ? AND (expired_at IS NULL OR expired_at > ?)", user.ID, time.Now()).
|
||||
Preload("Moderator").
|
||||
Order("created_at DESC").
|
||||
Find(&punishments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return punishments, nil
|
||||
}
|
||||
|
||||
func CountAllPunishments() (int64, error) {
|
||||
var count int64
|
||||
if err := database.C.
|
||||
Model(&models.Punishment{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func ListAllPunishments(take, offset int) ([]models.Punishment, error) {
|
||||
var punishments []models.Punishment
|
||||
if err := database.C.
|
||||
Preload("Account").
|
||||
Preload("Moderator").
|
||||
Order("created_at DESC").
|
||||
Take(take).Offset(offset).
|
||||
Find(&punishments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return punishments, nil
|
||||
}
|
||||
|
||||
func CountMadePunishments(moderator models.Account) (int64, error) {
|
||||
var count int64
|
||||
if err := database.C.
|
||||
Model(&models.Punishment{}).
|
||||
Where("moderator_id = ?", moderator.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func ListMadePunishments(moderator models.Account, take, offset int) ([]models.Punishment, error) {
|
||||
var punishments []models.Punishment
|
||||
if err := database.C.
|
||||
Where("moderator_id = ?", moderator.ID).
|
||||
Preload("Account").
|
||||
Order("created_at DESC").
|
||||
Take(take).Offset(offset).
|
||||
Find(&punishments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return punishments, nil
|
||||
}
|
||||
|
||||
func CheckLoginAbility(user models.Account) error {
|
||||
var punishments []models.Punishment
|
||||
if err := database.C.Where("account_id = ? AND (expired_at IS NULL OR expired_at > ?)", user.ID, time.Now()).
|
||||
Find(&punishments).Error; err != nil {
|
||||
return fmt.Errorf("failed to get punishments: %v", err)
|
||||
}
|
||||
|
||||
for _, punishment := range punishments {
|
||||
if punishment.Type == models.PunishmentTypeDisabled {
|
||||
return fmt.Errorf("account has been fully disabled due to: %s (case #%d)", punishment.Reason, punishment.ID)
|
||||
}
|
||||
// Limited punishment with no permissions override is fully limited
|
||||
// Refer https://solsynth.dev/terms/basic-law#provision-and-discontinuation-of-services
|
||||
if punishment.Type == models.PunishmentTypeLimited && len(punishment.PermNodes) == 0 {
|
||||
return fmt.Errorf("account has been limited login due to: %s (case #%d)", punishment.Reason, punishment.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
265
pkg/internal/services/realms.go
Normal file
265
pkg/internal/services/realms.go
Normal file
@ -0,0 +1,265 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
"git.solsynth.dev/hypernet/paperclip/pkg/filekit"
|
||||
pproto "git.solsynth.dev/hypernet/paperclip/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/gap"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListCommunityRealm() ([]models.Realm, error) {
|
||||
var realms []models.Realm
|
||||
if err := database.C.Where(&models.Realm{
|
||||
IsCommunity: true,
|
||||
}).Order("popularity DESC").Find(&realms).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
return realms, nil
|
||||
}
|
||||
|
||||
func ListOwnedRealm(user models.Account) ([]models.Realm, error) {
|
||||
var realms []models.Realm
|
||||
if err := database.C.Where(&models.Realm{AccountID: user.ID}).Find(&realms).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
return realms, nil
|
||||
}
|
||||
|
||||
func ListAvailableRealm(user models.Account, includePublic ...bool) ([]models.Realm, error) {
|
||||
var realms []models.Realm
|
||||
var members []models.RealmMember
|
||||
if err := database.C.Where(&models.RealmMember{
|
||||
AccountID: user.ID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
idx := lo.Map(members, func(item models.RealmMember, index int) uint {
|
||||
return item.RealmID
|
||||
})
|
||||
|
||||
tx := database.C
|
||||
if len(includePublic) > 0 && includePublic[0] {
|
||||
tx = tx.Where("is_public = ?", true)
|
||||
}
|
||||
|
||||
if err := tx.Where("id IN ?", idx).Find(&realms).Error; err != nil {
|
||||
return realms, err
|
||||
}
|
||||
|
||||
return realms, nil
|
||||
}
|
||||
|
||||
func GetRealmWithAlias(alias string) (models.Realm, error) {
|
||||
tx := database.C.Where("alias = ?", alias)
|
||||
|
||||
numericId, err := strconv.Atoi(alias)
|
||||
if err == nil {
|
||||
tx.Or("id = ?", numericId)
|
||||
}
|
||||
|
||||
var realm models.Realm
|
||||
if err := tx.First(&realm).Error; err != nil {
|
||||
return realm, err
|
||||
}
|
||||
return realm, nil
|
||||
}
|
||||
|
||||
func NewRealm(realm models.Realm, user models.Account) (models.Realm, error) {
|
||||
realm.Members = []models.RealmMember{
|
||||
{AccountID: user.ID, PowerLevel: 100},
|
||||
}
|
||||
|
||||
var attachments []string
|
||||
if realm.Avatar != nil && len(*realm.Avatar) > 0 {
|
||||
attachments = append(attachments, *realm.Avatar)
|
||||
}
|
||||
if realm.Banner != nil && len(*realm.Banner) > 0 {
|
||||
attachments = append(attachments, *realm.Banner)
|
||||
}
|
||||
if len(attachments) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &pproto.UpdateUsageRequest{
|
||||
Rid: attachments,
|
||||
Delta: 1,
|
||||
})
|
||||
}
|
||||
|
||||
err := database.C.Save(&realm).Error
|
||||
return realm, err
|
||||
}
|
||||
|
||||
func CountRealmMember(realmId uint) (int64, error) {
|
||||
var count int64
|
||||
if err := database.C.Where(&models.RealmMember{
|
||||
RealmID: realmId,
|
||||
}).Model(&models.RealmMember{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ListRealmMember(realmId uint, take int, offset int) ([]models.RealmMember, error) {
|
||||
var members []models.RealmMember
|
||||
|
||||
if err := database.C.
|
||||
Limit(take).Offset(offset).
|
||||
Where(&models.RealmMember{RealmID: realmId}).
|
||||
Preload("Account").
|
||||
Find(&members).Error; err != nil {
|
||||
return members, err
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func GetRealmMember(userId uint, realmId uint) (models.RealmMember, error) {
|
||||
var member models.RealmMember
|
||||
if err := database.C.Where(&models.RealmMember{
|
||||
AccountID: userId,
|
||||
RealmID: realmId,
|
||||
}).Find(&member).Error; err != nil {
|
||||
return member, err
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func AddRealmMember(user models.Account, affected models.Account, target models.Realm) error {
|
||||
var member models.RealmMember
|
||||
if err := database.C.Where(&models.RealmMember{
|
||||
AccountID: affected.ID,
|
||||
RealmID: target.ID,
|
||||
}).First(&member).Error; err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !target.IsCommunity {
|
||||
if member, err := GetRealmMember(user.ID, target.ID); err != nil {
|
||||
return fmt.Errorf("only realm member can add people: %v", err)
|
||||
} else if member.PowerLevel < 50 {
|
||||
return fmt.Errorf("only realm moderator can add member")
|
||||
}
|
||||
rel, err := GetRelationWithTwoNode(affected.ID, user.ID)
|
||||
if err != nil || HasPermNodeWithDefault(
|
||||
rel.PermNodes,
|
||||
"RealmAdd",
|
||||
true,
|
||||
rel.Status == models.RelationshipFriend,
|
||||
) {
|
||||
return fmt.Errorf("you unable to add this user to your realm")
|
||||
}
|
||||
}
|
||||
|
||||
member = models.RealmMember{
|
||||
RealmID: target.ID,
|
||||
AccountID: affected.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
if err == nil {
|
||||
database.C.Model(&models.Realm{}).
|
||||
Where("id = ?", target.ID).
|
||||
Update("popularity", gorm.Expr("popularity + ?", models.RealmPopularityMemberFactor))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveRealmMember(user models.Account, affected models.RealmMember, target models.Realm) error {
|
||||
if user.ID != affected.AccountID {
|
||||
if member, err := GetRealmMember(user.ID, target.ID); err != nil {
|
||||
return fmt.Errorf("only realm member can remove other member: %v", err)
|
||||
} else if member.PowerLevel < 50 {
|
||||
return fmt.Errorf("only realm moderator can kick member")
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.C.Delete(&affected).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
database.C.Model(&models.Realm{}).
|
||||
Where("id = ?", target.ID).
|
||||
Update("popularity", gorm.Expr("popularity - ?", models.RealmPopularityMemberFactor))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EditRealm(realm, og models.Realm) (models.Realm, error) {
|
||||
err := database.C.Save(&realm).Error
|
||||
if err == nil {
|
||||
var minusAttachments, plusAttachments []string
|
||||
if realm.Avatar != og.Avatar && realm.Avatar != nil {
|
||||
minusAttachments = append(minusAttachments, *og.Avatar)
|
||||
plusAttachments = append(plusAttachments, *realm.Avatar)
|
||||
}
|
||||
if realm.Banner != og.Banner && realm.Banner != nil {
|
||||
minusAttachments = append(minusAttachments, *og.Banner)
|
||||
plusAttachments = append(plusAttachments, *realm.Banner)
|
||||
}
|
||||
if len(minusAttachments) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &pproto.UpdateUsageRequest{
|
||||
Rid: minusAttachments,
|
||||
Delta: -1,
|
||||
})
|
||||
}
|
||||
if len(plusAttachments) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &pproto.UpdateUsageRequest{
|
||||
Rid: plusAttachments,
|
||||
Delta: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
return realm, err
|
||||
}
|
||||
|
||||
func DeleteRealm(realm models.Realm) error {
|
||||
tx := database.C.Begin()
|
||||
if err := tx.Where("realm_id = ?", realm.ID).Delete(&models.RealmMember{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&realm).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
} else {
|
||||
var attachments []string
|
||||
if realm.Avatar != nil && len(*realm.Avatar) > 0 {
|
||||
attachments = append(attachments, *realm.Avatar)
|
||||
}
|
||||
if realm.Banner != nil && len(*realm.Banner) > 0 {
|
||||
attachments = append(attachments, *realm.Banner)
|
||||
}
|
||||
if len(attachments) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &pproto.UpdateUsageRequest{
|
||||
Rid: attachments,
|
||||
Delta: -1,
|
||||
})
|
||||
}
|
||||
|
||||
conn := gap.Nx.GetNexusGrpcConn()
|
||||
_, _ = proto.NewDirectoryServiceClient(conn).BroadcastEvent(context.Background(), &proto.EventInfo{
|
||||
Event: "deletion",
|
||||
Data: nex.EncodeMap(map[string]any{
|
||||
"type": "realm",
|
||||
"id": realm.ID,
|
||||
}),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
190
pkg/internal/services/relationships.go
Normal file
190
pkg/internal/services/relationships.go
Normal file
@ -0,0 +1,190 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListAllRelationship(user models.Account) ([]models.AccountRelationship, error) {
|
||||
var relationships []models.AccountRelationship
|
||||
if err := database.C.
|
||||
Where("account_id = ?", user.ID).
|
||||
Preload("Account").
|
||||
Preload("Related").
|
||||
Find(&relationships).Error; err != nil {
|
||||
return relationships, err
|
||||
}
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
func ListRelationshipWithFilter(user models.Account, status ...models.RelationshipStatus) ([]models.AccountRelationship, error) {
|
||||
var relationships []models.AccountRelationship
|
||||
if err := database.C.
|
||||
Where("account_id = ? AND status IN ?", user.ID, status).
|
||||
Preload("Account").
|
||||
Preload("Related").
|
||||
Find(&relationships).Error; err != nil {
|
||||
return relationships, err
|
||||
}
|
||||
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
func GetRelationship(otherId uint) (models.AccountRelationship, error) {
|
||||
var relationship models.AccountRelationship
|
||||
if err := database.C.
|
||||
Where(&models.AccountRelationship{AccountID: otherId}).
|
||||
Preload("Account").
|
||||
Preload("Related").
|
||||
First(&relationship).Error; err != nil {
|
||||
return relationship, err
|
||||
}
|
||||
|
||||
return relationship, nil
|
||||
}
|
||||
|
||||
func GetRelationWithTwoNode(userId, relatedId uint, noPreload ...bool) (models.AccountRelationship, error) {
|
||||
var tx *gorm.DB
|
||||
if len(noPreload) > 0 && noPreload[0] {
|
||||
tx = database.C
|
||||
} else {
|
||||
tx = database.C.Preload("Account").Preload("Related")
|
||||
}
|
||||
|
||||
var relationship models.AccountRelationship
|
||||
if err := tx.
|
||||
Where(&models.AccountRelationship{AccountID: userId, RelatedID: relatedId}).
|
||||
First(&relationship).Error; err != nil {
|
||||
return relationship, err
|
||||
}
|
||||
|
||||
return relationship, nil
|
||||
}
|
||||
|
||||
func EditRelationship(relationship models.AccountRelationship) (models.AccountRelationship, error) {
|
||||
if err := database.C.Save(&relationship).Error; err != nil {
|
||||
return relationship, err
|
||||
}
|
||||
return relationship, nil
|
||||
}
|
||||
|
||||
func DeleteRelationship(relationship models.AccountRelationship) error {
|
||||
if err := database.C.Delete(&relationship).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBlockship(userA models.Account, userB models.Account) (models.AccountRelationship, error) {
|
||||
var err error
|
||||
var rel models.AccountRelationship
|
||||
if rel, err = GetRelationWithTwoNode(userA.ID, userB.ID, true); err == nil {
|
||||
rel.Status = models.RelationshipBlocked
|
||||
} else {
|
||||
rel = models.AccountRelationship{
|
||||
AccountID: userA.ID,
|
||||
RelatedID: userB.ID,
|
||||
Status: models.RelationshipBlocked,
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.C.Save(&rel).Error; err != nil {
|
||||
return rel, err
|
||||
}
|
||||
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func NewFriend(userA models.Account, userB models.Account, skipPending ...bool) (models.AccountRelationship, error) {
|
||||
relA := models.AccountRelationship{
|
||||
AccountID: userA.ID,
|
||||
RelatedID: userB.ID,
|
||||
Status: models.RelationshipWaiting,
|
||||
}
|
||||
relB := models.AccountRelationship{
|
||||
AccountID: userB.ID,
|
||||
RelatedID: userA.ID,
|
||||
Status: models.RelationshipPending,
|
||||
}
|
||||
|
||||
if userA.ID == userB.ID {
|
||||
return relA, fmt.Errorf("unable to make relationship with yourself")
|
||||
}
|
||||
|
||||
var dupeCount int
|
||||
if rel, err := GetRelationWithTwoNode(userA.ID, userB.ID, true); err == nil {
|
||||
relA = rel
|
||||
dupeCount++
|
||||
}
|
||||
if rel, err := GetRelationWithTwoNode(userB.ID, userA.ID, true); err == nil {
|
||||
relB = rel
|
||||
dupeCount++
|
||||
}
|
||||
|
||||
if dupeCount > 1 {
|
||||
return relA, fmt.Errorf("unable to recreate a relationship with that user")
|
||||
}
|
||||
|
||||
if len(skipPending) > 0 && skipPending[0] {
|
||||
relA.Status = models.RelationshipFriend
|
||||
relB.Status = models.RelationshipFriend
|
||||
}
|
||||
|
||||
if err := database.C.Save(&relA).Error; err != nil {
|
||||
return relA, err
|
||||
} else if err = database.C.Save(&relB).Error; err != nil {
|
||||
return relA, err
|
||||
} else {
|
||||
_ = NewNotification(models.Notification{
|
||||
Title: "New Friend Request",
|
||||
Subtitle: fmt.Sprintf("New friend request from %s", userA.Name),
|
||||
Body: fmt.Sprintf("You got a new friend request from %s. Go to your account page and decide how to deal it.", userA.Nick),
|
||||
Account: userB,
|
||||
AccountID: userB.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return relA, nil
|
||||
}
|
||||
|
||||
func HandleFriend(userA models.Account, userB models.Account, isAccept bool) error {
|
||||
relA, err := GetRelationWithTwoNode(userA.ID, userB.ID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("relationship was not found: %v", err)
|
||||
} else if relA.Status != models.RelationshipPending {
|
||||
return fmt.Errorf("relationship already handled")
|
||||
}
|
||||
|
||||
if isAccept {
|
||||
relA.Status = models.RelationshipFriend
|
||||
} else {
|
||||
relA.Status = models.RelationshipBlocked
|
||||
}
|
||||
|
||||
if err := database.C.Save(&relA).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relB, err := GetRelationWithTwoNode(userB.ID, userA.ID, true)
|
||||
if err == nil && relB.Status == models.RelationshipWaiting {
|
||||
relB.Status = models.RelationshipFriend
|
||||
if err := database.C.Save(&relB).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = NewNotification(models.Notification{
|
||||
Title: "Friend Request Processed",
|
||||
Subtitle: fmt.Sprintf("Your friend request to %s has been processsed.", userA.Name),
|
||||
Body: fmt.Sprintf("Your relationship status with %s has been updated, go check it out!", userA.Nick),
|
||||
Account: userB,
|
||||
AccountID: userB.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
76
pkg/internal/services/reports.go
Normal file
76
pkg/internal/services/reports.go
Normal file
@ -0,0 +1,76 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
)
|
||||
|
||||
func ListAbuseReport(account models.Account) ([]models.AbuseReport, error) {
|
||||
var reports []models.AbuseReport
|
||||
err := database.C.
|
||||
Where("account_id = ?", account.ID).
|
||||
Find(&reports).Error
|
||||
return reports, err
|
||||
}
|
||||
|
||||
func GetAbuseReport(id uint) (models.AbuseReport, error) {
|
||||
var report models.AbuseReport
|
||||
err := database.C.
|
||||
Where("id = ?", id).
|
||||
First(&report).Error
|
||||
return report, err
|
||||
}
|
||||
|
||||
func UpdateAbuseReportStatus(id uint, status, message string) error {
|
||||
var report models.AbuseReport
|
||||
err := database.C.
|
||||
Where("id = ?", id).
|
||||
Preload("Account").
|
||||
First(&report).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
report.Status = status
|
||||
account := report.Account
|
||||
|
||||
err = database.C.Save(&report).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = NewNotification(models.Notification{
|
||||
Topic: "reports.feedback",
|
||||
Title: localize.L.GetLocalizedString("subjectAbuseReportUpdated", account.Language),
|
||||
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyAbuseReportUpdated", account.Language), id, status, message),
|
||||
Account: account,
|
||||
AccountID: account.ID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAbuseReport(resource string, reason string, account models.Account) (models.AbuseReport, error) {
|
||||
var report models.AbuseReport
|
||||
if err := database.C.
|
||||
Where(
|
||||
"resource = ? AND account_id = ? AND status IN ?",
|
||||
resource,
|
||||
account.ID,
|
||||
[]string{models.ReportStatusPending, models.ReportStatusReviewing},
|
||||
).First(&report).Error; err == nil {
|
||||
return report, fmt.Errorf("you already reported this resource and it still in process")
|
||||
}
|
||||
|
||||
report = models.AbuseReport{
|
||||
Resource: resource,
|
||||
Reason: reason,
|
||||
Status: models.ReportStatusPending,
|
||||
AccountID: account.ID,
|
||||
}
|
||||
|
||||
err := database.C.Create(&report).Error
|
||||
return report, err
|
||||
}
|
116
pkg/internal/services/statuses.go
Normal file
116
pkg/internal/services/statuses.go
Normal file
@ -0,0 +1,116 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func KgStatusCache(uid uint) string {
|
||||
return fmt.Sprintf("user-status#%d", uid)
|
||||
}
|
||||
|
||||
func GetStatus(uid uint) (models.Status, error) {
|
||||
if val, err := cachekit.Get[models.Status](gap.Ca, KgStatusCache(uid)); err == nil {
|
||||
return val, nil
|
||||
}
|
||||
var status models.Status
|
||||
if err := database.C.
|
||||
Where("account_id = ?", uid).
|
||||
Where("clear_at > ?", time.Now()).
|
||||
First(&status).Error; err != nil {
|
||||
return status, err
|
||||
} else {
|
||||
CacheUserStatus(uid, status)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func CacheUserStatus(uid uint, status models.Status) {
|
||||
cachekit.Set[models.Status](
|
||||
gap.Ca,
|
||||
KgStatusCache(uid),
|
||||
status,
|
||||
time.Minute*5,
|
||||
fmt.Sprintf("user#%d", uid),
|
||||
)
|
||||
}
|
||||
|
||||
func GetUserOnline(uid uint) bool {
|
||||
pc := proto.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
resp, err := pc.CountStreamConnection(ctx, &proto.CountConnectionRequest{
|
||||
UserId: uint64(uid),
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return resp.Count > 0
|
||||
}
|
||||
|
||||
func GetStatusDisturbable(uid uint) error {
|
||||
status, err := GetStatus(uid)
|
||||
isOnline := GetUserOnline(uid)
|
||||
if isOnline && err != nil {
|
||||
return nil
|
||||
} else if err == nil && status.IsNoDisturb {
|
||||
return fmt.Errorf("do not disturb")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetStatusOnline(uid uint) error {
|
||||
status, err := GetStatus(uid)
|
||||
isOnline := GetUserOnline(uid)
|
||||
if isOnline && err != nil {
|
||||
return nil
|
||||
} else if err == nil && status.IsInvisible {
|
||||
return fmt.Errorf("invisible")
|
||||
} else if !isOnline {
|
||||
return fmt.Errorf("offline")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewStatus(user models.Account, status models.Status) (models.Status, error) {
|
||||
if err := database.C.Save(&status).Error; err != nil {
|
||||
return status, err
|
||||
} else {
|
||||
CacheUserStatus(user.ID, status)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func EditStatus(user models.Account, status models.Status) (models.Status, error) {
|
||||
if err := database.C.Save(&status).Error; err != nil {
|
||||
return status, err
|
||||
} else {
|
||||
CacheUserStatus(user.ID, status)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func ClearStatus(user models.Account) error {
|
||||
if err := database.C.
|
||||
Where("account_id = ?", user.ID).
|
||||
Where("clear_at > ?", time.Now()).
|
||||
Updates(models.Status{ClearAt: lo.ToPtr(time.Now())}).Error; err != nil {
|
||||
return err
|
||||
} else {
|
||||
cachekit.Delete(gap.Ca, KgStatusCache(user.ID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
258
pkg/internal/services/ticket.go
Normal file
258
pkg/internal/services/ticket.go
Normal file
@ -0,0 +1,258 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"gorm.io/datatypes"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const InternalTokenAudience = "solar-network"
|
||||
|
||||
// DetectRisk is used for detect user environment is suitable for no multifactorial authenticating or not.
|
||||
// Return the remaining steps, value is from 1 to 2, may appear 3 if user enabled the third-authentication-factor.
|
||||
func DetectRisk(user models.Account, ip, ua string) int {
|
||||
var clue int64
|
||||
if err := database.C.
|
||||
Where(models.AuthTicket{AccountID: user.ID, IpAddress: ip}).
|
||||
Where("available_at IS NOT NULL").
|
||||
Model(models.AuthTicket{}).
|
||||
Count(&clue).Error; err == nil {
|
||||
if clue >= 1 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 3
|
||||
}
|
||||
|
||||
// PickTicketAttempt is trying to pick up the ticket that hasn't completed but created by a same client (identify by ip address).
|
||||
// Then the client can continue their journey to get ticket activated.
|
||||
func PickTicketAttempt(user models.Account, ip string) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.
|
||||
Where("account_id = ? AND ip_address = ? AND expired_at < ? AND available_at IS NULL", user.ID, ip, time.Now()).
|
||||
First(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func NewTicket(user models.Account, ip, ua string) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if ticket, err := PickTicketAttempt(user, ip); err == nil {
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
steps := DetectRisk(user, ip, ua)
|
||||
if count := CountUserFactor(user.ID); count <= 0 {
|
||||
return ticket, fmt.Errorf("specified user didn't enable sign in")
|
||||
} else {
|
||||
steps = min(steps, int(count))
|
||||
|
||||
cfg, err := GetAuthPreference(user)
|
||||
if err == nil && cfg.Config.Data().MaximumAuthSteps >= 1 {
|
||||
steps = min(steps, cfg.Config.Data().MaximumAuthSteps)
|
||||
} else {
|
||||
steps = min(steps, 2)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ticket = models.AuthTicket{
|
||||
Claims: []string{"*"},
|
||||
Audiences: []string{InternalTokenAudience},
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
StepRemain: steps,
|
||||
Location: location,
|
||||
CoordinateX: coordinateX,
|
||||
CoordinateY: coordinateY,
|
||||
ExpiredAt: nil,
|
||||
AvailableAt: nil,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err = database.C.Save(&ticket).Error
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
func NewOauthTicket(
|
||||
user models.Account,
|
||||
client models.ThirdClient,
|
||||
claims, audiences []string,
|
||||
ip, ua string, nonce *string,
|
||||
) (models.AuthTicket, error) {
|
||||
if nonce != nil && len(*nonce) == 0 {
|
||||
nonce = nil
|
||||
}
|
||||
|
||||
ticket := models.AuthTicket{
|
||||
Claims: claims,
|
||||
Audiences: audiences,
|
||||
IpAddress: ip,
|
||||
UserAgent: ua,
|
||||
GrantToken: lo.ToPtr(uuid.NewString()),
|
||||
AccessToken: lo.ToPtr(uuid.NewString()),
|
||||
RefreshToken: lo.ToPtr(uuid.NewString()),
|
||||
AvailableAt: lo.ToPtr(time.Now()),
|
||||
ExpiredAt: lo.ToPtr(time.Now().Add(7 * 24 * time.Hour)),
|
||||
Nonce: nonce,
|
||||
ClientID: &client.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func ActiveTicket(ticket models.AuthTicket) (models.AuthTicket, error) {
|
||||
if ticket.AvailableAt != nil {
|
||||
return ticket, nil
|
||||
} else if err := ticket.IsCanBeAvailble(); err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
} else {
|
||||
var account models.Account
|
||||
if err := database.C.Where("id = ?", ticket.AccountID).Select("Language").First(&account).Error; err != nil {
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
_ = NewNotification(models.Notification{
|
||||
Topic: "passport.security.alert",
|
||||
Title: localize.L.GetLocalizedString("subjectLoginAlert", account.Language),
|
||||
Body: fmt.Sprintf(localize.L.GetLocalizedString("shortBodyLoginAlert", account.Language), ticket.IpAddress),
|
||||
Metadata: datatypes.JSONMap{
|
||||
"ip_address": ticket.IpAddress,
|
||||
"created_at": ticket.CreatedAt,
|
||||
"available_at": ticket.AvailableAt,
|
||||
},
|
||||
AccountID: ticket.AccountID,
|
||||
Priority: 5,
|
||||
})
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func ActiveTicketWithPassword(ticket models.AuthTicket, password string) (models.AuthTicket, error) {
|
||||
if ticket.AvailableAt != nil {
|
||||
return ticket, nil
|
||||
} else if ticket.StepRemain == 1 {
|
||||
return ticket, fmt.Errorf("multi-factor authentication required")
|
||||
}
|
||||
|
||||
factor, err := GetPasswordTypeFactor(ticket.AccountID)
|
||||
if err != nil {
|
||||
return ticket, fmt.Errorf("unable to authenticate, password factor was not found: %v", err)
|
||||
} else if err := CheckFactor(factor, password); err != nil {
|
||||
return ticket, fmt.Errorf("invalid password: %v", err)
|
||||
}
|
||||
|
||||
ticket.StepRemain--
|
||||
ticket.FactorTrail = append(ticket.FactorTrail, int(factor.ID))
|
||||
|
||||
ticket.AvailableAt = lo.ToPtr(time.Now())
|
||||
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func PerformTicketCheck(ticket models.AuthTicket, factor models.AuthFactor, code string) (models.AuthTicket, error) {
|
||||
if ticket.AvailableAt != nil {
|
||||
return ticket, nil
|
||||
} else if ticket.StepRemain <= 0 {
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
if lo.Contains(ticket.FactorTrail, int(factor.ID)) {
|
||||
return ticket, fmt.Errorf("already checked this ticket with factor %d", factor.ID)
|
||||
}
|
||||
|
||||
if err := CheckFactor(factor, code); err != nil {
|
||||
return ticket, fmt.Errorf("invalid code: %v", err)
|
||||
}
|
||||
|
||||
ticket.StepRemain--
|
||||
ticket.FactorTrail = append(ticket.FactorTrail, int(factor.ID))
|
||||
|
||||
if ticket.IsCanBeAvailble() == nil {
|
||||
return ActiveTicket(ticket)
|
||||
} else {
|
||||
if err := database.C.Save(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func RotateTicket(ticket models.AuthTicket, fullyRestart ...bool) (models.AuthTicket, error) {
|
||||
ticket.GrantToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.AccessToken = lo.ToPtr(uuid.NewString())
|
||||
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
|
||||
if len(fullyRestart) > 0 && fullyRestart[0] {
|
||||
ticket.LastGrantAt = nil
|
||||
}
|
||||
err := database.C.Save(&ticket).Error
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
func DoAutoSignoff() {
|
||||
duration := viper.GetDuration("security.auto_signoff") * time.Second
|
||||
deadline := time.Now().Add(-duration)
|
||||
|
||||
log.Debug().Time("before", deadline).Msg("Now signing off tickets...")
|
||||
|
||||
if tx := database.C.
|
||||
Where("last_grant_at < ?", deadline).
|
||||
Delete(&models.AuthTicket{}); tx.Error != nil {
|
||||
log.Error().Err(tx.Error).Msg("An error occurred when running auto sign off...")
|
||||
} else {
|
||||
log.Debug().Int64("affected", tx.RowsAffected).Msg("Auto sign off accomplished.")
|
||||
}
|
||||
}
|
29
pkg/internal/services/ticket_queries.go
Normal file
29
pkg/internal/services/ticket_queries.go
Normal file
@ -0,0 +1,29 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
)
|
||||
|
||||
func GetTicket(id uint) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(&models.AuthTicket{BaseModel: models.BaseModel{ID: id}}).
|
||||
First(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func GetTicketWithToken(tokenId string) (models.AuthTicket, error) {
|
||||
var ticket models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(models.AuthTicket{AccessToken: &tokenId}).
|
||||
Or(models.AuthTicket{RefreshToken: &tokenId}).
|
||||
First(&ticket).Error; err != nil {
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
124
pkg/internal/services/ticket_token.go
Normal file
124
pkg/internal/services/ticket_token.go
Normal file
@ -0,0 +1,124 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetToken(ticket models.AuthTicket) (atk, rtk string, err error) {
|
||||
if err = ticket.IsAvailable(); err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.AccessToken == nil || ticket.RefreshToken == nil {
|
||||
err = fmt.Errorf("unable to encode token, access or refresh token id missing")
|
||||
return
|
||||
}
|
||||
|
||||
atkDeadline := time.Duration(viper.GetInt64("security.access_token_duration")) * time.Second
|
||||
rtkDeadline := time.Duration(viper.GetInt64("security.refresh_token_duration")) * time.Second
|
||||
|
||||
sub := strconv.Itoa(int(ticket.AccountID))
|
||||
sed := strconv.Itoa(int(ticket.ID))
|
||||
atk, err = EncodeJwt(*ticket.AccessToken, JwtAccessType, sub, sed, nil, ticket.Audiences, time.Now().Add(atkDeadline))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rtk, err = EncodeJwt(*ticket.RefreshToken, JwtRefreshType, sub, sed, nil, ticket.Audiences, time.Now().Add(rtkDeadline))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
database.C.Model(&ticket).Update("last_grant_at", time.Now())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ExchangeToken(token string) (atk, rtk string, err error) {
|
||||
var ticket models.AuthTicket
|
||||
if err = database.C.Where(models.AuthTicket{GrantToken: &token}).First(&ticket).Error; err != nil {
|
||||
return
|
||||
} else if ticket.LastGrantAt != nil {
|
||||
err = fmt.Errorf("ticket was granted the first token, use refresh token instead")
|
||||
return
|
||||
} else if len(ticket.Audiences) > 1 {
|
||||
err = fmt.Errorf("should use authorization code grant type")
|
||||
return
|
||||
}
|
||||
|
||||
return GetToken(ticket)
|
||||
}
|
||||
|
||||
func ExchangeOauthToken(clientId, clientSecret, redirectUri, token string) (idk, atk, rtk string, err error) {
|
||||
var client models.ThirdClient
|
||||
if err = database.C.Where(models.ThirdClient{Alias: clientId}).First(&client).Error; err != nil {
|
||||
return
|
||||
} else if client.Secret != clientSecret {
|
||||
err = fmt.Errorf("invalid client secret")
|
||||
return
|
||||
} else if !client.IsDraft && !lo.Contains(client.Callbacks, redirectUri) {
|
||||
err = fmt.Errorf("invalid redirect uri")
|
||||
return
|
||||
}
|
||||
|
||||
var ticket models.AuthTicket
|
||||
if err = database.C.Where(models.AuthTicket{GrantToken: &token}).First(&ticket).Error; err != nil {
|
||||
return
|
||||
} else if ticket.LastGrantAt != nil {
|
||||
err = fmt.Errorf("ticket was granted the first token, use refresh token instead")
|
||||
return
|
||||
}
|
||||
|
||||
atk, rtk, err = GetToken(ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if err = database.C.Where(models.Account{
|
||||
BaseModel: models.BaseModel{ID: ticket.AccountID},
|
||||
}).Preload("Contacts").First(&user).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sub := strconv.Itoa(int(ticket.AccountID))
|
||||
sed := strconv.Itoa(int(ticket.ID))
|
||||
idk, err = EncodeJwt(*ticket.AccessToken, JwtAccessType, sub, sed, ticket.Nonce, ticket.Audiences, time.Now().Add(24*time.Minute), user)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func RefreshToken(token string) (atk, rtk string, err error) {
|
||||
parseInt := func(str string) int {
|
||||
val, _ := strconv.Atoi(str)
|
||||
return val
|
||||
}
|
||||
|
||||
var ticket models.AuthTicket
|
||||
var claims *PayloadClaims
|
||||
if claims, err = sec.ReadJwt(EReader, token, &PayloadClaims{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Type != JwtRefreshType {
|
||||
err = fmt.Errorf("invalid token type, expected refresh token")
|
||||
return
|
||||
} else if err = database.C.Where(models.AuthTicket{
|
||||
BaseModel: models.BaseModel{ID: uint(parseInt(claims.SessionID))},
|
||||
}).First(&ticket).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ticket, err = RotateTicket(ticket); err != nil {
|
||||
return
|
||||
} else {
|
||||
return GetToken(ticket)
|
||||
}
|
||||
}
|
108
pkg/internal/services/tokens.go
Normal file
108
pkg/internal/services/tokens.go
Normal file
@ -0,0 +1,108 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/localize"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func ValidateMagicToken(code string, mode models.MagicTokenType) (models.MagicToken, error) {
|
||||
var tk models.MagicToken
|
||||
if err := database.C.Where(models.MagicToken{Code: code, Type: mode}).First(&tk).Error; err != nil {
|
||||
return tk, err
|
||||
} else if tk.ExpiredAt != nil && time.Now().Unix() >= tk.ExpiredAt.Unix() {
|
||||
return tk, fmt.Errorf("token has been expired")
|
||||
}
|
||||
|
||||
return tk, nil
|
||||
}
|
||||
|
||||
func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expiredAt *time.Time) (models.MagicToken, error) {
|
||||
var uid uint
|
||||
if assignTo != nil {
|
||||
uid = assignTo.ID
|
||||
}
|
||||
|
||||
token := models.MagicToken{
|
||||
Code: strings.Replace(uuid.NewString(), "-", "", -1),
|
||||
Type: mode,
|
||||
AccountID: &uid,
|
||||
ExpiredAt: expiredAt,
|
||||
}
|
||||
|
||||
if err := database.C.Save(&token).Error; err != nil {
|
||||
return token, err
|
||||
} else {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NotifyMagicToken(token models.MagicToken, skipCheck ...bool) error {
|
||||
if token.AccountID == nil {
|
||||
return fmt.Errorf("could notify a non-assign magic token")
|
||||
}
|
||||
if token.LastNotifiedAt != nil && (len(skipCheck) == 0 || !skipCheck[0]) {
|
||||
if token.LastNotifiedAt.Unix() >= time.Now().Add(-60*time.Minute).Unix() {
|
||||
return fmt.Errorf("magic token has been notified in an hour")
|
||||
}
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
BaseModel: models.BaseModel{ID: *token.AccountID},
|
||||
}).Preload("Contacts").First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var subject string
|
||||
var content string
|
||||
switch token.Type {
|
||||
case models.ConfirmMagicToken:
|
||||
link := fmt.Sprintf("%s/flow/accounts/confirm?code=%s", viper.GetString("frontend_app"), token.Code)
|
||||
subject = fmt.Sprintf("[%s] %s", viper.GetString("name"), localize.L.GetLocalizedString("subjectConfirmRegistration", user.Language))
|
||||
content = localize.L.RenderLocalizedTemplateHTML("register-confirm.tmpl", user.Language, map[string]any{
|
||||
"User": user,
|
||||
"Link": link,
|
||||
})
|
||||
case models.ResetPasswordMagicToken:
|
||||
link := fmt.Sprintf("%s/flow/accounts/password-reset?code=%s", viper.GetString("frontend_app"), token.Code)
|
||||
subject = fmt.Sprintf("[%s] %s", viper.GetString("name"), localize.L.GetLocalizedString("subjectResetPassword", user.Language))
|
||||
content = localize.L.RenderLocalizedTemplateHTML("reset-password.tmpl", user.Language, map[string]any{
|
||||
"User": user,
|
||||
"Link": link,
|
||||
})
|
||||
case models.DeleteAccountMagicToken:
|
||||
link := fmt.Sprintf("%s/flow/accounts/deletion?code=%s", viper.GetString("frontend_app"), token.Code)
|
||||
subject = fmt.Sprintf("[%s] %s", viper.GetString("name"), localize.L.GetLocalizedString("subjectDeleteAccount", user.Language))
|
||||
content = localize.L.RenderLocalizedTemplateHTML("confirm-deletion.tmpl", user.Language, map[string]any{
|
||||
"User": user,
|
||||
"Link": link,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("unsupported magic token type to notify")
|
||||
}
|
||||
|
||||
err := gap.Px.PushEmail(pushkit.EmailDeliverRequest{
|
||||
To: user.GetPrimaryEmail().Content,
|
||||
Email: pushkit.EmailData{
|
||||
Subject: subject,
|
||||
HTML: &content,
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
database.C.Model(&token).Update("last_notified_at", time.Now())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
63
pkg/internal/web/admin/badges_api.go
Normal file
63
pkg/internal/web/admin/badges_api.go
Normal file
@ -0,0 +1,63 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func grantBadge(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminGrantBadges", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var account models.Account
|
||||
if account, err = services.GetAccount(data.AccountID); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("target account was not found: %v", err))
|
||||
}
|
||||
|
||||
badge := models.Badge{
|
||||
Type: data.Type,
|
||||
Metadata: data.Metadata,
|
||||
}
|
||||
|
||||
if err := services.GrantBadge(account, badge); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(badge)
|
||||
}
|
||||
}
|
||||
|
||||
func revokeBadge(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminRevokeBadges", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, _ := c.ParamsInt("badgeId", 0)
|
||||
|
||||
var badge models.Badge
|
||||
if err := database.C.Where("id = ?", id).First(&badge).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("target badge was not found: %v", err))
|
||||
}
|
||||
|
||||
if err := services.RevokeBadge(badge); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(badge)
|
||||
}
|
||||
}
|
40
pkg/internal/web/admin/factors_api.go
Normal file
40
pkg/internal/web/admin/factors_api.go
Normal file
@ -0,0 +1,40 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func getUserAuthFactors(c *fiber.Ctx) error {
|
||||
userId, _ := c.ParamsInt("user")
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminAuthFactors", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var factors []models.AuthFactor
|
||||
if err := database.C.Where("account_id = ?", userId).Find(&factors).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
encodedResp := lo.Map(factors, func(item models.AuthFactor, idx int) map[string]any {
|
||||
var encoded map[string]any
|
||||
raw, _ := jsoniter.Marshal(item)
|
||||
_ = jsoniter.Unmarshal(raw, &encoded)
|
||||
|
||||
// Blur out the secret if it isn't current rolling email one-time-password
|
||||
if item.Type != models.EmailPasswordFactor && len(item.Secret) != 6 {
|
||||
encoded["secret"] = "**CENSORED**"
|
||||
} else {
|
||||
encoded["secret"] = item.Secret
|
||||
}
|
||||
|
||||
return encoded
|
||||
})
|
||||
|
||||
return c.JSON(encodedResp)
|
||||
}
|
22
pkg/internal/web/admin/index.go
Normal file
22
pkg/internal/web/admin/index.go
Normal file
@ -0,0 +1,22 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func MapControllers(app *fiber.App, baseURL string) {
|
||||
admin := app.Group(baseURL)
|
||||
{
|
||||
admin.Post("/badges", grantBadge)
|
||||
admin.Delete("/badges/:badgeId", revokeBadge)
|
||||
|
||||
admin.Post("/notify/all", notifyAllUser)
|
||||
admin.Post("/notify/:user", notifyOneUser)
|
||||
|
||||
admin.Get("/users", listUser)
|
||||
admin.Get("/users/:user", getUser)
|
||||
admin.Get("/users/:user/factors", getUserAuthFactors)
|
||||
admin.Put("/users/:user/permissions", editUserPermission)
|
||||
admin.Post("/users/:user/confirm", forceConfirmAccount)
|
||||
}
|
||||
}
|
121
pkg/internal/web/admin/notify_api.go
Normal file
121
pkg/internal/web/admin/notify_api.go
Normal file
@ -0,0 +1,121 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func notifyAllUser(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
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"`
|
||||
Priority int `json:"priority"`
|
||||
IsRealtime bool `json:"is_realtime"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminNotifyAll", true); err != nil {
|
||||
return err
|
||||
}
|
||||
operator := c.Locals("user").(models.Account)
|
||||
|
||||
var users []models.Account
|
||||
if err := database.C.Find(&users).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddAuditRecord(operator, "notify.all", c.IP(), c.Get(fiber.HeaderUserAgent), map[string]any{
|
||||
"payload": data,
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
for _, user := range users {
|
||||
notification := models.Notification{
|
||||
Topic: data.Topic,
|
||||
Subtitle: data.Subtitle,
|
||||
Title: data.Title,
|
||||
Body: data.Body,
|
||||
Metadata: data.Metadata,
|
||||
Priority: data.Priority,
|
||||
Account: user,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
if data.IsRealtime {
|
||||
if err := services.PushNotification(notification); err != nil {
|
||||
log.Error().Err(err).Uint("user", user.ID).Msg("Failed to push notification...")
|
||||
}
|
||||
} else {
|
||||
if err := services.NewNotification(notification); err != nil {
|
||||
log.Error().Err(err).Uint("user", user.ID).Msg("Failed to create notification...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func notifyOneUser(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
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"`
|
||||
Priority int `json:"priority"`
|
||||
IsRealtime bool `json:"is_realtime"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminNotifyAll", true); err != nil {
|
||||
return err
|
||||
}
|
||||
operator := c.Locals("user").(models.Account)
|
||||
|
||||
userId, _ := c.ParamsInt("user", 0)
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddAuditRecord(operator, "notify.one", c.IP(), c.Get(fiber.HeaderUserAgent), map[string]any{
|
||||
"user_id": user.ID,
|
||||
"payload": data,
|
||||
})
|
||||
}
|
||||
|
||||
notification := models.Notification{
|
||||
Topic: data.Topic,
|
||||
Subtitle: data.Subtitle,
|
||||
Title: data.Title,
|
||||
Body: data.Body,
|
||||
Priority: data.Priority,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
if data.IsRealtime {
|
||||
if err := services.PushNotification(notification); err != nil {
|
||||
log.Error().Err(err).Uint("user", user.ID).Msg("Failed to push notification...")
|
||||
}
|
||||
} else {
|
||||
if err := services.NewNotification(notification); err != nil {
|
||||
log.Error().Err(err).Uint("user", user.ID).Msg("Failed to create notification...")
|
||||
}
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
50
pkg/internal/web/admin/permissions_api.go
Normal file
50
pkg/internal/web/admin/permissions_api.go
Normal file
@ -0,0 +1,50 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func editUserPermission(c *fiber.Ctx) error {
|
||||
userId, _ := c.ParamsInt("user")
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminUserPermission", true); err != nil {
|
||||
return err
|
||||
}
|
||||
operator := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
PermNodes map[string]any `json:"perm_nodes" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err))
|
||||
}
|
||||
|
||||
prev := user.PermNodes
|
||||
user.PermNodes = data.PermNodes
|
||||
|
||||
services.InvalidUserAuthCache(user.ID)
|
||||
|
||||
if err := database.C.Save(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddAuditRecord(operator, "user.permissions.edit", c.IP(), c.Get(fiber.HeaderUserAgent), map[string]any{
|
||||
"user_id": user.ID,
|
||||
"previous_permissions": prev,
|
||||
"new_permissions": data.PermNodes,
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
72
pkg/internal/web/admin/users_api.go
Normal file
72
pkg/internal/web/admin/users_api.go
Normal file
@ -0,0 +1,72 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listUser(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminUser", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := database.C.Model(&models.Account{}).Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
var items []models.Account
|
||||
if err := database.C.Offset(offset).Limit(take).Order("id ASC").Find(&items).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": items,
|
||||
})
|
||||
}
|
||||
|
||||
func getUser(c *fiber.Ctx) error {
|
||||
userId, _ := c.ParamsInt("user")
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminUser", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err))
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
func forceConfirmAccount(c *fiber.Ctx) error {
|
||||
userId, _ := c.ParamsInt("user")
|
||||
|
||||
if err := exts.EnsureGrantedPerm(c, "AdminUserConfirmation", true); err != nil {
|
||||
return err
|
||||
}
|
||||
operator := c.Locals("user").(models.Account)
|
||||
|
||||
var user models.Account
|
||||
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err))
|
||||
}
|
||||
|
||||
if err := services.ForceConfirmAccount(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddAuditRecord(operator, "user.confirm", c.IP(), c.Get(fiber.HeaderUserAgent), map[string]any{
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
321
pkg/internal/web/api/accounts_api.go
Normal file
321
pkg/internal/web/api/accounts_api.go
Normal file
@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
|
||||
"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/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getUserInBatch(c *fiber.Ctx) error {
|
||||
id := c.Query("id")
|
||||
list := strings.Split(id, ",")
|
||||
var nameList []string
|
||||
numericList := lo.Filter(lo.Map(list, func(str string, i int) int {
|
||||
value, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
nameList = append(nameList, str)
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}), func(vak int, idx int) bool {
|
||||
return vak > 0
|
||||
})
|
||||
|
||||
tx := database.C
|
||||
if len(numericList) > 0 {
|
||||
tx = tx.Where("id IN ?", numericList)
|
||||
}
|
||||
if len(nameList) > 0 {
|
||||
tx = tx.Or("name IN ?", nameList)
|
||||
}
|
||||
if len(nameList) == 0 && len(numericList) == 0 {
|
||||
return c.JSON([]models.Account{})
|
||||
}
|
||||
|
||||
var accounts []models.Account
|
||||
if err := tx.
|
||||
Preload("Profile").
|
||||
Preload("Badges", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("badges.is_active DESC, badges.type DESC")
|
||||
}).
|
||||
Find(&accounts).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(accounts)
|
||||
}
|
||||
|
||||
func lookupAccount(c *fiber.Ctx) error {
|
||||
probe := c.Query("probe")
|
||||
if len(probe) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "lookup probe is required")
|
||||
}
|
||||
|
||||
user, err := services.LookupAccount(probe)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
func searchAccount(c *fiber.Ctx) error {
|
||||
probe := c.Query("probe")
|
||||
if len(probe) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "search probe is required")
|
||||
}
|
||||
|
||||
users, err := services.SearchAccount(probe)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(users)
|
||||
}
|
||||
|
||||
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())
|
||||
} else {
|
||||
data.PermNodes = c.Locals("nex_user").(*sec.UserInfo).PermNodes
|
||||
}
|
||||
|
||||
var resp fiber.Map
|
||||
raw, _ := jsoniter.Marshal(data)
|
||||
_ = jsoniter.Unmarshal(raw, &resp)
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
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"`
|
||||
Description string `json:"description"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Location string `json:"location"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
Gender string `json:"gender"`
|
||||
Pronouns string `json:"pronouns"`
|
||||
Links map[string]string `json:"links"`
|
||||
Birthday time.Time `json:"birthday"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else {
|
||||
data.Nick = strings.TrimSpace(data.Nick)
|
||||
}
|
||||
if !services.ValidateAccountName(data.Nick, 1, 24) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid account nick, length requires 4 to 24")
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
links := make(map[string]any)
|
||||
for k, v := range data.Links {
|
||||
links[k] = v
|
||||
}
|
||||
|
||||
account.Nick = data.Nick
|
||||
account.Profile.Gender = data.Gender
|
||||
account.Profile.Pronouns = data.Pronouns
|
||||
account.Profile.Location = data.Location
|
||||
account.Profile.TimeZone = data.TimeZone
|
||||
account.Profile.Links = links
|
||||
account.Profile.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.AddEvent(user.ID, "profile.edit", nil, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
services.InvalidUserAuthCache(account.ID)
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func updateAccountLanguage(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Language string `json:"language" validate:"required,bcp47_language_tag"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := database.C.Model(&models.Account{}).Where("id = ?", user.ID).
|
||||
Updates(&models.Account{Language: data.Language}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
services.AddEvent(user.ID, "profile.edit.language", nil, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
services.InvalidUserAuthCache(user.ID)
|
||||
|
||||
user.Language = data.Language
|
||||
|
||||
return c.JSON(user)
|
||||
}
|
||||
|
||||
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"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=4,max=32"`
|
||||
Language string `json:"language" validate:"required,bcp47_language_tag"`
|
||||
CaptchaToken string `json:"captcha_token" validate:"required"`
|
||||
MagicToken string `json:"magic_token"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else {
|
||||
data.Name = strings.TrimSpace(data.Name)
|
||||
data.Nick = strings.TrimSpace(data.Nick)
|
||||
data.Email = strings.TrimSpace(data.Email)
|
||||
}
|
||||
if _, err := strconv.Atoi(data.Name); err == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid account name, cannot be pure number")
|
||||
}
|
||||
if !services.ValidateAccountName(data.Nick, 1, 24) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid account nick, length requires 4 to 24")
|
||||
}
|
||||
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 !gap.Nx.ValidateCaptcha(data.CaptchaToken, c.IP()) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "captcha check failed")
|
||||
}
|
||||
|
||||
if user, err := services.CreateAccount(
|
||||
data.Name,
|
||||
data.Nick,
|
||||
data.Email,
|
||||
data.Password,
|
||||
data.Language,
|
||||
); 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)
|
||||
}
|
||||
|
||||
func reNotifyRegisterConfirm(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var magicToken models.MagicToken
|
||||
if err := database.C.Where("account_id = ? AND type = ?", user.ID, models.ConfirmMagicToken).First(&magicToken).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := services.NotifyMagicToken(magicToken); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func requestDeleteAccount(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
if err := services.CheckAbleToDeleteAccount(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else if err = services.RequestDeleteAccount(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func confirmDeleteAccount(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.ConfirmDeleteAccount(data.Code); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
161
pkg/internal/web/api/auth_api.go
Normal file
161
pkg/internal/web/api/auth_api.go
Normal file
@ -0,0 +1,161 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
|
||||
)
|
||||
|
||||
func getTicket(c *fiber.Ctx) error {
|
||||
ticketId, err := c.ParamsInt("ticketId")
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "ticket id is required")
|
||||
}
|
||||
|
||||
ticket, err := services.GetTicket(uint(ticketId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket %d not found", ticketId))
|
||||
} else {
|
||||
return c.JSON(ticket)
|
||||
}
|
||||
}
|
||||
|
||||
func doAuthenticate(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
} else if user.SuspendedAt != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "account was suspended")
|
||||
} else if err := services.CheckLoginAbility(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable() == nil,
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func doAuthTicketCheck(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
TicketID uint `json:"ticket_id" validate:"required"`
|
||||
FactorID uint `json:"factor_id" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := services.GetTicket(data.TicketID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
factor, err := services.GetFactor(data.FactorID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("factor was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err = services.PerformTicketCheck(ticket, factor, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to authenticate: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable() == nil,
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func getToken(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||
ClientID string `json:"client_id" form:"client_id"`
|
||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||
GrantType string `json:"grant_type" form:"grant_type"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var idk, atk, rtk string
|
||||
switch data.GrantType {
|
||||
case "refresh_token":
|
||||
// Refresh Token
|
||||
atk, rtk, err = services.RefreshToken(data.RefreshToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "authorization_code":
|
||||
// Authorization Code Mode
|
||||
idk, atk, rtk, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "password":
|
||||
// Password Mode
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
}
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
|
||||
} else if err := ticket.IsAvailable(); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("risk detected: %v (ticketId=%d)", err, ticket.ID))
|
||||
}
|
||||
idk, atk, rtk, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, *ticket.GrantToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "grant_token":
|
||||
// Internal Usage
|
||||
atk, rtk, err = services.ExchangeToken(data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
|
||||
}
|
||||
|
||||
if len(idk) == 0 {
|
||||
idk = atk
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"id_token": idk,
|
||||
"access_token": atk,
|
||||
"refresh_token": rtk,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": (30 * time.Minute).Seconds(),
|
||||
})
|
||||
}
|
110
pkg/internal/web/api/avatar_api.go
Normal file
110
pkg/internal/web/api/avatar_api.go
Normal file
@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/paperclip/pkg/filekit"
|
||||
"git.solsynth.dev/hypernet/paperclip/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/gap"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/services"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
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 string `json:"attachment" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
og := user.Avatar
|
||||
if err := database.C.Model(&user).Update("avatar", data.AttachmentID).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddEvent(user.ID, "profile.edit.avatar", nil, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
services.InvalidUserAuthCache(user.ID)
|
||||
}
|
||||
|
||||
if og != nil && len(*og) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &proto.UpdateUsageRequest{
|
||||
Rid: []string{*og},
|
||||
Delta: -1,
|
||||
})
|
||||
}
|
||||
filekit.CountAttachmentUsage(gap.Nx, &proto.UpdateUsageRequest{
|
||||
Rid: []string{*user.Avatar},
|
||||
Delta: 1,
|
||||
})
|
||||
|
||||
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 string `json:"attachment" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
og := user.Banner
|
||||
if err := database.C.Model(&user).Update("banner", data.AttachmentID).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.AddEvent(user.ID, "profile.edit.banner", nil, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
services.InvalidUserAuthCache(user.ID)
|
||||
}
|
||||
|
||||
if og != nil && len(*og) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &proto.UpdateUsageRequest{
|
||||
Rid: []string{*og},
|
||||
Delta: -1,
|
||||
})
|
||||
}
|
||||
filekit.CountAttachmentUsage(gap.Nx, &proto.UpdateUsageRequest{
|
||||
Rid: []string{*user.Banner},
|
||||
Delta: 1,
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
42
pkg/internal/web/api/badges_api.go
Normal file
42
pkg/internal/web/api/badges_api.go
Normal file
@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"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/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listUserBadge(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var badges []models.Badge
|
||||
if err := database.C.Where("account_id = ?", user.ID).Find(&badges).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(badges)
|
||||
}
|
||||
|
||||
func activeUserBadge(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
badgeId, _ := c.ParamsInt("badgeId", 0)
|
||||
|
||||
var badge models.Badge
|
||||
if err := database.C.Where("id = ? AND account_id = ?", badgeId, user.ID).First(&badge).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := services.ActiveBadge(badge); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
219
pkg/internal/web/api/bot_token_api.go
Normal file
219
pkg/internal/web/api/bot_token_api.go
Normal file
@ -0,0 +1,219 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func listBotKeys(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var tx *gorm.DB
|
||||
|
||||
botId, _ := c.ParamsInt("botId", 0)
|
||||
if botId > 0 {
|
||||
var bot models.Account
|
||||
if err := database.C.Where("automated_id = ? AND id = ?", user.ID, botId).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("bot not found: %v", err))
|
||||
}
|
||||
tx = database.C.Where("account_id = ?", bot.ID)
|
||||
} else {
|
||||
tx = database.C.Where("account_id = ?", user.ID)
|
||||
}
|
||||
|
||||
countTx := tx
|
||||
var count int64
|
||||
if err := countTx.Model(&models.ApiKey{}).Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var keys []models.ApiKey
|
||||
if err := tx.Preload("Ticket").Find(&keys).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": keys,
|
||||
})
|
||||
}
|
||||
|
||||
func getBotKey(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
id, _ := c.ParamsInt("id", 0)
|
||||
|
||||
var key models.ApiKey
|
||||
if err := database.C.
|
||||
Where("id = ? AND account_id = ?", id, user.ID).
|
||||
Preload("Ticket").
|
||||
First(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(key)
|
||||
}
|
||||
|
||||
func createBotKey(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
Lifecycle *int64 `json:"lifecycle"`
|
||||
Claims []string `json:"claims"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := user
|
||||
|
||||
botId, _ := c.ParamsInt("botId", 0)
|
||||
if botId > 0 {
|
||||
var bot models.Account
|
||||
if err := database.C.Where("automated_id = ? AND id = ?", user.ID, botId).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("bot not found: %v", err))
|
||||
}
|
||||
target = bot
|
||||
}
|
||||
|
||||
key, err := services.NewApiKey(target, models.ApiKey{
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
Lifecycle: data.Lifecycle,
|
||||
}, c.IP(), c.Get(fiber.HeaderUserAgent), data.Claims)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(key)
|
||||
}
|
||||
|
||||
func editBotKey(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
Lifecycle *int64 `json:"lifecycle"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, _ := c.ParamsInt("id", 0)
|
||||
|
||||
var tx *gorm.DB
|
||||
|
||||
botId, _ := c.ParamsInt("botId", 0)
|
||||
if botId > 0 {
|
||||
var bot models.Account
|
||||
if err := database.C.Where("automated_id = ? AND id = ?", user.ID, botId).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("bot not found: %v", err))
|
||||
}
|
||||
tx = database.C.Where("account_id = ?", bot.ID)
|
||||
} else {
|
||||
tx = database.C.Where("account_id = ?", user.ID)
|
||||
}
|
||||
|
||||
var key models.ApiKey
|
||||
if err := tx.Where("id = ?", id).First(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
key.Name = data.Name
|
||||
key.Description = data.Description
|
||||
key.Lifecycle = data.Lifecycle
|
||||
|
||||
if err := database.C.Save(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(key)
|
||||
}
|
||||
|
||||
func rollBotKey(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
id, _ := c.ParamsInt("id", 0)
|
||||
|
||||
var tx *gorm.DB
|
||||
|
||||
botId, _ := c.ParamsInt("botId", 0)
|
||||
if botId > 0 {
|
||||
var bot models.Account
|
||||
if err := database.C.Where("automated_id = ? AND id = ?", user.ID, botId).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("bot not found: %v", err))
|
||||
}
|
||||
tx = database.C.Where("account_id = ?", bot.ID)
|
||||
} else {
|
||||
tx = database.C.Where("account_id = ?", user.ID)
|
||||
}
|
||||
|
||||
var key models.ApiKey
|
||||
if err := tx.Where("id = ?", id).First(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if key, err := services.RollApiKey(key); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(key)
|
||||
}
|
||||
}
|
||||
|
||||
func revokeBotKey(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
id, _ := c.ParamsInt("id", 0)
|
||||
|
||||
var tx *gorm.DB
|
||||
|
||||
botId, _ := c.ParamsInt("botId", 0)
|
||||
if botId > 0 {
|
||||
var bot models.Account
|
||||
if err := database.C.Where("automated_id = ? AND id = ?", user.ID, botId).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("bot not found: %v", err))
|
||||
}
|
||||
tx = database.C.Where("account_id = ?", bot.ID)
|
||||
} else {
|
||||
tx = database.C.Where("account_id = ?", user.ID)
|
||||
}
|
||||
|
||||
var key models.ApiKey
|
||||
if err := tx.Where("id = ?", id).First(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.Delete(&key).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(key)
|
||||
}
|
101
pkg/internal/web/api/bots_api.go
Normal file
101
pkg/internal/web/api/bots_api.go
Normal file
@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/datatypes"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func listBots(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
tx := database.C.Where("automated_id = ?", user.ID)
|
||||
|
||||
countTx := tx
|
||||
var count int64
|
||||
if err := countTx.Model(&models.Account{}).Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var bots []models.Account
|
||||
if err := tx.Find(&bots).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": bots,
|
||||
})
|
||||
}
|
||||
|
||||
func createBot(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
cnt, _ := services.GetBotCount(user)
|
||||
if err := exts.EnsureGrantedPerm(c, "CreateBots", cnt+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Name string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
|
||||
Nick string `json:"nick" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else {
|
||||
data.Name = strings.TrimSpace(data.Name)
|
||||
data.Nick = strings.TrimSpace(data.Nick)
|
||||
}
|
||||
|
||||
if !services.ValidateAccountName(data.Nick, 4, 24) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid bot nick, length requires 4 to 24")
|
||||
}
|
||||
|
||||
bot, err := services.NewBot(user, models.Account{
|
||||
Name: data.Name,
|
||||
Nick: data.Nick,
|
||||
Description: data.Description,
|
||||
ConfirmedAt: lo.ToPtr(time.Now()),
|
||||
PermNodes: datatypes.JSONMap{},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(bot)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteBot(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
id, _ := c.ParamsInt("botId", 0)
|
||||
|
||||
var bot models.Account
|
||||
if err := database.C.Where("id = ? AND automated_id = ?", id, user.ID).First(&bot).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.DeleteAccount(bot.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(bot)
|
||||
}
|
118
pkg/internal/web/api/check_in_api.go
Normal file
118
pkg/internal/web/api/check_in_api.go
Normal file
@ -0,0 +1,118 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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/passport/pkg/internal/services"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listCheckInRecord(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)
|
||||
|
||||
var count int64
|
||||
if err := database.C.
|
||||
Model(&models.CheckInRecord{}).
|
||||
Where("account_id = ?", user.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var records []models.CheckInRecord
|
||||
if err := database.C.
|
||||
Where("account_id = ?", user.ID).
|
||||
Limit(take).Offset(offset).
|
||||
Order("created_at DESC").
|
||||
Find(&records).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": records,
|
||||
})
|
||||
}
|
||||
|
||||
func listOtherUserCheckInRecord(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
alias := c.Params("alias")
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{Name: alias}).
|
||||
First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := database.C.
|
||||
Model(&models.CheckInRecord{}).
|
||||
Where("account_id = ?", account.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var records []models.CheckInRecord
|
||||
if err := database.C.
|
||||
Where("account_id = ?", account.ID).
|
||||
Limit(take).Offset(offset).
|
||||
Order("created_at DESC").
|
||||
Find(&records).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": records,
|
||||
})
|
||||
}
|
||||
|
||||
func getTodayCheckIn(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
if record, err := services.GetTodayCheckIn(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(record)
|
||||
}
|
||||
}
|
||||
|
||||
func doCheckIn(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
CaptchaToken string `json:"captcha_token" validate:"required"`
|
||||
}
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gap.Nx.ValidateCaptcha(data.CaptchaToken, c.IP()) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "captcha check failed")
|
||||
}
|
||||
|
||||
if record, err := services.CheckIn(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
services.AddEvent(user.ID, "checkIn", map[string]any{
|
||||
"check_in_record": record,
|
||||
}, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
return c.JSON(record)
|
||||
}
|
||||
}
|
130
pkg/internal/web/api/contacts_api.go
Normal file
130
pkg/internal/web/api/contacts_api.go
Normal file
@ -0,0 +1,130 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listContact(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var contacts []models.AccountContact
|
||||
if err := database.C.Where("account_id = ?", user.ID).Find(&contacts).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(contacts)
|
||||
}
|
||||
|
||||
func getContact(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
contactId, _ := c.ParamsInt("contactId")
|
||||
|
||||
var contact models.AccountContact
|
||||
if err := database.C.Where("account_id = ? AND id = ?", user.ID, contactId).First(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(contact)
|
||||
}
|
||||
|
||||
func createContact(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Type int8 `json:"type"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contact := models.AccountContact{
|
||||
Type: data.Type,
|
||||
Content: data.Content,
|
||||
IsPublic: data.IsPublic,
|
||||
IsPrimary: false,
|
||||
VerifiedAt: nil,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
if err := database.C.Create(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(contact)
|
||||
}
|
||||
|
||||
func updateContact(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
contactId, _ := c.ParamsInt("contactId")
|
||||
|
||||
var data struct {
|
||||
Type int8 `json:"type"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contact models.AccountContact
|
||||
if err := database.C.Where("account_id = ? AND id = ?", user.ID, contactId).First(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
contact.Type = data.Type
|
||||
contact.IsPublic = data.IsPublic
|
||||
if contact.Content != data.Content {
|
||||
contact.Content = data.Content
|
||||
contact.VerifiedAt = nil
|
||||
}
|
||||
|
||||
if err := database.C.Save(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(contact)
|
||||
}
|
||||
|
||||
func deleteContact(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
contactId, _ := c.ParamsInt("contactId")
|
||||
|
||||
var contact models.AccountContact
|
||||
if err := database.C.Where("account_id = ? AND id = ?", user.ID, contactId).First(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if contact.IsPrimary {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "cannot delete primary contact")
|
||||
}
|
||||
|
||||
if err := database.C.Delete(&contact).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
40
pkg/internal/web/api/events_api.go
Normal file
40
pkg/internal/web/api/events_api.go
Normal file
@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
167
pkg/internal/web/api/factors_api.go
Normal file
167
pkg/internal/web/api/factors_api.go
Normal file
@ -0,0 +1,167 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getAvailableFactors(c *fiber.Ctx) error {
|
||||
ticketId := c.QueryInt("ticketId", 0)
|
||||
if ticketId <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "must provide ticket id as a query parameter")
|
||||
}
|
||||
|
||||
ticket, err := services.GetTicket(uint(ticketId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err))
|
||||
}
|
||||
factors, err := services.ListUserFactor(ticket.AccountID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(factors)
|
||||
}
|
||||
|
||||
func requestFactorToken(c *fiber.Ctx) error {
|
||||
id, _ := c.ParamsInt("factorId", 0)
|
||||
|
||||
factor, err := services.GetFactor(uint(id))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if sent, err := services.GetFactorCode(factor, c.IP()); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if !sent {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func listFactor(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var factors []models.AuthFactor
|
||||
if err := database.C.Where("account_id = ?", user.ID).Find(&factors).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(factors)
|
||||
}
|
||||
|
||||
func createFactor(c *fiber.Ctx) error {
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Type models.AuthFactorType `json:"type"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typeWhitelist := []models.AuthFactorType{
|
||||
models.EmailPasswordFactor,
|
||||
models.InAppNotifyFactor,
|
||||
models.TimeOtpFactor,
|
||||
}
|
||||
if !lo.Contains(typeWhitelist, data.Type) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid factor type")
|
||||
}
|
||||
|
||||
// Currently, each type of factor can only be created once
|
||||
var currentCount int64
|
||||
if err := database.C.Model(&models.AuthFactor{}).
|
||||
Where("account_id = ? AND type = ?", user.ID, data.Type).
|
||||
Count(¤tCount).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("unable to check current factor count: %v", err))
|
||||
} else if currentCount > 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "this type of factor already exists")
|
||||
}
|
||||
|
||||
factor := models.AuthFactor{
|
||||
Type: data.Type,
|
||||
Secret: data.Secret,
|
||||
Account: user,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
additionalOnceConfig := map[string]any{}
|
||||
|
||||
switch data.Type {
|
||||
case models.TimeOtpFactor:
|
||||
cfg := totp.GenerateOpts{
|
||||
Issuer: viper.GetString("name"),
|
||||
AccountName: user.Name,
|
||||
Period: 30,
|
||||
SecretSize: 20,
|
||||
Digits: 6,
|
||||
}
|
||||
key, err := totp.Generate(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate totp key: %v", err)
|
||||
}
|
||||
factor.Secret = key.Secret()
|
||||
factor.Config = map[string]any{
|
||||
"issuer": cfg.Issuer,
|
||||
"account_name": cfg.AccountName,
|
||||
"period": cfg.Period,
|
||||
"secret_size": cfg.SecretSize,
|
||||
"digits": cfg.Digits,
|
||||
}
|
||||
additionalOnceConfig["url"] = key.URL()
|
||||
}
|
||||
|
||||
if err := database.C.Create(&factor).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if len(additionalOnceConfig) > 0 {
|
||||
for k, v := range additionalOnceConfig {
|
||||
factor.Config[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(factor)
|
||||
}
|
||||
|
||||
func deleteFactor(c *fiber.Ctx) error {
|
||||
id, _ := c.ParamsInt("factorId", 0)
|
||||
|
||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(models.Account)
|
||||
|
||||
var factor models.AuthFactor
|
||||
if err := database.C.Where("id = ? AND account_id = ?", id, user.ID).First(&factor).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if factor.Type == models.PasswordAuthFactor {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "unable to delete password factor")
|
||||
}
|
||||
|
||||
if err := database.C.Delete(&factor).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user