Compare commits
342 Commits
6cdee57a18
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
313f9fec43
|
|||
|
337ce15b37
|
|||
|
37f06fe98a
|
|||
|
79fbbd1862
|
|||
|
e8571492f0
|
|||
|
f17c88547b
|
|||
|
5439b6d370
|
|||
|
78fba57f94
|
|||
|
fa50f5c771
|
|||
|
47745bbba8
|
|||
|
e1709f8e50
|
|||
|
11388c4f35
|
|||
|
b4e32d5a6d
|
|||
|
170afb4a3b
|
|||
|
9f511c0682
|
|||
|
fec6fa7312
|
|||
|
d230d7474e
|
|||
|
545facba22
|
|||
|
550254b844
|
|||
|
cdd25236e4
|
|||
|
ee1ff5a550
|
|||
|
1a1f7ddaa9
|
|||
|
6ac2ce1197
|
|||
|
3c5e365e1a
|
|||
|
73ca60e1ce
|
|||
|
25a2bf75c5
|
|||
|
1a5deabadb
|
|||
|
17483d31fe
|
|||
|
6a890ab17f
|
|||
|
0aa39ef1f4
|
|||
|
7dc301e9f4
|
|||
|
e6fc2f6130
|
|||
|
a315eea087
|
|||
|
79ccd0036e
|
|||
|
e7df62e673
|
|||
|
83e62ba825
|
|||
|
fc29d62a00
|
|||
|
bd23a53fbb
|
|||
|
0e51c1ee39
|
|||
|
afbecff995
|
|||
|
c90c8da62e
|
|||
|
cd612ab24d
|
|||
|
eddc23a2e8
|
|||
|
c43c37a127
|
|||
|
5cf00407b4
|
|||
|
f4a5b37892
|
|||
|
3c4e078bdd
|
|||
|
6411268090
|
|||
|
0fc57ac637
|
|||
|
c9e987e2ba
|
|||
|
b2f216f1bd
|
|||
|
67e2cbbd04
|
|||
|
45159484d9
|
|||
| 7afc6ec25e | |||
| 2c22c0ec5c | |||
| 33dc448871 | |||
| 69a7756886 | |||
| f793a7516f | |||
| b7ac942807 | |||
| cee71097af | |||
| e5c12b4cfe | |||
| e3df4fcf42 | |||
| 7ad479bc87 | |||
| ff10fe10ce | |||
| 22fdcd2020 | |||
|
a1cac494dc
|
|||
|
afd37c620a
|
|||
|
f1d47a53d3
|
|||
|
8566334f59
|
|||
|
4f0b4262ed
|
|||
|
a7a6b7aa4e
|
|||
|
42fdceaf88
|
|||
|
2af9d23aba
|
|||
|
e8406f731e
|
|||
|
050504ade6
|
|||
|
6504c20708
|
|||
|
|
2ad3ba2400
|
||
|
99424ee55f
|
|||
|
9c945d69a9
|
|||
|
f5a7fa3551
|
|||
|
8f1d5280f7
|
|||
|
0ac96ab3e6
|
|||
|
a2eb882398
|
|||
|
7536fdc1ac
|
|||
|
287f315c00
|
|||
|
1504954be4
|
|||
|
82c476fa80
|
|||
|
5c6f19e8b6
|
|||
|
83cec316bc
|
|||
|
304bf0f50d
|
|||
|
6e88597af0
|
|||
|
8c90837a67
|
|||
|
c05724a9ee
|
|||
|
cbc358b96e
|
|||
|
392a15c849
|
|||
|
1d885feb1f
|
|||
|
70d1544cfe
|
|||
|
8938fa052b
|
|||
|
c9775bcd8b
|
|||
|
4715e49533
|
|||
|
e2a8abba34
|
|||
|
39f555b780
|
|||
|
2aa344a11f
|
|||
|
88a14bfced
|
|||
|
b70095c99e
|
|||
|
5da6e9ce25
|
|||
|
3b39141bf0
|
|||
|
9016b21464
|
|||
|
12a02d13dc
|
|||
|
83bd6c2830
|
|||
|
f27b991d69
|
|||
|
0f1c5b1293
|
|||
|
fabba842ce
|
|||
|
5ece89268f
|
|||
|
b8c89fcf5f
|
|||
|
65f86a8156
|
|||
|
a0f6087d3e
|
|||
|
f898243de5
|
|||
|
8d8bfa3db5
|
|||
|
220b4d2ea3
|
|||
|
2a0788ea86
|
|||
|
3ac1f4165f
|
|||
|
44a97c6d0f
|
|||
|
c75423bf84
|
|||
|
937f382f93
|
|||
|
654b196bfd
|
|||
|
f7bde8ef2e
|
|||
|
732d9866db
|
|||
|
330b037dca
|
|||
|
79dfa8499c
|
|||
|
89e7f1a41a
|
|||
|
e3c0b60337
|
|||
|
140a3070d6
|
|||
|
a56333fda8
|
|||
|
2b5f55f359
|
|||
|
e480bd6548
|
|||
|
9fb67ce2be
|
|||
|
1c7192db17
|
|||
|
9ded703143
|
|||
|
3f535a8249
|
|||
|
18fa741e4d
|
|||
|
4cd4a8cae6
|
|||
|
d90e22b641
|
|||
|
4f7632af53
|
|||
|
ca080f4e2a
|
|||
|
5a5239e335
|
|||
|
314995e5f9
|
|||
|
8e11ba4631
|
|||
|
dfd5532b20
|
|||
|
986f63c0af
|
|||
|
154c929859
|
|||
|
f779435cf0
|
|||
|
5f6eb9f2a2
|
|||
|
3f44d2d9c2
|
|||
|
b8f89ab655
|
|||
|
83df018d34
|
|||
|
7b3fe24b7c
|
|||
|
75c4edfa3d
|
|||
|
a060901cc3
|
|||
|
8e41514d05
|
|||
|
9aff7d8f26
|
|||
|
2f26b2ddb5
|
|||
|
96d76b3657
|
|||
|
4e45a9b6d0
|
|||
|
27ac4d9b4a
|
|||
|
a60a796345
|
|||
|
14f50ecdb2
|
|||
|
b1c78dce28
|
|||
|
585ec46282
|
|||
|
8f69b61799
|
|||
|
64bab332c9
|
|||
|
38401a5f69
|
|||
|
f03d472c30
|
|||
|
2d6f6700f0
|
|||
|
2e11fc5d9c
|
|||
|
ac428946e7
|
|||
|
e4329dfc2b
|
|||
|
5dbbdc62e6
|
|||
|
200614a5c9
|
|||
|
4ac5b1c101
|
|||
|
b7e6009706
|
|||
|
fd262239e4
|
|||
|
cf761d218d
|
|||
|
110627f27e
|
|||
|
64392c32c6
|
|||
|
3f8f2547be
|
|||
|
632fa6cf8e
|
|||
|
d04f8cdc44
|
|||
|
97f5677a97
|
|||
|
2ed4a4da02
|
|||
|
100fe32f8e
|
|||
|
231f591767
|
|||
|
0e7aaed154
|
|||
|
89c2d11f19
|
|||
|
cd93491d98
|
|||
|
9b83ab565a
|
|||
|
5e17bbd965
|
|||
|
de0d05df0a
|
|||
|
b2c5f8de38
|
|||
|
ecbb890cac
|
|||
|
63f8439886
|
|||
|
194f1fa1fe
|
|||
|
55afbb29b4
|
|||
|
2e76a4c6a7
|
|||
|
5c540db325
|
|||
|
4cda783fed
|
|||
|
c4951f820a
|
|||
|
a04d562d61
|
|||
|
f0cca0cda4
|
|||
|
087cd4ee51
|
|||
|
164e271d81
|
|||
|
1b2933ba0e
|
|||
|
aa85aab55e
|
|||
|
197d14fb72
|
|||
|
725fd18536
|
|||
|
ea28436628
|
|||
|
7e37b92f24
|
|||
|
7edcda544b
|
|||
|
b8a2e24bd0
|
|||
|
8e792ced68
|
|||
|
a80c3cd1dd
|
|||
|
67e22eb793
|
|||
|
aaedddfd2f
|
|||
|
f8a3d0ca45
|
|||
|
6a9c013799
|
|||
|
70846e0d1e
|
|||
|
0710ffce72
|
|||
|
9e840901d1
|
|||
|
0f1c8e327e
|
|||
|
ddffb0da23
|
|||
|
b4d0959de4
|
|||
|
c2fd1c5cc8
|
|||
|
eddfa9a884
|
|||
|
b0684492fa
|
|||
|
aea7fddef0
|
|||
|
ef64c29ea7
|
|||
|
5f7f078f02
|
|||
|
1adfda54a6
|
|||
|
3510d6c1f8
|
|||
|
1fa90b15c3
|
|||
|
aa8e57bd89
|
|||
|
d6acae1625
|
|||
|
8dbdb58327
|
|||
|
61d2d2aef3
|
|||
|
0b710fd538
|
|||
|
d70ade4907
|
|||
|
a98ab26fa4
|
|||
|
62da1e096e
|
|||
|
fd1c89392f
|
|||
|
ae93f49691
|
|||
|
743f8373b0
|
|||
|
4796653896
|
|||
|
4dfd4cd529
|
|||
|
bd8eecbc7d
|
|||
|
cbec9bf2b3
|
|||
|
3d685b5a86
|
|||
|
83fe326962
|
|||
|
5b6bc9ce42
|
|||
|
e0e1abab93
|
|||
|
9f927c907a
|
|||
|
27ba3b7bef
|
|||
|
63f71d3b81
|
|||
|
e40d175c8e
|
|||
|
304e1d95ed
|
|||
|
acd3c95c80
|
|||
| 8973d518a2 | |||
| b5b4bb9d66 | |||
|
4c438cf4e4
|
|||
|
d44eef6bb7
|
|||
|
a49450bf9e
|
|||
|
228d838c37
|
|||
| 580402a5c2 | |||
| d46af028dc | |||
| cdcd05ea52 | |||
|
3f05dbe1e6
|
|||
|
7d76b85055
|
|||
|
af66dc6155
|
|||
|
8bafd52f43
|
|||
|
0a861fa674
|
|||
|
a48f5ad2fa
|
|||
|
f89a483380
|
|||
|
fb7ecaffe9
|
|||
|
b3fe91444d
|
|||
|
b6003544c8
|
|||
|
959bb8be0b
|
|||
|
10f148a07f
|
|||
|
e6492eeb94
|
|||
|
e87bda4f33
|
|||
|
afc62f311b
|
|||
|
2b99d415de
|
|||
|
a06248f3be
|
|||
|
81a518a98b
|
|||
|
98e32b67e1
|
|||
|
6681ffccdf
|
|||
|
3dbcc00a2d
|
|||
|
8e43d6699c
|
|||
|
b30d9db69d
|
|||
|
c7cefb3898
|
|||
|
d3d823c85f
|
|||
|
bfeb46a61f
|
|||
|
9e649d83e5
|
|||
|
c672d174f6
|
|||
|
9135edbd60
|
|||
|
5b571f7a84
|
|||
|
3a86d387bd
|
|||
|
32a27d974a
|
|||
|
9e51414a13
|
|||
|
f94220dcc3
|
|||
|
9c7cfb3da6
|
|||
|
942767aed3
|
|||
|
a5a354e929
|
|||
|
43f95ba4af
|
|||
| be3d778420 | |||
| 9ac598cd98 | |||
| 606c74c587 | |||
| e4e15b2f6e | |||
| 1d387a33c5 | |||
| 634c922903 | |||
| 3e9656db23 | |||
| 06c51e599d | |||
| b888bb25b0 | |||
| 44616895cf | |||
| 2148e47b10 | |||
| f5a811a6a2 | |||
| 1302a5ea03 | |||
|
f8b6c1b1df
|
|||
|
396ab10469
|
|||
|
ca08c997c8
|
|||
|
bd726f80ea
|
|||
|
cd2bcd597c
|
|||
|
fd4da4f1a1
|
|||
|
485d0de64b
|
|||
|
b933522123
|
|||
|
2d92d5fba7
|
|||
|
d314942c08
|
|||
|
1505783c62
|
|||
|
f130401ff8
|
|||
|
0fb5c8b758
|
|||
|
dc128c0392
|
|||
|
e2345a8d4a
|
|||
|
55e7d3520a
|
|||
|
b81a43019a
|
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
TZ=Asia/Shanghai
|
||||||
|
LOG_LEVEL=debug
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TZ=Asia/Shanghai
|
||||||
12
.envrc
Normal file
12
.envrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export DIRENV_WARN_TIMEOUT=20s
|
||||||
|
|
||||||
|
eval "$(devenv direnvrc)"
|
||||||
|
|
||||||
|
# `use devenv` supports the same options as the `devenv shell` command.
|
||||||
|
#
|
||||||
|
# To silence all output, use `--quiet`.
|
||||||
|
#
|
||||||
|
# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true
|
||||||
|
use devenv
|
||||||
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
devenv.local.yaml
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
|
# build files
|
||||||
|
.outputs/
|
||||||
|
|
||||||
|
# go binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# test binary
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# profiles and artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# dependency directories
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# editor/ide
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# apple crap
|
||||||
|
.DS_Store
|
||||||
|
__MACOSX
|
||||||
|
._*
|
||||||
|
|
||||||
|
# go gen
|
||||||
|
*_gen.go
|
||||||
|
|
||||||
|
# test files
|
||||||
|
.test/
|
||||||
41
.zed/settings.json
Normal file
41
.zed/settings.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Folder-specific settings
|
||||||
|
//
|
||||||
|
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||||
|
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||||
|
|
||||||
|
{
|
||||||
|
"tab_size": 4,
|
||||||
|
"format_on_save": "on",
|
||||||
|
"languages": {
|
||||||
|
"Nix": {
|
||||||
|
"tab_size": 2,
|
||||||
|
},
|
||||||
|
"TypeScript": {
|
||||||
|
"tab_size": 2,
|
||||||
|
"language_servers": [
|
||||||
|
"typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!deno",
|
||||||
|
"...",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"TSX": {
|
||||||
|
"tab_size": 2,
|
||||||
|
"language_servers": [
|
||||||
|
"typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!deno",
|
||||||
|
"...",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"JavaScript": {
|
||||||
|
"tab_size": 2,
|
||||||
|
"language_servers": [
|
||||||
|
"typescript-language-server",
|
||||||
|
"!vtsls",
|
||||||
|
"!deno",
|
||||||
|
"...",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
13
Containerfile
Normal file
13
Containerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM docker.io/golang:1.25.5-alpine AS backend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
RUN go mod tidy && \
|
||||||
|
go generate . && \
|
||||||
|
go build -o /app/nixcn-cms
|
||||||
|
|
||||||
|
FROM docker.io/alpine:3.23
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=backend-build /app/nixcn-cms /app/nixcn-cms
|
||||||
|
EXPOSE 8000
|
||||||
|
ENTRYPOINT [ "/app/nixcn-cms" ]
|
||||||
23
README.md
23
README.md
@@ -1,2 +1,25 @@
|
|||||||
# nixcn-cms
|
# nixcn-cms
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
1. **Root docs serve the zh-CN version** _[MUST]_
|
||||||
|
2. **Use sign-off via `git commit -s`** _[MUST]_
|
||||||
|
3. **Do not modify the `main` branch for any reason** _[MUST]_
|
||||||
|
4. **Do not omit the commit subject for any reason** _[MUST]_
|
||||||
|
5. **Describe all changes in the commit message** _[MUST]_
|
||||||
|
6. **Rebase before submitting patches** _[MUST]_
|
||||||
|
7. **Commit message written in english** _[MUST]_
|
||||||
|
8. **Use OpenPGP/SSH for commit signing** _[MUST]_
|
||||||
|
9. **Split commits for large or multi-part changes** _[OPTION]_
|
||||||
|
10. **Have fun contributing :)** _[VERY NECESSARY]_
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
- Nix
|
||||||
|
- Devenv
|
||||||
|
- Direnv
|
||||||
|
|
||||||
|
## Notice
|
||||||
|
|
||||||
|
1. Client and all nix files use 2 space tab.
|
||||||
|
2. All Golang files and other configs use 4 space tab.
|
||||||
|
|||||||
18
api/agenda/handler.go
Normal file
18
api/agenda/handler.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package agenda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/service/service_agenda"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgendaHandler struct {
|
||||||
|
svc service_agenda.AgendaService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
agendaSvc := service_agenda.NewAgendaService()
|
||||||
|
agendaHandler := &AgendaHandler{agendaSvc}
|
||||||
|
|
||||||
|
r.POST("/submit", agendaHandler.Submit)
|
||||||
|
}
|
||||||
99
api/agenda/submit.go
Normal file
99
api/agenda/submit.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package agenda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_agenda"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Submit handles the submission of a new agenda item.
|
||||||
|
//
|
||||||
|
// @Summary Submit Agenda
|
||||||
|
// @Description Creates a new agenda item for a specific attendance record.
|
||||||
|
// @Tags Agenda
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body service_agenda.SubmitData true "Agenda Submission Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_agenda.SubmitResponse}
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /agenda/submit [post]
|
||||||
|
func (self *AgendaHandler) Submit(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(service_agenda.SubmitData)
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(data); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.EventId.String() == "" || data.Name == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Submit(&service_agenda.SubmitPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), &result.Data)
|
||||||
|
}
|
||||||
87
api/auth/exchange.go
Normal file
87
api/auth/exchange.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exchange handles the authorization code swap process.
|
||||||
|
//
|
||||||
|
// @Summary Exchange Auth Code
|
||||||
|
// @Description Exchanges client credentials and user session for a specific redirect authorization code.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.ExchangeData true "Exchange Request Credentials"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.ExchangeResponse} "Successful exchange"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/exchange [post]
|
||||||
|
func (self *AuthHandler) Exchange(c *gin.Context) {
|
||||||
|
var exchangeData service_auth.ExchangeData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&exchangeData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUnauthorized).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 401, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Exchange(&service_auth.ExchangePayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &exchangeData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
23
api/auth/handler.go
Normal file
23
api/auth/handler.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
svc service_auth.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
authSvc := service_auth.NewAuthService()
|
||||||
|
authHandler := &AuthHandler{authSvc}
|
||||||
|
|
||||||
|
r.GET("/redirect", authHandler.Redirect)
|
||||||
|
r.POST("/magic", middleware.ApiVersionCheck(), authHandler.Magic)
|
||||||
|
r.POST("/token", middleware.ApiVersionCheck(), authHandler.Token)
|
||||||
|
r.POST("/refresh", middleware.ApiVersionCheck(), authHandler.Refresh)
|
||||||
|
r.POST("/exchange", middleware.ApiVersionCheck(), middleware.JWTAuth(), authHandler.Exchange)
|
||||||
|
}
|
||||||
55
api/auth/magic.go
Normal file
55
api/auth/magic.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Magic handles the "Magic Link" authentication request.
|
||||||
|
//
|
||||||
|
// @Summary Request Magic Link
|
||||||
|
// @Description Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.MagicData true "Magic Link Request Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.MagicResponse} "Successful request"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Turnstile Verification Failed"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/magic [post]
|
||||||
|
func (self *AuthHandler) Magic(c *gin.Context) {
|
||||||
|
var magicData service_auth.MagicData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&magicData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
magicData.ClientIP = c.ClientIP()
|
||||||
|
|
||||||
|
result := self.svc.Magic(&service_auth.MagicPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &magicData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
61
api/auth/redirect.go
Normal file
61
api/auth/redirect.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redirect handles the post-verification callback and redirects the user to the target application.
|
||||||
|
//
|
||||||
|
// @Summary Handle Auth Callback and Redirect
|
||||||
|
// @Description Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept x-www-form-urlencoded
|
||||||
|
// @Produce json
|
||||||
|
// @Produce html
|
||||||
|
// @Param client_id query string true "Client Identifier"
|
||||||
|
// @Param redirect_uri query string true "Target Redirect URI"
|
||||||
|
// @Param code query string true "Temporary Verification Code"
|
||||||
|
// @Param state query string false "Opaque state used to maintain state between the request and callback"
|
||||||
|
// @Success 302 {string} string "Redirect to the provided RedirectUri with a new code"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input / Client Not Found / URI Mismatch"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Verification Code"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/redirect [get]
|
||||||
|
func (self *AuthHandler) Redirect(c *gin.Context) {
|
||||||
|
data := &service_auth.RedirectData{
|
||||||
|
ClientId: c.Query("client_id"),
|
||||||
|
RedirectUri: c.Query("redirect_uri"),
|
||||||
|
State: c.Query("state"),
|
||||||
|
Code: c.Query("code"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ClientId == "" || data.RedirectUri == "" || data.Code == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Redirect(&service_auth.RedirectPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(302, result.Data)
|
||||||
|
}
|
||||||
53
api/auth/refresh.go
Normal file
53
api/auth/refresh.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refresh handles the token rotation process.
|
||||||
|
//
|
||||||
|
// @Summary Refresh Access Token
|
||||||
|
// @Description Accepts a valid refresh token to issue a new access token and a rotated refresh token.
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.RefreshData true "Refresh Token Body"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful rotation"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Invalid Refresh Token"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/refresh [post]
|
||||||
|
func (self *AuthHandler) Refresh(c *gin.Context) {
|
||||||
|
var refreshData service_auth.RefreshData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&refreshData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRefresh).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Refresh(&service_auth.RefreshPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &refreshData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
53
api/auth/token.go
Normal file
53
api/auth/token.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_auth"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token exchanges an authorization code for access and refresh tokens.
|
||||||
|
//
|
||||||
|
// @Summary Exchange Code for Token
|
||||||
|
// @Description Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_auth.TokenData true "Token Request Body"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_auth.TokenResponse} "Successful token issuance"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Invalid or Expired Code"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Router /auth/token [post]
|
||||||
|
func (self *AuthHandler) Token(c *gin.Context) {
|
||||||
|
var tokenData service_auth.TokenData
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&tokenData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Token(&service_auth.TokenPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &tokenData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
59
api/event/attendance.go
Normal file
59
api/event/attendance.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AttendanceList handles the retrieval of the attendance list for a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Get Attendance List
|
||||||
|
// @Description Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
|
||||||
|
// @Tags Event
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]service_event.AttendanceListResponse} "Successful retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/attendance [get]
|
||||||
|
func (self *EventHandler) AttendanceList(c *gin.Context) {
|
||||||
|
eventIdStr := c.Query("event_id")
|
||||||
|
|
||||||
|
eventId, err := uuid.Parse(eventIdStr)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput). // 或者 CommonErrorUuidParseFailed
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listData := service_event.AttendanceListData{
|
||||||
|
EventId: eventId,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.AttendanceList(&service_event.AttendanceListPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &listData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
177
api/event/checkin.go
Normal file
177
api/event/checkin.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Checkin generates a check-in code for a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Generate Check-in Code
|
||||||
|
// @Description Creates a temporary check-in code for the authenticated user and event.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinResponse} "Successfully generated code"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin [get]
|
||||||
|
func (self *EventHandler) Checkin(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.Checkin(&service_event.CheckinPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.CheckinData{EventId: eventId},
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckinSubmit validates a check-in code to complete attendance.
|
||||||
|
//
|
||||||
|
// @Summary Submit Check-in Code
|
||||||
|
// @Description Submits the generated code to mark the user as attended.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_event.CheckinSubmitData true "Checkin Code Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=nil} "Attendance marked successfully"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Code or Input"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin/submit [post]
|
||||||
|
func (self *EventHandler) CheckinSubmit(c *gin.Context) {
|
||||||
|
var data service_event.CheckinSubmitData
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.CheckinSubmit(&service_event.CheckinSubmitPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &data,
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckinQuery retrieves the check-in status of a user for an event.
|
||||||
|
//
|
||||||
|
// @Summary Query Check-in Status
|
||||||
|
// @Description Returns the timestamp of when the user checked in, or null if not yet checked in.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.CheckinQueryResponse} "Current attendance status"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "Record Not Found"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/checkin/query [get]
|
||||||
|
func (self *EventHandler) CheckinQuery(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.CheckinQuery(&service_event.CheckinQueryPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.CheckinQueryData{EventId: eventId},
|
||||||
|
})
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
27
api/event/handler.go
Normal file
27
api/event/handler.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandler struct {
|
||||||
|
svc service_event.EventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
eventSvc := service_event.NewEventService()
|
||||||
|
eventHandler := &EventHandler{eventSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
|
||||||
|
r.GET("/info", eventHandler.Info)
|
||||||
|
r.GET("/checkin", eventHandler.Checkin)
|
||||||
|
r.GET("/checkin/query", eventHandler.CheckinQuery)
|
||||||
|
r.POST("/checkin/submit", middleware.Permission(20), eventHandler.CheckinSubmit)
|
||||||
|
r.POST("/join", eventHandler.Join)
|
||||||
|
r.GET("/list", eventHandler.List)
|
||||||
|
r.GET("/attendance", middleware.Permission(40), eventHandler.AttendanceList)
|
||||||
|
r.GET("/joined", eventHandler.Joined)
|
||||||
|
}
|
||||||
88
api/event/info.go
Normal file
88
api/event/info.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves basic information about a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Get Event Information
|
||||||
|
// @Description Fetches the name, start time, and end time of an event using its UUID.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param event_id query string true "Event UUID"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=data.EventIndexDoc} "Successful retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "Event Not Found"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/info [get]
|
||||||
|
func (self *EventHandler) Info(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIdOrig := c.Query("event_id")
|
||||||
|
eventId, err := uuid.Parse(eventIdOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetEventInfo(&service_event.EventInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.EventInfoData{
|
||||||
|
EventId: eventId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 200, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
71
api/event/join.go
Normal file
71
api/event/join.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Join handles the request for a user to join a specific event.
|
||||||
|
//
|
||||||
|
// @Summary Join an Event
|
||||||
|
// @Description Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body service_event.EventJoinData true "Event Join Details (UserId and EventId are required)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_event.EventJoinResponse} "Successfully joined the event"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input or UUID Parse Failed"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized / Missing User ID / Event Limit Exceeded"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / Database Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/join [post]
|
||||||
|
func (self *EventHandler) Join(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceJoin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinData service_event.EventJoinData
|
||||||
|
if err := c.ShouldBindJSON(&joinData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceJoin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
joinData.UserId = userIdOrig.(string)
|
||||||
|
|
||||||
|
payload := &service_event.EventJoinPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &joinData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.JoinEvent(payload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
}
|
||||||
94
api/event/joined.go
Normal file
94
api/event/joined.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetJoined retrieves a paginated list of events that the current user has joined.
|
||||||
|
//
|
||||||
|
// @Summary Get Joined Events
|
||||||
|
// @Description Fetches a list of events where the authenticated user is a participant. Supports pagination.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Maximum number of events to return (default 20)"
|
||||||
|
// @Param offset query int false "Number of events to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.EventIndexDoc} "Successful retrieval of joined events"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/joined [get]
|
||||||
|
func (self *EventHandler) Joined(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinedQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query JoinedQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := &service_event.JoinedEventListPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.JoinedEventListData{
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetJoinedEvent(payload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
99
api/event/list.go
Normal file
99
api/event/list.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_event"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List retrieves a paginated list of events from the database.
|
||||||
|
//
|
||||||
|
// @Summary List Events
|
||||||
|
// @Description Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
||||||
|
// @Tags Event
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Maximum number of events to return (default 20)"
|
||||||
|
// @Param offset query int false "Number of events to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.EventIndexDoc} "Successful paginated list retrieval"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Missing offset or malformed parameters)"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database query failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /event/list [get]
|
||||||
|
func (self *EventHandler) List(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query ListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
// Handle binding error (e.g., syntax errors in query string)
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload for the service layer
|
||||||
|
eventListPayload := &service_event.EventListPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
Data: &service_event.EventListData{
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the service implementation
|
||||||
|
result := self.svc.ListEvents(eventListPayload)
|
||||||
|
|
||||||
|
// Check if the service returned any exception
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return successful response with event data
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
17
api/handler.go
Normal file
17
api/handler.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/api/auth"
|
||||||
|
"nixcn-cms/api/event"
|
||||||
|
"nixcn-cms/api/kyc"
|
||||||
|
"nixcn-cms/api/user"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(r *gin.RouterGroup) {
|
||||||
|
auth.ApiHandler(r.Group("/auth"))
|
||||||
|
user.ApiHandler(r.Group("/user"))
|
||||||
|
event.ApiHandler(r.Group("/event"))
|
||||||
|
kyc.ApiHandler(r.Group("/kyc"))
|
||||||
|
}
|
||||||
21
api/kyc/handler.go
Normal file
21
api/kyc/handler.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KycHandler struct {
|
||||||
|
svc service_kyc.KycService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
kycSvc := service_kyc.NewKycService()
|
||||||
|
kycHandler := &KycHandler{kycSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(10))
|
||||||
|
r.POST("/session", kycHandler.Session)
|
||||||
|
r.POST("/query", kycHandler.Query)
|
||||||
|
}
|
||||||
66
api/kyc/query.go
Normal file
66
api/kyc/query.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Query KYC Status
|
||||||
|
// @Description Checks the current state of a KYC session and updates local database if approved.
|
||||||
|
// @Tags KYC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_kyc.KycQueryData true "KYC query data (KycId)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_kyc.KycQueryResponse} "Query processed (success/pending/failed)"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid UUID or input"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /kyc/query [post]
|
||||||
|
func (self *KycHandler) Query(c *gin.Context) {
|
||||||
|
_, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryData service_kyc.KycQueryData
|
||||||
|
if err := c.ShouldBindJSON(&queryData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPayload := &service_kyc.KycQueryPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &queryData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.QueryKyc(queryPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
68
api/kyc/session.go
Normal file
68
api/kyc/session.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_kyc"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Create KYC Session
|
||||||
|
// @Description Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.
|
||||||
|
// @Tags KYC
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_kyc.KycSessionData true "KYC session data (Type and Base64 Identity)"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_kyc.KycSessionResponse} "Session created successfully"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid input or decode failed"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "Missing User ID"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error / KYC Service Error"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /kyc/session [post]
|
||||||
|
func (self *KycHandler) Session(c *gin.Context) {
|
||||||
|
userIdFromHeaderOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceSession).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionData service_kyc.KycSessionData
|
||||||
|
if err := c.ShouldBindJSON(&sessionData); err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceKyc).
|
||||||
|
SetEndpoint(exception.EndpointKycServiceSession).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData.UserId = userIdFromHeaderOrig.(string)
|
||||||
|
|
||||||
|
kycPayload := &service_kyc.KycSessionPayload{
|
||||||
|
Context: c,
|
||||||
|
Data: &sessionData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.SessionKyc(kycPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
6
api/user/create.go
Normal file
6
api/user/create.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
func (self *UserHandler) Create(c *gin.Context) {
|
||||||
|
}
|
||||||
24
api/user/handler.go
Normal file
24
api/user/handler.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
svc service_user.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiHandler(r *gin.RouterGroup) {
|
||||||
|
userSvc := service_user.NewUserService()
|
||||||
|
userHandler := &UserHandler{userSvc}
|
||||||
|
|
||||||
|
r.Use(middleware.ApiVersionCheck(), middleware.JWTAuth(), middleware.Permission(5))
|
||||||
|
r.GET("/info", userHandler.Info)
|
||||||
|
r.GET("/info/:user_id", userHandler.Other)
|
||||||
|
r.PATCH("/update", userHandler.Update)
|
||||||
|
r.GET("/list", middleware.Permission(20), userHandler.List)
|
||||||
|
r.POST("/create", middleware.Permission(50), userHandler.Create)
|
||||||
|
}
|
||||||
70
api/user/info.go
Normal file
70
api/user/info.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves the profile information of the currently authenticated user.
|
||||||
|
//
|
||||||
|
// @Summary Get My User Information
|
||||||
|
// @Description Fetches the complete profile data for the user associated with the provided session/token.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/info [get]
|
||||||
|
func (self *UserHandler) Info(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInfoPayload := &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
IsOther: false,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetUserInfo(UserInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
61
api/user/list.go
Normal file
61
api/user/list.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List retrieves a paginated list of users from the search engine.
|
||||||
|
//
|
||||||
|
// @Summary List Users
|
||||||
|
// @Description Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query string false "Maximum number of users to return (default 0)"
|
||||||
|
// @Param offset query string true "Number of users to skip"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=[]data.UserIndexDoc} "Successful paginated list retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Format Error)"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Search Engine or Missing Offset)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/list [get]
|
||||||
|
func (self *UserHandler) List(c *gin.Context) {
|
||||||
|
type ListQuery struct {
|
||||||
|
Limit *string `form:"limit"`
|
||||||
|
Offset *string `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var query ListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusClient).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpResponse(c, 400, exception)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userListPayload := &service_user.UserListPayload{
|
||||||
|
Context: c,
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.ListUsers(userListPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
99
api/user/other.go
Normal file
99
api/user/other.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info retrieves the profile information of the other user.
|
||||||
|
//
|
||||||
|
// @Summary Get Other User Information
|
||||||
|
// @Description Fetches the complete profile data for the user associated with the provided session/token.
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param user_id path string true "Other user id"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=service_user.UserInfoData} "Successful profile retrieval"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 404 {object} utils.RespStatus{data=nil} "User Not Found"
|
||||||
|
// @Failure 403 {object} utils.RespStatus{data=nil} "User Not Public"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/info/{user_id} [get]
|
||||||
|
func (self *UserHandler) Other(c *gin.Context) {
|
||||||
|
userIdFromUrlOrig := c.Param("user_id")
|
||||||
|
|
||||||
|
userIdFromUrl, err := uuid.Parse(userIdFromUrlOrig)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdFromHeaderOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdFromHeader, err := uuid.Parse(userIdFromHeaderOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var UserInfoPayload = &service_user.UserInfoPayload{}
|
||||||
|
if userIdFromUrl == userIdFromHeader {
|
||||||
|
UserInfoPayload = &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userIdFromHeader,
|
||||||
|
IsOther: false,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
} else if userIdFromUrl != userIdFromHeader {
|
||||||
|
UserInfoPayload = &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userIdFromUrl,
|
||||||
|
IsOther: true,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.GetUserInfo(UserInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
84
api/user/update.go
Normal file
84
api/user/update.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update modifies the profile information for the currently authenticated user.
|
||||||
|
//
|
||||||
|
// @Summary Update User Information
|
||||||
|
// @Description Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
|
||||||
|
// @Description Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body service_user.UserInfoData true "Updated User Profile Data"
|
||||||
|
// @Success 200 {object} utils.RespStatus{data=nil} "Successful profile update"
|
||||||
|
// @Failure 400 {object} utils.RespStatus{data=nil} "Invalid Input (Validation Failed)"
|
||||||
|
// @Failure 401 {object} utils.RespStatus{data=nil} "Missing User ID / Unauthorized"
|
||||||
|
// @Failure 500 {object} utils.RespStatus{data=nil} "Internal Server Error (Database Error / UUID Parse Failed)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /user/update [patch]
|
||||||
|
func (self *UserHandler) Update(c *gin.Context) {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfoPayload := &service_user.UserInfoPayload{
|
||||||
|
Context: c,
|
||||||
|
UserId: userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.ShouldBindJSON(&userInfoPayload.Data)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceUser).
|
||||||
|
SetEndpoint(exception.EndpointUserServiceUpdate).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpResponse(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := self.svc.UpdateUserInfo(userInfoPayload)
|
||||||
|
|
||||||
|
if result.Common.Exception.Original != exception.CommonSuccess {
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.HttpResponse(c, result.Common.HttpCode, result.Common.Exception.String(), result.Data)
|
||||||
|
}
|
||||||
14
cmd/gen_exception/definitions/common.yaml
Normal file
14
cmd/gen_exception/definitions/common.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
common:
|
||||||
|
success: "00000"
|
||||||
|
error:
|
||||||
|
invalid_input: "00001"
|
||||||
|
unauthorized: "00002"
|
||||||
|
internal: "00003"
|
||||||
|
permission_denied: "00004"
|
||||||
|
uuid_parse_failed: "00005"
|
||||||
|
database: "00006"
|
||||||
|
missing_user_id: "00007"
|
||||||
|
user_not_found: "00008"
|
||||||
|
user_not_public: "00009"
|
||||||
|
base64_decode_failed: "00010"
|
||||||
|
json_decode_failed: "00011"
|
||||||
33
cmd/gen_exception/definitions/endpoint.yaml
Normal file
33
cmd/gen_exception/definitions/endpoint.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
endpoint:
|
||||||
|
auth:
|
||||||
|
service:
|
||||||
|
redirect: "01"
|
||||||
|
magic: "02"
|
||||||
|
token: "03"
|
||||||
|
refresh: "04"
|
||||||
|
exchange: "05"
|
||||||
|
event:
|
||||||
|
service:
|
||||||
|
info: "01"
|
||||||
|
checkin: "02"
|
||||||
|
checkin_query: "03"
|
||||||
|
checkin_submit: "04"
|
||||||
|
list: "05"
|
||||||
|
join: "06"
|
||||||
|
attendance_list: "07"
|
||||||
|
user:
|
||||||
|
service:
|
||||||
|
info: "01"
|
||||||
|
update: "02"
|
||||||
|
list: "03"
|
||||||
|
full: "04"
|
||||||
|
create: "05"
|
||||||
|
kyc:
|
||||||
|
service:
|
||||||
|
session: "01"
|
||||||
|
query: "02"
|
||||||
|
agenda:
|
||||||
|
service:
|
||||||
|
submit: "01"
|
||||||
|
middleware:
|
||||||
|
service: "01"
|
||||||
6
cmd/gen_exception/definitions/middleware.yaml
Normal file
6
cmd/gen_exception/definitions/middleware.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
middleware:
|
||||||
|
service:
|
||||||
|
gin_logger: "901"
|
||||||
|
jwt: "902"
|
||||||
|
permission: "903"
|
||||||
|
api_version: "904"
|
||||||
6
cmd/gen_exception/definitions/service.yaml
Normal file
6
cmd/gen_exception/definitions/service.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
service:
|
||||||
|
auth: "001"
|
||||||
|
user: "002"
|
||||||
|
event: "003"
|
||||||
|
kyc: "004"
|
||||||
|
agenda: "005"
|
||||||
47
cmd/gen_exception/definitions/specific.yaml
Normal file
47
cmd/gen_exception/definitions/specific.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
api:
|
||||||
|
version:
|
||||||
|
not_found: "00001"
|
||||||
|
auth:
|
||||||
|
redirect:
|
||||||
|
token_invalid: "00001"
|
||||||
|
client_not_found: "00002"
|
||||||
|
uri_mismatch: "00003"
|
||||||
|
invalid_uri: "00004"
|
||||||
|
magic:
|
||||||
|
turnstile_failed: "00001"
|
||||||
|
code_gen_failed: "00002"
|
||||||
|
invalid_external_url: "00003"
|
||||||
|
invalid_email_config: "00004"
|
||||||
|
token:
|
||||||
|
invalid_token: "00001"
|
||||||
|
gen_failed: "00002"
|
||||||
|
refresh:
|
||||||
|
invalid_token: "00001"
|
||||||
|
renew_failed: "00002"
|
||||||
|
exchange:
|
||||||
|
get_user_id_failed: "00001"
|
||||||
|
code_gen_failed: "00002"
|
||||||
|
invalid_redirect_uri: "00003"
|
||||||
|
user:
|
||||||
|
list:
|
||||||
|
database_failed: "00001"
|
||||||
|
event:
|
||||||
|
info:
|
||||||
|
not_found: "00001"
|
||||||
|
checkin:
|
||||||
|
gen_code_failed: "00001"
|
||||||
|
checkin_query:
|
||||||
|
record_not_found: "00001"
|
||||||
|
list:
|
||||||
|
database_failed: "00001"
|
||||||
|
join:
|
||||||
|
event_invalid: "00001"
|
||||||
|
limit_exceeded: "00002"
|
||||||
|
attendance:
|
||||||
|
list_error: "00001"
|
||||||
|
kyc_info_decrypt_failed: "00002"
|
||||||
|
kyc:
|
||||||
|
session:
|
||||||
|
failed: "00001"
|
||||||
|
query:
|
||||||
|
failed: "00001"
|
||||||
5
cmd/gen_exception/definitions/status.yaml
Normal file
5
cmd/gen_exception/definitions/status.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
status:
|
||||||
|
success: "2"
|
||||||
|
user: "4"
|
||||||
|
server: "5"
|
||||||
|
client: "6"
|
||||||
3
cmd/gen_exception/definitions/types.yaml
Normal file
3
cmd/gen_exception/definitions/types.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
type:
|
||||||
|
common: "1"
|
||||||
|
specific: "0"
|
||||||
8
cmd/gen_exception/exception.tmpl
Normal file
8
cmd/gen_exception/exception.tmpl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Code generated by gen-exception; DO NOT EDIT.
|
||||||
|
package exception
|
||||||
|
|
||||||
|
const (
|
||||||
|
{{- range .Items }}
|
||||||
|
{{ .Name }} = "{{ .Value }}"
|
||||||
|
{{- end }}
|
||||||
|
)
|
||||||
95
cmd/gen_exception/main.go
Normal file
95
cmd/gen_exception/main.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorItem struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TplData struct {
|
||||||
|
Items []ErrorItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCamel(s string) string {
|
||||||
|
caser := cases.Title(language.English)
|
||||||
|
s = strings.ReplaceAll(s, "-", "_")
|
||||||
|
parts := strings.Split(s, "_")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = caser.String(parts[i])
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveParse(prefix string, raw any, items *[]ErrorItem) {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, val := range v {
|
||||||
|
recursiveParse(prefix+toCamel(key), val, items)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
*items = append(*items, ErrorItem{
|
||||||
|
Name: prefix,
|
||||||
|
Value: v,
|
||||||
|
})
|
||||||
|
case int, int64:
|
||||||
|
*items = append(*items, ErrorItem{
|
||||||
|
Name: prefix,
|
||||||
|
Value: fmt.Sprintf("%v", v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
yamlDir := "cmd/gen_exception/definitions"
|
||||||
|
outputDir := "internal/exception"
|
||||||
|
tplPath := "cmd/gen_exception/exception.tmpl"
|
||||||
|
|
||||||
|
if _, err := os.Stat(tplPath); os.IsNotExist(err) {
|
||||||
|
log.Fatalf("Cannot found tmpl %s", tplPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{"ToCamel": toCamel}
|
||||||
|
tmpl := template.Must(template.New("exception.tmpl").Funcs(funcMap).ParseFiles(tplPath))
|
||||||
|
|
||||||
|
os.MkdirAll(outputDir, 0755)
|
||||||
|
|
||||||
|
files, _ := filepath.Glob(filepath.Join(yamlDir, "*.yaml"))
|
||||||
|
for _, yamlFile := range files {
|
||||||
|
content, err := os.ReadFile(yamlFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Read file error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawData any
|
||||||
|
if err := yaml.Unmarshal(content, &rawData); err != nil {
|
||||||
|
log.Printf("Unmarshal error in %s: %v", yamlFile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []ErrorItem
|
||||||
|
recursiveParse("", rawData, &items)
|
||||||
|
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(yamlFile), filepath.Ext(yamlFile))
|
||||||
|
outputFileName := baseName + "_gen.go"
|
||||||
|
outputPath := filepath.Join(outputDir, outputFileName)
|
||||||
|
|
||||||
|
f, _ := os.Create(outputPath)
|
||||||
|
tmpl.Execute(f, TplData{Items: items})
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Generated: %s (%d constants)\n", outputPath, len(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
44
config.default.yaml
Normal file
44
config.default.yaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
server:
|
||||||
|
application: example
|
||||||
|
address: :8000
|
||||||
|
external_url: https://example.com
|
||||||
|
debug_mode: false
|
||||||
|
log_level: debug
|
||||||
|
service_name: nixcn-cms-backend
|
||||||
|
database:
|
||||||
|
type: postgres
|
||||||
|
host: 127.0.0.1
|
||||||
|
name: postgres
|
||||||
|
username: postgres
|
||||||
|
password: postgres
|
||||||
|
service_name: nixcn-cms-postgres
|
||||||
|
cache:
|
||||||
|
hosts: ["127.0.0.1:6379"]
|
||||||
|
master: ""
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
service_name: nixcn-cms-redis
|
||||||
|
email:
|
||||||
|
host:
|
||||||
|
port:
|
||||||
|
username:
|
||||||
|
password:
|
||||||
|
security:
|
||||||
|
insecure_skip_verify:
|
||||||
|
secrets:
|
||||||
|
turnstile_secret: example
|
||||||
|
client_secret_key: aes_32_byte_string
|
||||||
|
kyc_info_key: aes_32_byte_string
|
||||||
|
ttl:
|
||||||
|
auth_code_ttl: 10m
|
||||||
|
access_ttl: 15s
|
||||||
|
refresh_ttl: 168h
|
||||||
|
checkin_code_ttl: 10m
|
||||||
|
kyc:
|
||||||
|
ali_access_key_id: example
|
||||||
|
ali_access_key_secret: example
|
||||||
|
passport_reader_public_key: example
|
||||||
|
passport_reader_secret: example
|
||||||
|
tracer:
|
||||||
|
otel_controller_endpoint: localhost:4317
|
||||||
41
config/config.go
Normal file
41
config/config.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigDir() string {
|
||||||
|
env := os.Getenv("CONFIG_PATH")
|
||||||
|
if env != "" {
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
// Read global config
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.AddConfigPath(ConfigDir())
|
||||||
|
|
||||||
|
// Bind ENV
|
||||||
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Println("[Config] No config file found, using Env vars only.")
|
||||||
|
} else {
|
||||||
|
log.Fatalf("[Config] Fatal error reading config file: %s \n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
if err := viper.Unmarshal(conf); err != nil {
|
||||||
|
log.Fatalln("[Condig] Can't unmarshal config!")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
config/env.go
Normal file
14
config/env.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TZ() string {
|
||||||
|
tz := os.Getenv("TZ")
|
||||||
|
|
||||||
|
if tz == "" {
|
||||||
|
return "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
return tz
|
||||||
|
}
|
||||||
72
config/types.go
Normal file
72
config/types.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Server server `yaml:"server"`
|
||||||
|
Database database `yaml:"database"`
|
||||||
|
Cache cache `yaml:"cache"`
|
||||||
|
Email email `yaml:"email"`
|
||||||
|
Secrets secrets `yaml:"secrets"`
|
||||||
|
TTL ttl `yaml:"ttl"`
|
||||||
|
KYC kyc `yaml:"kyc"`
|
||||||
|
Tracer tracer `yaml:"tracer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
Application string `yaml:"application"`
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
ExternalUrl string `yaml:"external_url"`
|
||||||
|
DebugMode string `yaml:"debug_mode"`
|
||||||
|
LogLevel string `yaml:"log_level"`
|
||||||
|
ServiceName string `yaml:"service_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
ServiceName string `yaml:"service_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cache struct {
|
||||||
|
Hosts []string `yaml:"hosts"`
|
||||||
|
Master string `yaml:"master"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"passowrd"`
|
||||||
|
DB int `yaml:"db"`
|
||||||
|
ServiceName string `yaml:"service_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type email struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port string `yaml:"port"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Security string `yaml:"security"`
|
||||||
|
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type secrets struct {
|
||||||
|
TurnstileSecret string `yaml:"turnstile_secret"`
|
||||||
|
ClientSecretKey string `yaml:"client_secret_key"`
|
||||||
|
KycInfoKey string `yaml:"kyc_info_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ttl struct {
|
||||||
|
AuthCodeTTL string `yaml:"auth_code_ttl"`
|
||||||
|
AccessTTL string `yaml:"access_ttl"`
|
||||||
|
RefreshTTL string `yaml:"refresh_ttl"`
|
||||||
|
CheckinCodeTTL string `yaml:"checkin_code_ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type kyc struct {
|
||||||
|
AliAccessKeyId string `yaml:"ali_access_key_id"`
|
||||||
|
AliAccessKeySecret string `yaml:"ali_access_key_secret"`
|
||||||
|
PassportReaderPublicKey string `yaml:"passport_reader_public_key"`
|
||||||
|
PassportReaderSecret string `yaml:"passport_reader_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tracer struct {
|
||||||
|
OtelControllerEndpoint string `yaml:"otel_controller_endpoint"`
|
||||||
|
}
|
||||||
125
data/agenda.go
Normal file
125
data/agenda.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agenda struct {
|
||||||
|
Id uint `json:"id" gorm:"primarykey;autoIncrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
AgendaId uuid.UUID `json:"agenda_id" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;not null"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar(255);not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text;not null"`
|
||||||
|
IsApproved bool `json:"is_approved" gorm:"type:boolean;default:false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) SetAttendanceId(id uuid.UUID) *Agenda {
|
||||||
|
self.AttendanceId = id
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) SetName(name string) *Agenda {
|
||||||
|
self.Name = name
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) SetDescription(desc string) *Agenda {
|
||||||
|
self.Description = desc
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) SetIsApproved(approved bool) *Agenda {
|
||||||
|
self.IsApproved = approved
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) GetByAgendaId(ctx context.Context, agendaId uuid.UUID) (*Agenda, error) {
|
||||||
|
var agenda Agenda
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("agenda_id = ?", agendaId).
|
||||||
|
First(&agenda).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &agenda, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) GetListByAttendanceId(ctx context.Context, attendanceId uuid.UUID) (*[]Agenda, error) {
|
||||||
|
var result []Agenda
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Agenda{}).
|
||||||
|
Where("attendance_id = ?", attendanceId).
|
||||||
|
Order("id ASC").
|
||||||
|
Scan(&result).Error
|
||||||
|
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) Create(ctx context.Context) error {
|
||||||
|
self.UUID = uuid.New()
|
||||||
|
self.AgendaId = uuid.New()
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(&self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) Update(ctx context.Context, agendaId uuid.UUID) (*Agenda, error) {
|
||||||
|
var agenda Agenda
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.
|
||||||
|
Where("agenda_id = ?", agendaId).
|
||||||
|
First(&agenda).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.
|
||||||
|
Model(&agenda).
|
||||||
|
Updates(self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.
|
||||||
|
Where("agenda_id = ?", agendaId).
|
||||||
|
First(&agenda).Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &agenda, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Agenda) Delete(ctx context.Context, agendaId uuid.UUID) error {
|
||||||
|
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
result := tx.Where("agenda_id = ?", agendaId).Delete(&Agenda{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
295
data/attendance.go
Normal file
295
data/attendance.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Attendance struct {
|
||||||
|
Id uint `json:"id" gorm:"primarykey;autoIncrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
|
||||||
|
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
|
||||||
|
KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
|
||||||
|
Role string `json:"role" gorm:"type:varchar(255);not null"`
|
||||||
|
State string `json:"state" gorm:"type:varchar(255);not null"` // suspended | out_of_limit | success
|
||||||
|
CheckinAt time.Time `json:"checkin_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceSearchDoc struct {
|
||||||
|
AttendanceId string `json:"attendance_id"`
|
||||||
|
EventId string `json:"event_id"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CheckinAt time.Time `json:"checkin_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) SetEventId(s uuid.UUID) *Attendance {
|
||||||
|
self.EventId = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) SetUserId(s uuid.UUID) *Attendance {
|
||||||
|
self.UserId = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) SetKycId(s uuid.UUID) *Attendance {
|
||||||
|
self.KycId = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) SetRole(s string) *Attendance {
|
||||||
|
self.Role = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) SetState(s string) *Attendance {
|
||||||
|
self.State = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
|
||||||
|
var attendance Attendance
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("user_id = ? AND event_id = ?", userId, eventId).
|
||||||
|
First(&attendance).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &attendance, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceUsers struct {
|
||||||
|
UserId uuid.UUID `json:"user_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CheckinAt time.Time `json:"checkin_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GetUsersByEventID(ctx context.Context, eventID uuid.UUID) (*[]AttendanceUsers, error) {
|
||||||
|
var result []AttendanceUsers
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Attendance{}).
|
||||||
|
Select("user_id, checkin_at").
|
||||||
|
Where("event_id = ?", eventID).
|
||||||
|
Order("checkin_at ASC").
|
||||||
|
Scan(&result).Error
|
||||||
|
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceEvent struct {
|
||||||
|
EventId uuid.UUID `json:"event_id"`
|
||||||
|
CheckinAt time.Time `json:"checkin_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID) (*[]AttendanceEvent, error) {
|
||||||
|
var result []AttendanceEvent
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Attendance{}).
|
||||||
|
Select("event_id, checkin_at").
|
||||||
|
Where("user_id = ?", userID).
|
||||||
|
Order("checkin_at ASC").
|
||||||
|
Scan(&result).Error
|
||||||
|
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) Create(ctx context.Context) (uuid.UUID, error) {
|
||||||
|
self.UUID = uuid.New()
|
||||||
|
self.AttendanceId = uuid.New()
|
||||||
|
|
||||||
|
// DB transaction for strong consistency
|
||||||
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(&self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.AttendanceId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GetAttendanceListByEventId(ctx context.Context, eventId uuid.UUID) (*[]Attendance, error) {
|
||||||
|
var result []Attendance
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("event_id = ?", eventId).
|
||||||
|
Order("checkin_at DESC").
|
||||||
|
Find(&result).Error
|
||||||
|
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GetJoinedEventIDs(ctx context.Context, userId uuid.UUID, eventIds []uuid.UUID) (map[uuid.UUID]bool, error) {
|
||||||
|
joinedMap := make(map[uuid.UUID]bool)
|
||||||
|
|
||||||
|
if len(eventIds) == 0 {
|
||||||
|
return joinedMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundEventIds []uuid.UUID
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Attendance{}).
|
||||||
|
Where("user_id = ? AND event_id IN ?", userId, eventIds).
|
||||||
|
Pluck("event_id", &foundEventIds).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range foundEventIds {
|
||||||
|
joinedMap[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinedMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) CountUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Attendance{}).
|
||||||
|
Where("event_id = ?", eventID).
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) CountCheckedInUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Attendance{}).
|
||||||
|
Where("event_id = ? AND checkin_at IS NOT NULL AND checkin_at > ?", eventID, time.Time{}). // 过滤未签到用户
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) {
|
||||||
|
var attendance Attendance
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Lock the row for update
|
||||||
|
if err := tx.
|
||||||
|
Where("attendance_id = ?", attendanceId).
|
||||||
|
First(&attendance).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.
|
||||||
|
Model(&attendance).
|
||||||
|
Updates(self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload to ensure struct is up to date
|
||||||
|
return tx.
|
||||||
|
Where("attendance_id = ?", attendanceId).
|
||||||
|
First(&attendance).Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &attendance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
|
||||||
|
ttl := viper.GetDuration("ttl.checkin_code_ttl")
|
||||||
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
for {
|
||||||
|
code := fmt.Sprintf("%06d", rng.Intn(900000)+100000)
|
||||||
|
ok, err := Redis.SetNX(
|
||||||
|
ctx,
|
||||||
|
"checkin_code:"+code,
|
||||||
|
"user_id:"+self.UserId.String()+":event_id:"+eventId.String(),
|
||||||
|
ttl,
|
||||||
|
).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return &code, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Attendance) VerifyCheckinCode(ctx context.Context, checkinCode string) error {
|
||||||
|
val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("[Attendance Data] invalid or expired checkin code")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected format: user_id:<uuid>:event_id:<uuid>
|
||||||
|
parts := strings.Split(val, ":")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return errors.New("[Attendance Data] invalid checkin code format")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdStr := parts[1]
|
||||||
|
eventIdStr := parts[3]
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventId, err := uuid.Parse(eventIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attendanceData, err := self.GetAttendance(ctx, userId, eventId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
self.CheckinAt = time.Now()
|
||||||
|
_, err = self.Update(ctx, attendanceData.AttendanceId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
92
data/client.go
Normal file
92
data/client.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"nixcn-cms/internal/cryptography"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Id uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
ClientId string `json:"client_id" gorm:"type:varchar(255);uniqueIndex;not null"`
|
||||||
|
ClientSecret string `json:"client_secret" gorm:"type:varchar(255);not null"`
|
||||||
|
ClientName string `json:"client_name" gorm:"type:varchar(255);uniqueIndex;not null"`
|
||||||
|
RedirectUri datatypes.JSON `json:"redirect_uri" gorm:"type:json;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) GetClientByClientId(ctx context.Context, clientId string) (*Client, error) {
|
||||||
|
var client Client
|
||||||
|
if err := Database.WithContext(ctx).
|
||||||
|
Where("client_id = ?", clientId).
|
||||||
|
First(&client).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) GetDecryptedSecret() ([]byte, error) {
|
||||||
|
secretKey := viper.GetString("secrets.client_secret_key")
|
||||||
|
secret, err := cryptography.AESCBCDecrypt(self.ClientSecret, []byte(secretKey))
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientParams struct {
|
||||||
|
ClientId string
|
||||||
|
ClientName string
|
||||||
|
RedirectUri []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) Create(ctx context.Context, params *ClientParams) (*Client, error) {
|
||||||
|
jsonRedirectUri, err := json.Marshal(params.RedirectUri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encKey := viper.GetString("secrets.client_secret_key")
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientSecret := base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
encryptedSecret, err := cryptography.AESCBCEncrypt([]byte(clientSecret), []byte(encKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
UUID: uuid.New(),
|
||||||
|
ClientId: params.ClientId,
|
||||||
|
ClientSecret: encryptedSecret,
|
||||||
|
ClientName: params.ClientName,
|
||||||
|
RedirectUri: jsonRedirectUri,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Database.WithContext(ctx).Create(&client).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) ValidateRedirectURI(redirectURI string) error {
|
||||||
|
var uris []string
|
||||||
|
if err := json.Unmarshal(self.RedirectUri, &uris); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range uris {
|
||||||
|
if strings.HasPrefix(redirectURI, prefix) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("[Client Data] redirect uri not match")
|
||||||
|
}
|
||||||
63
data/data.go
Normal file
63
data/data.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data/drivers"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Database *gorm.DB
|
||||||
|
var Redis redis.UniversalClient
|
||||||
|
|
||||||
|
func Init(ctx context.Context) {
|
||||||
|
// Init database
|
||||||
|
dbType := viper.GetString("database.type")
|
||||||
|
exDSN := drivers.ExternalDSN{
|
||||||
|
Host: viper.GetString("database.host"),
|
||||||
|
Name: viper.GetString("database.name"),
|
||||||
|
Username: viper.GetString("database.username"),
|
||||||
|
Password: viper.GetString("database.password"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbType != "postgres" {
|
||||||
|
slog.ErrorContext(ctx, "[Database] Only support postgras db!")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conect to db
|
||||||
|
db, err := drivers.Postgres(exDSN)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "[Database] Error connecting to db!", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto migrate
|
||||||
|
err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{}, &Kyc{})
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "[Database] Error migrating database!", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
Database = db
|
||||||
|
|
||||||
|
// Init redis conection
|
||||||
|
rdbAddress := viper.GetStringSlice("cache.hosts")
|
||||||
|
rDSN := drivers.RedisDSN{
|
||||||
|
Hosts: rdbAddress,
|
||||||
|
Master: viper.GetString("cache.master"),
|
||||||
|
Username: viper.GetString("cache.username"),
|
||||||
|
Password: viper.GetString("cache.password"),
|
||||||
|
DB: viper.GetInt("cache.db"),
|
||||||
|
}
|
||||||
|
rdb, err := drivers.Redis(rDSN)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "[Redis] Error connecting to Redis!", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
Redis = rdb
|
||||||
|
}
|
||||||
48
data/drivers/postgres.go
Normal file
48
data/drivers/postgres.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package drivers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"nixcn-cms/config"
|
||||||
|
"nixcn-cms/logger"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/plugin/opentelemetry/tracing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SplitHostPort(url string) (host, port string) {
|
||||||
|
if !strings.Contains(url, ":") {
|
||||||
|
return url, "5432"
|
||||||
|
}
|
||||||
|
split := strings.Split(url, ":")
|
||||||
|
return split[0], split[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Postgres(dsn ExternalDSN) (*gorm.DB, error) {
|
||||||
|
serviceName := viper.GetString("database.service_name")
|
||||||
|
|
||||||
|
host, port := SplitHostPort(dsn.Host)
|
||||||
|
conn := "host=" + host + " user=" + dsn.Username + " password=" + dsn.Password + " dbname=" + dsn.Name + " port=" + port + " sslmode=disable TimeZone=" + config.TZ()
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(conn), &gorm.Config{
|
||||||
|
Logger: logger.GormLogger(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Use(tracing.NewPlugin(
|
||||||
|
tracing.WithAttributes(
|
||||||
|
attribute.String("db.instance", serviceName),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("[Database] Error starting otel plugin!", "name", serviceName, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, err
|
||||||
|
}
|
||||||
44
data/drivers/redis.go
Normal file
44
data/drivers/redis.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package drivers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/extra/redisotel/v9"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Redis(dsn RedisDSN) (redis.UniversalClient, error) {
|
||||||
|
serviceName := viper.GetString("cache.service_name")
|
||||||
|
|
||||||
|
// Connect to Redis
|
||||||
|
rdb := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
|
Addrs: dsn.Hosts,
|
||||||
|
MasterName: dsn.Master,
|
||||||
|
Username: dsn.Username,
|
||||||
|
Password: dsn.Password,
|
||||||
|
DB: dsn.DB,
|
||||||
|
})
|
||||||
|
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
semconv.DBSystemRedis,
|
||||||
|
attribute.String("db.instance", serviceName),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := redisotel.InstrumentMetrics(rdb, redisotel.WithAttributes(attrs...)); err != nil {
|
||||||
|
slog.Error("[Redis] Error starting otel metrics plugin!", "name", serviceName, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := redisotel.InstrumentTracing(rdb, redisotel.WithAttributes(attrs...)); err != nil {
|
||||||
|
slog.Error("[Redis] Error starting otel tracing plugin!", "name", serviceName, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping redis
|
||||||
|
ctx := context.Background()
|
||||||
|
_, err := rdb.Ping(ctx).Result()
|
||||||
|
|
||||||
|
return rdb, err
|
||||||
|
}
|
||||||
21
data/drivers/types.go
Normal file
21
data/drivers/types.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package drivers
|
||||||
|
|
||||||
|
type ExternalDSN struct {
|
||||||
|
Host string
|
||||||
|
Name string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisDSN struct {
|
||||||
|
Hosts []string
|
||||||
|
Master string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
DB int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeiliDSN struct {
|
||||||
|
Host string
|
||||||
|
ApiKey string
|
||||||
|
}
|
||||||
143
data/event.go
Normal file
143
data/event.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Id uint `json:"id" gorm:"primarykey;autoincrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar(255);index;not null"`
|
||||||
|
Type string `json:"type" gotm:"type:varchar(255);index;not null"` // official | party
|
||||||
|
Description string `json:"description" gorm:"type:text;not null"`
|
||||||
|
StartTime time.Time `json:"start_time" gorm:"index"`
|
||||||
|
EndTime time.Time `json:"end_time" gorm:"index"`
|
||||||
|
Thumbnail string `json:"thumbnail" gorm:"type:varchar(255)"`
|
||||||
|
Owner uuid.UUID `json:"owner" gorm:"type:uuid;index;not null"`
|
||||||
|
EnableKYC bool `json:"enable_kyc" gorm:"not null"`
|
||||||
|
Quota int64 `json:"quota" gorm:"not null"`
|
||||||
|
Limit int64 `json:"limit" gorm:"not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventIndexDoc struct {
|
||||||
|
EventId string `json:"event_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartTime time.Time `json:"start_time"`
|
||||||
|
EndTime time.Time `json:"end_time"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
EnableKYC bool `json:"enable_kyc"`
|
||||||
|
IsJoined bool `json:"is_joined"`
|
||||||
|
IsCheckedIn bool `json:"is_checked_in"`
|
||||||
|
JoinCount int64 `json:"join_count"`
|
||||||
|
CheckinCount int64 `json:"checkin_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Event) GetEventById(ctx context.Context, eventId uuid.UUID) (*Event, error) {
|
||||||
|
var event Event
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("event_id = ?", eventId).
|
||||||
|
First(&event).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Event) UpdateEventById(ctx context.Context, eventId uuid.UUID) error {
|
||||||
|
// DB transaction
|
||||||
|
if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Update by business key
|
||||||
|
if err := tx.
|
||||||
|
Model(&Event{}).
|
||||||
|
Where("event_id = ?", eventId).
|
||||||
|
Updates(self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload to ensure struct is fresh
|
||||||
|
return tx.
|
||||||
|
Where("event_id = ?", eventId).
|
||||||
|
First(self).Error
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Event) Create(ctx context.Context) error {
|
||||||
|
self.UUID = uuid.New()
|
||||||
|
self.EventId = uuid.New()
|
||||||
|
|
||||||
|
// DB transaction only
|
||||||
|
if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Event) FastListEvents(ctx context.Context, limit, offset int64) (*[]EventIndexDoc, error) {
|
||||||
|
var results []EventIndexDoc
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Model(&Event{}).
|
||||||
|
Select("event_id", "name", "type", "description", "start_time", "end_time", "thumbnail", "enable_kyc").
|
||||||
|
Limit(int(limit)).
|
||||||
|
Offset(int(offset)).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Event) GetEventsByUserId(ctx context.Context, userId uuid.UUID, limit, offset int64) (*[]EventIndexDoc, error) {
|
||||||
|
var results []EventIndexDoc
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Table("events").
|
||||||
|
Select(`
|
||||||
|
events.event_id,
|
||||||
|
events.name,
|
||||||
|
events.type,
|
||||||
|
events.description,
|
||||||
|
events.start_time,
|
||||||
|
events.end_time,
|
||||||
|
events.thumbnail,
|
||||||
|
events.enable_kyc,
|
||||||
|
(SELECT COUNT(*) FROM attendances WHERE attendances.event_id = events.event_id) as join_count
|
||||||
|
`).
|
||||||
|
Joins("JOIN attendances ON attendances.event_id = events.event_id").
|
||||||
|
Where("attendances.user_id = ?", userId).
|
||||||
|
Order("events.start_time DESC").
|
||||||
|
Limit(int(limit)).
|
||||||
|
Offset(int(offset)).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &results, nil
|
||||||
|
}
|
||||||
92
data/kyc.go
Normal file
92
data/kyc.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kyc struct {
|
||||||
|
Id uint `json:"id" gorm:"primarykey;autoincrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"`
|
||||||
|
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;not null"`
|
||||||
|
KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueindex;not null"`
|
||||||
|
Type string `json:"type" gorm:"type:varchar(255);not null"`
|
||||||
|
KycInfo string `json:"kyc_info" gorm:"type:text"` // aes256(base64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) SetUserId(id uuid.UUID) *Kyc {
|
||||||
|
self.UserId = id
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) SetType(t string) *Kyc {
|
||||||
|
self.Type = t
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) SetKycInfo(info string) *Kyc {
|
||||||
|
self.KycInfo = info
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) Create(ctx context.Context) (uuid.UUID, error) {
|
||||||
|
self.UUID = uuid.New()
|
||||||
|
self.KycId = uuid.New()
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
return tx.Create(self).Error
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.KycId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) GetByKycId(ctx context.Context, kycId *uuid.UUID) (*Kyc, error) {
|
||||||
|
var kyc Kyc
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("kyc_id = ?", kycId).
|
||||||
|
First(&kyc).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &kyc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) ListByUserId(ctx context.Context, userId *uuid.UUID) ([]Kyc, error) {
|
||||||
|
var list []Kyc
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("user_id = ?", userId).
|
||||||
|
Find(&list).Error
|
||||||
|
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) UpdateByKycID(ctx context.Context, kycId *uuid.UUID, updates map[string]any) error {
|
||||||
|
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
return tx.Model(&Kyc{}).
|
||||||
|
Where("kyc_id = ?", kycId).
|
||||||
|
Updates(updates).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) DeleteByKycID(ctx context.Context, kycId *uuid.UUID) error {
|
||||||
|
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
return tx.Where("kyc_id = ?", kycId).
|
||||||
|
Delete(&Kyc{}).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Kyc) DeleteAllByUserId(ctx context.Context, userId *uuid.UUID) error {
|
||||||
|
return Database.WithContext(ctx).
|
||||||
|
Where("user_id = ?", userId).
|
||||||
|
Delete(&Kyc{}).Error
|
||||||
|
}
|
||||||
154
data/user.go
Normal file
154
data/user.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id uint `json:"id" gorm:"primarykey;autoincrement"`
|
||||||
|
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"`
|
||||||
|
UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueindex;not null"`
|
||||||
|
Email string `json:"email" gorm:"type:varchar(255);uniqueindex;not null"`
|
||||||
|
Username string `json:"username" gorm:"type:varchar(255);uniqueindex;not null"`
|
||||||
|
Nickname string `json:"nickname" gorm:"type:text"`
|
||||||
|
Subtitle string `json:"subtitle" gorm:"type:text"`
|
||||||
|
Avatar string `json:"avatar" gorm:"type:text"`
|
||||||
|
Bio string `json:"bio" gorm:"type:text"`
|
||||||
|
PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"`
|
||||||
|
AllowPublic bool `json:"allow_public" gorm:"default:false;not null"`
|
||||||
|
KycInfo string `json:"kyc_info" gorm:"type:text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserIndexDoc struct {
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Subtitle string `json:"subtitle"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetEmail(s string) *User {
|
||||||
|
self.Email = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetUsername(s string) *User {
|
||||||
|
self.Username = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetNickname(s string) *User {
|
||||||
|
self.Nickname = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetSubtitle(s string) *User {
|
||||||
|
self.Subtitle = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetAvatar(s string) *User {
|
||||||
|
self.Avatar = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetBio(s string) *User {
|
||||||
|
self.Bio = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetPermissionLevel(s uint) *User {
|
||||||
|
self.PermissionLevel = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) SetAllowPublic(s bool) *User {
|
||||||
|
self.AllowPublic = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) GetByEmail(ctx context.Context, email *string) (*User, error) {
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("email = ?", email).
|
||||||
|
First(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) GetByUserId(ctx context.Context, userId *uuid.UUID) (*User, error) {
|
||||||
|
var user User
|
||||||
|
|
||||||
|
err := Database.WithContext(ctx).
|
||||||
|
Where("user_id = ?", userId).
|
||||||
|
First(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) Create(ctx context.Context) error {
|
||||||
|
self.UUID = uuid.New()
|
||||||
|
self.UserId = uuid.New()
|
||||||
|
|
||||||
|
// DB transaction only
|
||||||
|
if err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Create(self).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) UpdateByUserID(ctx context.Context, userId *uuid.UUID, updates map[string]any) error {
|
||||||
|
return Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&User{}).
|
||||||
|
Where("user_id = ?", userId).
|
||||||
|
Updates(updates).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedUser User
|
||||||
|
if err := tx.Where("user_id = ?", userId).First(&updatedUser).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *User) FastListUsers(ctx context.Context, limit, offset *int) (*[]UserIndexDoc, error) {
|
||||||
|
var results []UserIndexDoc
|
||||||
|
|
||||||
|
query := Database.WithContext(ctx).Model(&User{})
|
||||||
|
|
||||||
|
err := query.Select("user_id", "email", "username", "nickname", "subtitle", "avatar").
|
||||||
|
Limit(*limit).
|
||||||
|
Offset(*offset).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &results, nil
|
||||||
|
}
|
||||||
11
deploy/Caddyfile
Normal file
11
deploy/Caddyfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
test.nix.org.cn {
|
||||||
|
tls /etc/caddy/cert.crt /etc/caddy/cert.key
|
||||||
|
|
||||||
|
handle /app/api/* {
|
||||||
|
reverse_proxy cms-server:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /app/* {
|
||||||
|
reverse_proxy cms-client:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
83
deploy/compose.yaml
Normal file
83
deploy/compose.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: docker.io/postgres:18-alpine
|
||||||
|
container_name: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
volumes:
|
||||||
|
- ./data/postgres:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: docker.io/redis:8-alpine
|
||||||
|
container_name: redis
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
lgtm:
|
||||||
|
image: grafana/otel-lgtm:latest
|
||||||
|
container_name: lgtm
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "4317:4317" # OTLP gRPC
|
||||||
|
- "4318:4318" # OTLP HTTP
|
||||||
|
volumes:
|
||||||
|
- ./data/lgtm:/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"curl -f http://localhost:3000/api/health || exit 1",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: docker.io/caddy:latest
|
||||||
|
container_name: caddy
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- ./cert.crt:/etc/caddy/cert.crt
|
||||||
|
- ./cert.key:/etc/caddy/cert.key
|
||||||
|
- ./data/caddy/data:/data
|
||||||
|
- ./data/caddy/config:/config
|
||||||
|
|
||||||
|
cms-client:
|
||||||
|
image: registry.asnk.io/nixcn/cms-client:dev
|
||||||
|
container_name: cms-client
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
lgtm:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
cms-server:
|
||||||
|
image: registry.asnk.io/nixcn/cms-server:dev
|
||||||
|
container_name: cms-server
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
lgtm:
|
||||||
|
condition: service_healthy
|
||||||
182
devenv.lock
Normal file
182
devenv.lock
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"devenv": {
|
||||||
|
"locked": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"lastModified": 1772840537,
|
||||||
|
"narHash": "sha256-Axb80AW0e6Xd/UmtjFnTTPOqxzkI4tzGa8ykk8nKQoo=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "b6208f517a7359d6046c89b5ac0de20cfca3b4c0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1761588595,
|
||||||
|
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"go-overlay",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765016596,
|
||||||
|
"narHash": "sha256-rhSqPNxDVow7OQKi4qS5H8Au0P4S3AYbawBSmJNUtBQ=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"go-overlay",
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"go-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772865556,
|
||||||
|
"narHash": "sha256-IIYNduS/rTlLzDxDyHqXd8JHl9F7UDf3EiEdtwupop4=",
|
||||||
|
"owner": "purpleclay",
|
||||||
|
"repo": "go-overlay",
|
||||||
|
"rev": "8e54102a2301c8003f7a15f2536c6530d3035c4e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "purpleclay",
|
||||||
|
"repo": "go-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-src": "nixpkgs-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772749504,
|
||||||
|
"narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"rev": "08543693199362c1fddb8f52126030d0d374ba2e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "rolling",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772173633,
|
||||||
|
"narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": "devenv",
|
||||||
|
"go-overlay": "go-overlay",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
61
devenv.nix
Normal file
61
devenv.nix
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
process.managers.process-compose = {
|
||||||
|
settings.log_level = "info";
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
git
|
||||||
|
just
|
||||||
|
watchexec
|
||||||
|
fvm
|
||||||
|
podman
|
||||||
|
];
|
||||||
|
|
||||||
|
dotenv = {
|
||||||
|
enable = true;
|
||||||
|
filename = [
|
||||||
|
".env.production"
|
||||||
|
".env.development"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
languages = {
|
||||||
|
go = {
|
||||||
|
enable = true;
|
||||||
|
version = "1.26.0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
env.PODMAN_COMPOSE_PROVIDER = "none";
|
||||||
|
|
||||||
|
processes = {
|
||||||
|
server.exec = "sleep 30 && just watch";
|
||||||
|
lgtm.exec = ''
|
||||||
|
podman rm -f lgtm || true
|
||||||
|
podman run --name lgtm \
|
||||||
|
-p 3000:3000 -p 4317:4317 -p 4318:4318 \
|
||||||
|
-e OTEL_METRIC_EXPORT_INTERVAL=5000 \
|
||||||
|
docker.io/grafana/otel-lgtm:latest
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
redis = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
postgres = {
|
||||||
|
enable = true;
|
||||||
|
createDatabase = true;
|
||||||
|
listen_addresses = "127.0.0.1";
|
||||||
|
initialDatabases = [
|
||||||
|
{
|
||||||
|
name = "postgres";
|
||||||
|
user = "postgres";
|
||||||
|
pass = "postgres";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
8
devenv.yaml
Normal file
8
devenv.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
inputs:
|
||||||
|
go-overlay:
|
||||||
|
url: github:purpleclay/go-overlay
|
||||||
|
inputs:
|
||||||
|
nixpkgs:
|
||||||
|
follows: nixpkgs
|
||||||
|
nixpkgs:
|
||||||
|
url: github:cachix/devenv-nixpkgs/rolling
|
||||||
2429
docs/docs.go
Normal file
2429
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
2409
docs/swagger.json
Normal file
2409
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1358
docs/swagger.yaml
Normal file
1358
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6
generate.go
Normal file
6
generate.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate go run ./cmd/gen_exception/main.go
|
||||||
|
|
||||||
|
//go:generate swag fmt
|
||||||
|
//go:generate swag init -g server/server.go
|
||||||
138
go.mod
Normal file
138
go.mod
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
module nixcn-cms
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
replace (
|
||||||
|
google.golang.org/genproto => google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1
|
||||||
|
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1
|
||||||
|
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alibabacloud-go/cloudauth-20190307/v4 v4.13.1
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14
|
||||||
|
github.com/alibabacloud-go/tea v1.4.0
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.9
|
||||||
|
github.com/aliyun/credentials-go v1.4.11
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/goccy/go-json v0.10.5
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/redis/go-redis/extra/redisotel/v9 v9.17.3
|
||||||
|
github.com/redis/go-redis/v9 v9.17.3
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
github.com/swaggo/files v1.0.1
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0
|
||||||
|
go.opentelemetry.io/otel v1.39.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
|
||||||
|
go.opentelemetry.io/otel/log v0.15.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.15.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0
|
||||||
|
golang.org/x/crypto v0.47.0
|
||||||
|
golang.org/x/text v0.33.0
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/datatypes v1.2.7
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
gorm.io/plugin/opentelemetry v0.1.16
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/ClickHouse/ch-go v0.61.5 // indirect
|
||||||
|
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.2 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-faster/city v1.0.1 // indirect
|
||||||
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // 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/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/paulmach/orb v0.11.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||||
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.17.3 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
|
golang.org/x/net v0.48.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gorm.io/driver/clickhouse v0.7.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.7 // indirect
|
||||||
|
)
|
||||||
65
internal/authcode/authcode.go
Normal file
65
internal/authcode/authcode.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package authcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
ClientId string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthCode(ctx context.Context, clientId string, email string) (string, error) {
|
||||||
|
// generate random code
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
key := "auth_code:" + code
|
||||||
|
|
||||||
|
ttl := viper.GetDuration("ttl.auth_code_ttl")
|
||||||
|
|
||||||
|
// store auth code metadata in Redis
|
||||||
|
if err := data.Redis.HSet(
|
||||||
|
ctx,
|
||||||
|
key,
|
||||||
|
map[string]any{
|
||||||
|
"client_id": clientId,
|
||||||
|
"email": email,
|
||||||
|
},
|
||||||
|
).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set expiration (one-time auth code)
|
||||||
|
if err := data.Redis.Expire(ctx, key, ttl).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyAuthCode(ctx context.Context, code string) (*Token, bool) {
|
||||||
|
key := "auth_code:" + code
|
||||||
|
|
||||||
|
// Read auth code payload
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete auth code immediately (one-time use)
|
||||||
|
_ = data.Redis.Del(ctx, key).Err()
|
||||||
|
|
||||||
|
return &Token{
|
||||||
|
ClientId: dataMap["client_id"],
|
||||||
|
Email: dataMap["email"],
|
||||||
|
}, true
|
||||||
|
}
|
||||||
291
internal/authtoken/authtoken.go
Normal file
291
internal/authtoken/authtoken.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package authtoken
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Application string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtClaims struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate jwt clames
|
||||||
|
func (self *Token) NewClaims(clientId string, userId uuid.UUID) JwtClaims {
|
||||||
|
return JwtClaims{
|
||||||
|
ClientId: clientId,
|
||||||
|
UserID: userId,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.access_ttl"))),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: self.Application,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate access token
|
||||||
|
func (self *Token) GenerateAccessToken(ctx context.Context, clientId string, userId uuid.UUID) (string, error) {
|
||||||
|
claims := self.NewClaims(clientId, userId)
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
clientData, err := new(data.Client).GetClientByClientId(ctx, clientId)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := clientData.GetDecryptedSecret()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting decrypted secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedToken, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error signing token: %v", err)
|
||||||
|
}
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate refresh token
|
||||||
|
func (self *Token) GenerateRefreshToken() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue both access and refresh token
|
||||||
|
func (self *Token) IssueTokens(ctx context.Context, clientId string, userId uuid.UUID) (string, string, error) {
|
||||||
|
// access token
|
||||||
|
access, err := self.GenerateAccessToken(ctx, clientId, userId)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh token
|
||||||
|
refresh, err := self.GenerateRefreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := viper.GetDuration("ttl.refresh_ttl")
|
||||||
|
|
||||||
|
refreshKey := "refresh:" + refresh
|
||||||
|
|
||||||
|
// refresh -> user + client
|
||||||
|
if err := data.Redis.HSet(
|
||||||
|
ctx,
|
||||||
|
refreshKey,
|
||||||
|
map[string]any{
|
||||||
|
"user_id": userId.String(),
|
||||||
|
"client_id": clientId,
|
||||||
|
},
|
||||||
|
).Err(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := data.Redis.Expire(ctx, refreshKey, ttl).Err(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// user -> refresh tokens
|
||||||
|
userSetKey := "user:" + userId.String() + ":refresh_tokens"
|
||||||
|
|
||||||
|
if err := data.Redis.SAdd(ctx, userSetKey, refresh).Err(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
||||||
|
|
||||||
|
// client -> refresh tokens
|
||||||
|
clientSetKey := "client:" + clientId + ":refresh_tokens"
|
||||||
|
_ = data.Redis.SAdd(ctx, clientSetKey, refresh).Err()
|
||||||
|
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
|
||||||
|
|
||||||
|
return access, refresh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh access token
|
||||||
|
func (self *Token) RefreshAccessToken(ctx context.Context, refreshToken string) (string, error) {
|
||||||
|
key := "refresh:" + refreshToken
|
||||||
|
|
||||||
|
// read refresh token bind data
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return "", errors.New("[Auth Token] invalid refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdStr := dataMap["user_id"]
|
||||||
|
clientId := dataMap["client_id"]
|
||||||
|
|
||||||
|
if userIdStr == "" || clientId == "" {
|
||||||
|
return "", errors.New("[Auth Token] refresh token corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new access token
|
||||||
|
return self.GenerateAccessToken(ctx, clientId, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Token) RenewRefreshToken(ctx context.Context, refreshToken string) (string, error) {
|
||||||
|
ttl := viper.GetDuration("ttl.refresh_ttl")
|
||||||
|
|
||||||
|
oldKey := "refresh:" + refreshToken
|
||||||
|
|
||||||
|
// read old refresh token bind data
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, oldKey).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return "", errors.New("[Auth Token] invalid refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdStr := dataMap["user_id"]
|
||||||
|
clientId := dataMap["client_id"]
|
||||||
|
|
||||||
|
if userIdStr == "" || clientId == "" {
|
||||||
|
return "", errors.New("[Auth Token] refresh token corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new refresh token
|
||||||
|
newRefresh, err := self.GenerateRefreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// revoke old refresh token
|
||||||
|
if err := self.RevokeRefreshToken(ctx, refreshToken); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
newKey := "refresh:" + newRefresh
|
||||||
|
|
||||||
|
// refresh -> user + client
|
||||||
|
if err := data.Redis.HSet(
|
||||||
|
ctx,
|
||||||
|
newKey,
|
||||||
|
map[string]any{
|
||||||
|
"user_id": userIdStr,
|
||||||
|
"client_id": clientId,
|
||||||
|
},
|
||||||
|
).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := data.Redis.Expire(ctx, newKey, ttl).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// user -> refresh tokens
|
||||||
|
userSetKey := "user:" + userIdStr + ":refresh_tokens"
|
||||||
|
if err := data.Redis.SAdd(ctx, userSetKey, newRefresh).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
||||||
|
|
||||||
|
// client -> refresh tokens
|
||||||
|
clientSetKey := "client:" + clientId + ":refresh_tokens"
|
||||||
|
_ = data.Redis.SAdd(ctx, clientSetKey, newRefresh).Err()
|
||||||
|
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
|
||||||
|
|
||||||
|
return newRefresh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Token) RevokeRefreshToken(ctx context.Context, refreshToken string) error {
|
||||||
|
refreshKey := "refresh:" + refreshToken
|
||||||
|
|
||||||
|
// read refresh token metadata (user_id, client_id)
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, refreshKey).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
// Token already revoked or not found
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := dataMap["user_id"]
|
||||||
|
clientID := dataMap["client_id"]
|
||||||
|
|
||||||
|
// build index keys
|
||||||
|
userSetKey := "user:" + userID + ":refresh_tokens"
|
||||||
|
clientSetKey := "client:" + clientID + ":refresh_tokens"
|
||||||
|
|
||||||
|
// remove refresh token and all related indexes atomically
|
||||||
|
pipe := data.Redis.TxPipeline()
|
||||||
|
|
||||||
|
// remove main refresh token record
|
||||||
|
pipe.Del(ctx, refreshKey)
|
||||||
|
|
||||||
|
// remove refresh token from user's active refresh token set
|
||||||
|
pipe.SRem(ctx, userSetKey, refreshToken)
|
||||||
|
|
||||||
|
// remove refresh token from client's active refresh token set
|
||||||
|
pipe.SRem(ctx, clientSetKey, refreshToken)
|
||||||
|
|
||||||
|
_, err = pipe.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Token) HeaderVerify(ctx context.Context, header string) (string, error) {
|
||||||
|
if header == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split header to 2
|
||||||
|
parts := strings.SplitN(header, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
return "", errors.New("[Auth Token] invalid Authorization header format")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := parts[1]
|
||||||
|
|
||||||
|
// Verify access token
|
||||||
|
claims := &JwtClaims{}
|
||||||
|
token, err := jwt.ParseWithClaims(
|
||||||
|
tokenStr,
|
||||||
|
claims,
|
||||||
|
func(token *jwt.Token) (any, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, errors.New("[Auth Token] unexpected signing method")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.ClientId == "" {
|
||||||
|
return nil, errors.New("[Auth Token] client_id missing in token")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData, err := new(data.Client).GetClientByClientId(ctx, claims.ClientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := clientData.GetDecryptedSecret()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting decrypted secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
fmt.Println(err)
|
||||||
|
return "", errors.New("[Auth Token] invalid or expired token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.UserID.String(), nil
|
||||||
|
}
|
||||||
208
internal/cryptography/aes.go
Normal file
208
internal/cryptography/aes.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := io.ReadFull(rand.Reader, b)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKey(key []byte) ([]byte, error) {
|
||||||
|
switch len(key) {
|
||||||
|
case 16, 24, 32:
|
||||||
|
return key, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("[Cryptography AES] AES key length must be 16, 24, or 32 bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESGCMEncrypt(plaintext, key []byte) (string, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := randomBytes(gcm.NonceSize())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||||
|
out := append(nonce, ciphertext...)
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESGCMDecrypt(encoded string, key []byte) ([]byte, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("[Cryptography AES] ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := data[:gcm.NonceSize()]
|
||||||
|
ciphertext := data[gcm.NonceSize():]
|
||||||
|
|
||||||
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(data)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(data, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Unpad(data []byte) ([]byte, error) {
|
||||||
|
length := len(data)
|
||||||
|
if length == 0 {
|
||||||
|
return nil, errors.New("[Cryptography AES] invalid padding")
|
||||||
|
}
|
||||||
|
padding := int(data[length-1])
|
||||||
|
if padding == 0 || padding > length {
|
||||||
|
return nil, errors.New("[Cryptography AES] invalid padding")
|
||||||
|
}
|
||||||
|
return data[:length-padding], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESCBCEncrypt(plaintext, key []byte) (string, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext = pkcs7Pad(plaintext, block.BlockSize())
|
||||||
|
|
||||||
|
iv, err := randomBytes(block.BlockSize())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(plaintext, plaintext)
|
||||||
|
|
||||||
|
out := append(iv, plaintext...)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESCBCDecrypt(encoded string, key []byte) ([]byte, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < block.BlockSize() {
|
||||||
|
return nil, errors.New("[Cryptography AES] ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := data[:block.BlockSize()]
|
||||||
|
data = data[block.BlockSize():]
|
||||||
|
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
mode.CryptBlocks(data, data)
|
||||||
|
|
||||||
|
return pkcs7Unpad(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESCFBEncrypt(plaintext, key []byte) (string, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv, err := randomBytes(block.BlockSize())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := cipher.NewCFBEncrypter(block, iv)
|
||||||
|
stream.XORKeyStream(plaintext, plaintext)
|
||||||
|
|
||||||
|
out := append(iv, plaintext...)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AESCFBDecrypt(encoded string, key []byte) ([]byte, error) {
|
||||||
|
key, err := normalizeKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < block.BlockSize() {
|
||||||
|
return nil, errors.New("[Cryptography AES] ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := data[:block.BlockSize()]
|
||||||
|
data = data[block.BlockSize():]
|
||||||
|
|
||||||
|
stream := cipher.NewCFBDecrypter(block, iv)
|
||||||
|
stream.XORKeyStream(data, data)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
20
internal/cryptography/base64.go
Normal file
20
internal/cryptography/base64.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsBase64Std(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if len(s)%4 != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := base64.StdEncoding.Strict().DecodeString(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
22
internal/cryptography/bcrypt.go
Normal file
22
internal/cryptography/bcrypt.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package cryptography
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
func BcryptHash(input string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword(
|
||||||
|
[]byte(input),
|
||||||
|
bcrypt.DefaultCost,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BcryptVerify(input string, hashed string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword(
|
||||||
|
[]byte(hashed),
|
||||||
|
[]byte(input),
|
||||||
|
)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
84
internal/email/email.go
Normal file
84
internal/email/email.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
gomail "gopkg.in/gomail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
dialer *gomail.Dialer
|
||||||
|
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
username string
|
||||||
|
security string
|
||||||
|
insecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) NewSMTPClient() (*Client, error) {
|
||||||
|
host := viper.GetString("email.host")
|
||||||
|
port := viper.GetInt("email.port")
|
||||||
|
user := viper.GetString("email.username")
|
||||||
|
pass := viper.GetString("email.password")
|
||||||
|
|
||||||
|
security := strings.ToLower(viper.GetString("email.security"))
|
||||||
|
insecure := viper.GetBool("email.insecure_skip_verify")
|
||||||
|
|
||||||
|
if host == "" || port == 0 || user == "" {
|
||||||
|
return nil, errors.New("[Email] SMTP config not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pass == "" {
|
||||||
|
return nil, errors.New("[Email] SMTP basic auth requires email.password")
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := gomail.NewDialer(host, port, user, pass)
|
||||||
|
dialer.TLSConfig = &tls.Config{
|
||||||
|
ServerName: host,
|
||||||
|
InsecureSkipVerify: insecure,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch security {
|
||||||
|
case "ssl":
|
||||||
|
dialer.SSL = true
|
||||||
|
case "starttls":
|
||||||
|
dialer.SSL = false
|
||||||
|
case "plain", "":
|
||||||
|
dialer.SSL = false
|
||||||
|
dialer.TLSConfig = nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("[Email] unknown smtp security mode: " + security)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
dialer: dialer,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
username: user,
|
||||||
|
security: security,
|
||||||
|
insecure: insecure,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Send(from, to, subject, html string) (string, error) {
|
||||||
|
if c.dialer == nil {
|
||||||
|
return "", errors.New("[Email] SMTP dialer not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", from)
|
||||||
|
m.SetHeader("To", to)
|
||||||
|
m.SetHeader("Subject", subject)
|
||||||
|
m.SetBody("text/html", html)
|
||||||
|
|
||||||
|
if err := c.dialer.DialAndSend(m); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Format(time.RFC3339Nano), nil
|
||||||
|
}
|
||||||
76
internal/exception/builder.go
Normal file
76
internal/exception/builder.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package exception
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 12 chars len
|
||||||
|
// :1=status
|
||||||
|
// :3=service
|
||||||
|
// :2=endpoint
|
||||||
|
// :1=common/specific
|
||||||
|
// :5=original
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
Status string
|
||||||
|
Service string
|
||||||
|
Endpoint string
|
||||||
|
Type string
|
||||||
|
Original string
|
||||||
|
Error error
|
||||||
|
ErrorCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetStatus(s string) *Builder {
|
||||||
|
self.Status = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetService(s string) *Builder {
|
||||||
|
self.Service = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetEndpoint(s string) *Builder {
|
||||||
|
self.Endpoint = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetType(s string) *Builder {
|
||||||
|
self.Type = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetOriginal(s string) *Builder {
|
||||||
|
self.Original = s
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) SetError(e error) *Builder {
|
||||||
|
self.Error = e
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) build() {
|
||||||
|
self.ErrorCode = fmt.Sprintf("%s%s%s%s%s",
|
||||||
|
self.Status,
|
||||||
|
self.Service,
|
||||||
|
self.Endpoint,
|
||||||
|
self.Type,
|
||||||
|
self.Original,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) Throw(ctx context.Context) *Builder {
|
||||||
|
self.build()
|
||||||
|
if self.Error != nil {
|
||||||
|
ErrorHandler(ctx, self.Status, self.ErrorCode, self.Error)
|
||||||
|
}
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Builder) String() string {
|
||||||
|
self.build()
|
||||||
|
return self.ErrorCode
|
||||||
|
}
|
||||||
19
internal/exception/error.go
Normal file
19
internal/exception/error.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package exception
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrorHandler(ctx context.Context, status string, errorCode string, err error) {
|
||||||
|
switch status {
|
||||||
|
case StatusSuccess:
|
||||||
|
slog.InfoContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
|
||||||
|
case StatusUser:
|
||||||
|
slog.WarnContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
|
||||||
|
case StatusServer:
|
||||||
|
slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
|
||||||
|
case StatusClient:
|
||||||
|
slog.ErrorContext(ctx, "Service exception! ErrId: "+errorCode, "id", errorCode, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
116
internal/kyc/cnrid.go
Normal file
116
internal/kyc/cnrid.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
alicloudauth20190307 "github.com/alibabacloud-go/cloudauth-20190307/v4/client"
|
||||||
|
aliopenapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||||
|
aliutil "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||||
|
alitea "github.com/alibabacloud-go/tea/tea"
|
||||||
|
alicredential "github.com/aliyun/credentials-go/credentials"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CNRidMD5AliEnc(kyc *CNRidInfo) (*AliCloudAuth, error) {
|
||||||
|
// MD5 Legal Name rule: First Chinese char md5enc, remaining plain, at least 2 Chinese chars
|
||||||
|
if len(kyc.LegalName) < 2 || utf8.RuneCountInString(kyc.LegalName) < 2 {
|
||||||
|
return nil, fmt.Errorf("input string must have at least 2 Chinese characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
lnFirstRune, size := utf8.DecodeRuneInString(kyc.LegalName)
|
||||||
|
if lnFirstRune == utf8.RuneError {
|
||||||
|
return nil, fmt.Errorf("invalid first character")
|
||||||
|
}
|
||||||
|
|
||||||
|
lnHash := md5.New()
|
||||||
|
lnHash.Write([]byte(string(lnFirstRune)))
|
||||||
|
lnFirstHash := hex.EncodeToString(lnHash.Sum(nil))
|
||||||
|
|
||||||
|
lnRemaining := kyc.LegalName[size:]
|
||||||
|
|
||||||
|
ln := lnFirstHash + lnRemaining
|
||||||
|
|
||||||
|
// MD5 Resident Id rule: First 6 char plain, middle birthdate md5enc, last 4 char plain, at least 18 chars
|
||||||
|
if len(kyc.ResidentId) < 18 {
|
||||||
|
return nil, fmt.Errorf("input string must have at least 18 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
ridPrefix := kyc.ResidentId[:6]
|
||||||
|
ridSuffix := kyc.ResidentId[len(kyc.ResidentId)-4:]
|
||||||
|
ridMiddle := kyc.ResidentId[6 : len(kyc.ResidentId)-4]
|
||||||
|
|
||||||
|
ridHash := md5.New()
|
||||||
|
ridHash.Write([]byte(ridMiddle))
|
||||||
|
ridMiddleHash := hex.EncodeToString(ridHash.Sum(nil))
|
||||||
|
|
||||||
|
rid := ridPrefix + ridMiddleHash + ridSuffix
|
||||||
|
|
||||||
|
// Aliyun Id2MetaVerify API Params
|
||||||
|
var kycAli AliCloudAuth
|
||||||
|
kycAli.ParamType = "md5"
|
||||||
|
kycAli.UserName = ln
|
||||||
|
kycAli.IdentifyNum = rid
|
||||||
|
|
||||||
|
return &kycAli, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AliId2MetaVerify(kycAli *AliCloudAuth) (*string, error) {
|
||||||
|
// Create aliyun openapi credential
|
||||||
|
credentialConfig := new(alicredential.Config).
|
||||||
|
SetType("access_key").
|
||||||
|
SetAccessKeyId(viper.GetString("kyc.ali_access_key_id")).
|
||||||
|
SetAccessKeySecret(viper.GetString("kyc.ali_access_key_secret"))
|
||||||
|
credential, err := alicredential.NewCredential(credentialConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create aliyun cloudauth client
|
||||||
|
config := &aliopenapi.Config{
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
|
config.Endpoint = alitea.String("cloudauth.aliyuncs.com")
|
||||||
|
client := &alicloudauth20190307.Client{}
|
||||||
|
client, err = alicloudauth20190307.NewClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Id2MetaVerify request
|
||||||
|
id2MetaVerifyRequest := &alicloudauth20190307.Id2MetaVerifyRequest{
|
||||||
|
ParamType: &kycAli.ParamType,
|
||||||
|
UserName: &kycAli.UserName,
|
||||||
|
IdentifyNum: &kycAli.IdentifyNum,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client runtime request
|
||||||
|
runtime := &aliutil.RuntimeOptions{}
|
||||||
|
resp, tryErr := func() (*alicloudauth20190307.Id2MetaVerifyResponse, error) {
|
||||||
|
defer func() {
|
||||||
|
if r := alitea.Recover(recover()); r != nil {
|
||||||
|
err = r
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resp, err := client.Id2MetaVerifyWithOptions(id2MetaVerifyRequest, runtime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Try error handler ??? from ali generated sdk
|
||||||
|
if tryErr != nil {
|
||||||
|
var error = &alitea.SDKError{}
|
||||||
|
if t, ok := tryErr.(*alitea.SDKError); ok {
|
||||||
|
error = t
|
||||||
|
} else {
|
||||||
|
error.Message = alitea.String(tryErr.Error())
|
||||||
|
}
|
||||||
|
return nil, error
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Body.ResultObject.BizCode, err
|
||||||
|
}
|
||||||
39
internal/kyc/crypto.go
Normal file
39
internal/kyc/crypto.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"nixcn-cms/internal/cryptography"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncodeAES(kyc any) (*string, error) {
|
||||||
|
plainJson, err := json.Marshal(kyc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesKey := viper.GetString("secrets.kyc_info_key")
|
||||||
|
encrypted, err := cryptography.AESCBCEncrypt(plainJson, []byte(aesKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &encrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeAES(cipherStr string) (any, error) {
|
||||||
|
aesKey := viper.GetString("secrets.kyc_info_key")
|
||||||
|
plainBytes, err := cryptography.AESCBCDecrypt(cipherStr, []byte(aesKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kycInfo any
|
||||||
|
if err := json.Unmarshal(plainBytes, &kycInfo); err != nil {
|
||||||
|
return nil, errors.New("[KYC] invalid decrypted json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &kycInfo, nil
|
||||||
|
}
|
||||||
91
internal/kyc/passport.go
Normal file
91
internal/kyc/passport.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateCreated = "CREATED"
|
||||||
|
StateInitiated = "INITIATED"
|
||||||
|
StateFailed = "FAILED"
|
||||||
|
StateAborted = "ABORTED"
|
||||||
|
StateCompleted = "COMPLETED"
|
||||||
|
StateRejected = "REJECTED"
|
||||||
|
StateApproved = "APPROVED"
|
||||||
|
)
|
||||||
|
|
||||||
|
func doPassportRequest(ctx context.Context, method, path string, body any, target any) error {
|
||||||
|
publicKey := viper.GetString("kyc.passport_reader_public_key")
|
||||||
|
secret := viper.GetString("kyc.passport_reader_secret")
|
||||||
|
baseURL := "https://passportreader.app/api/v1"
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
jsonData, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal request failed: %w", err)
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewBuffer(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetBasicAuth(publicKey, secret)
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("http request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("api error: status %d, body %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
if target != nil {
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
|
||||||
|
return fmt.Errorf("decode response failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateSession(ctx context.Context) (*PassportReaderSessionResponse, error) {
|
||||||
|
var resp PassportReaderSessionResponse
|
||||||
|
err := doPassportRequest(ctx, "POST", "/session.create", nil, &resp)
|
||||||
|
return &resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSessionState(ctx context.Context, sessionID int) (string, error) {
|
||||||
|
payload := PassportReaderGetSessionRequest{ID: sessionID}
|
||||||
|
var resp PassportReaderStateResponse
|
||||||
|
err := doPassportRequest(ctx, "POST", "/session.state", payload, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.State, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSessionDetails(ctx context.Context, sessionID int) (*PassportReaderSessionDetailResponse, error) {
|
||||||
|
payload := PassportReaderGetSessionRequest{ID: sessionID}
|
||||||
|
var resp PassportReaderSessionDetailResponse
|
||||||
|
err := doPassportRequest(ctx, "POST", "/session.get", payload, &resp)
|
||||||
|
return &resp, err
|
||||||
|
}
|
||||||
50
internal/kyc/types.go
Normal file
50
internal/kyc/types.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package kyc
|
||||||
|
|
||||||
|
type CNRidInfo struct {
|
||||||
|
LegalName string `json:"legal_name"`
|
||||||
|
ResidentId string `json:"resident_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportResp struct {
|
||||||
|
GivenNames string `json:"given_names"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
Nationality string `json:"nationality"`
|
||||||
|
DateOfBirth string `json:"date_of_birth"`
|
||||||
|
DocumentType string `json:"document_type"`
|
||||||
|
DocumentNumber string `json:"document_number"`
|
||||||
|
ExpiryDate string `json:"expiry_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliCloudAuth struct {
|
||||||
|
ParamType string `json:"param_type"`
|
||||||
|
IdentifyNum string `json:"identify_num"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportReaderSessionResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportReaderGetSessionRequest struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportReaderStateResponse struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PassportReaderSessionDetailResponse struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
GivenNames string `json:"given_names"`
|
||||||
|
Surname string `json:"surname"`
|
||||||
|
Nationality string `json:"nationality"`
|
||||||
|
DateOfBirth string `json:"date_of_birth"`
|
||||||
|
DocumentType string `json:"document_type"`
|
||||||
|
DocumentNumber string `json:"document_number"`
|
||||||
|
ExpiryDate string `json:"expiry_date"`
|
||||||
|
}
|
||||||
34
internal/turnstile/turnstile.go
Normal file
34
internal/turnstile/turnstile.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package turnstile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VerifyTurnstile(token, ip string) (bool, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("secret", viper.GetString("secrets.turnstile_secret"))
|
||||||
|
form.Set("response", token)
|
||||||
|
form.Set("remoteip", ip)
|
||||||
|
|
||||||
|
resp, err := http.PostForm(
|
||||||
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
form,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Success, nil
|
||||||
|
}
|
||||||
35
justfile
Normal file
35
justfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
project_name := "nixcn-cms"
|
||||||
|
go_cmd := `realpath $(which go)`
|
||||||
|
project_dir := justfile_directory()
|
||||||
|
output_dir := join(project_dir, ".outputs")
|
||||||
|
server_exec_path := join(output_dir, project_name)
|
||||||
|
server_entry := "main.go"
|
||||||
|
|
||||||
|
install:
|
||||||
|
go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
cd {{ project_dir }} && go mod tidy
|
||||||
|
|
||||||
|
generate:
|
||||||
|
cd {{ project_dir }} && go generate .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
mkdir -p .outputs
|
||||||
|
find .outputs -mindepth 1 ! -path .outputs/config.yaml -exec rm -rf {} +
|
||||||
|
|
||||||
|
build:
|
||||||
|
{{ go_cmd }} build -o {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }} {{ server_entry }}
|
||||||
|
|
||||||
|
run:
|
||||||
|
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}{{ if os() == "windows" { ".exe" } else { "" } }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} GO_ENV=test go test -C .. ./...
|
||||||
|
|
||||||
|
watch:
|
||||||
|
watchexec -r -e go,yaml,tpl -i '.devenv/**' -i '.direnv/**' -i 'client/**' -i 'vendor/**' 'go build -o {{ server_exec_path }} . && cd {{ output_dir }} && CONFIG_PATH={{ output_dir }} {{ server_exec_path }}'
|
||||||
|
|
||||||
|
dev: clean install generate
|
||||||
|
devenv up --verbose
|
||||||
|
|
||||||
|
back: clean install generate
|
||||||
|
devenv up postgres redis lgtm --verbose
|
||||||
28
logger/gorm.go
Normal file
28
logger/gorm.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SlogWriter struct{}
|
||||||
|
|
||||||
|
func (w *SlogWriter) Printf(format string, args ...any) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
slog.Info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GormLogger() logger.Interface {
|
||||||
|
return logger.New(
|
||||||
|
&SlogWriter{},
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: 200 * time.Millisecond,
|
||||||
|
LogLevel: logger.Warn,
|
||||||
|
IgnoreRecordNotFoundError: true,
|
||||||
|
Colorful: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
97
logger/slog.go
Normal file
97
logger/slog.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiHandler struct {
|
||||||
|
handlers []slog.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) Enabled(ctx context.Context, l slog.Level) bool {
|
||||||
|
for _, h := range m.handlers {
|
||||||
|
if h.Enabled(ctx, l) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
span := trace.SpanFromContext(ctx)
|
||||||
|
if span.SpanContext().HasTraceID() {
|
||||||
|
r.AddAttrs(
|
||||||
|
slog.String("trace_id", span.SpanContext().TraceID().String()),
|
||||||
|
slog.String("span_id", span.SpanContext().SpanID().String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range m.handlers {
|
||||||
|
_ = h.Handle(ctx, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
newHandlers := make([]slog.Handler, len(m.handlers))
|
||||||
|
for i, h := range m.handlers {
|
||||||
|
newHandlers[i] = h.WithAttrs(attrs)
|
||||||
|
}
|
||||||
|
return &multiHandler{handlers: newHandlers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiHandler) WithGroup(name string) slog.Handler {
|
||||||
|
newHandlers := make([]slog.Handler, len(m.handlers))
|
||||||
|
for i, h := range m.handlers {
|
||||||
|
newHandlers[i] = h.WithGroup(name)
|
||||||
|
}
|
||||||
|
return &multiHandler{handlers: newHandlers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
levelStr := strings.ToLower(viper.GetString("server.log_level"))
|
||||||
|
var level slog.Level
|
||||||
|
switch levelStr {
|
||||||
|
case "debug":
|
||||||
|
level = slog.LevelDebug
|
||||||
|
case "warn":
|
||||||
|
level = slog.LevelWarn
|
||||||
|
case "error":
|
||||||
|
level = slog.LevelError
|
||||||
|
default:
|
||||||
|
level = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var writer io.Writer = os.Stdout
|
||||||
|
if level == slog.LevelDebug {
|
||||||
|
file, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
|
||||||
|
writer = io.MultiWriter(os.Stdout, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
localHandler := slog.NewJSONHandler(writer, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
AddSource: true,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
return slog.String(a.Key, a.Value.Time().Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
otelHandler := otelslog.NewHandler(viper.GetString("server.service_name"))
|
||||||
|
|
||||||
|
combinedHandler := &multiHandler{
|
||||||
|
handlers: []slog.Handler{localHandler, otelHandler},
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(combinedHandler))
|
||||||
|
}
|
||||||
31
main.go
Normal file
31
main.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"nixcn-cms/config"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/logger"
|
||||||
|
"nixcn-cms/server"
|
||||||
|
"nixcn-cms/tracer"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
// OTEL
|
||||||
|
ctx := context.Background()
|
||||||
|
shutdown := tracer.Init(ctx)
|
||||||
|
defer func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := shutdown(ctx); err != nil {
|
||||||
|
slog.Error("[Main] Tracer shutdown failed!", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Init()
|
||||||
|
data.Init(ctx)
|
||||||
|
server.Start(ctx)
|
||||||
|
}
|
||||||
27
middleware/api_version.go
Normal file
27
middleware/api_version.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiVersionCheck() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
apiVersion := c.GetHeader("X-Api-Version")
|
||||||
|
if apiVersion == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.MiddlewareServiceApiVersion).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.ApiVersionNotFound).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpAbort(c, 400, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
56
middleware/gin_logger.go
Normal file
56
middleware/gin_logger.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GinLogger() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var body []byte
|
||||||
|
if c.Request.Body != nil {
|
||||||
|
body, _ = io.ReadAll(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
headerJSON, _ := json.Marshal(c.Request.Header)
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var errorMessage string
|
||||||
|
if len(c.Errors) > 0 {
|
||||||
|
errorMessage = c.Errors.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []any{
|
||||||
|
"status", c.Writer.Status(),
|
||||||
|
"method", c.Request.Method,
|
||||||
|
"uri", c.Request.RequestURI,
|
||||||
|
"ip", c.ClientIP(),
|
||||||
|
"latency", time.Since(startTime).String(),
|
||||||
|
"user_agent", c.Request.UserAgent(),
|
||||||
|
"headers", string(headerJSON),
|
||||||
|
"request_body", string(body),
|
||||||
|
"errors", errorMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
status := c.Writer.Status()
|
||||||
|
if len(c.Errors) > 0 || status >= 500 {
|
||||||
|
slog.ErrorContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
|
||||||
|
} else if status >= 400 {
|
||||||
|
slog.WarnContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
|
||||||
|
} else {
|
||||||
|
slog.InfoContext(ctx, fmt.Sprintf("%d %s %s", c.Writer.Status(), c.Request.Method, c.Request.RequestURI), fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
middleware/jwt.go
Normal file
36
middleware/jwt.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/internal/authtoken"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JWTAuth() gin.HandlerFunc {
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
auth := c.GetHeader("Authorization")
|
||||||
|
|
||||||
|
authtoken := new(authtoken.Token)
|
||||||
|
uid, err := authtoken.HeaderVerify(c, auth)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.MiddlewareServiceJwt).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUnauthorized).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpAbort(c, 401, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("user_id", uid)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
85
middleware/permission.go
Normal file
85
middleware/permission.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Permission(requiredLevel uint) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var permissionLevel uint
|
||||||
|
permissionLevelPrev, ok := c.Get("permission_level")
|
||||||
|
if !ok {
|
||||||
|
userIdOrig, ok := c.Get("user_id")
|
||||||
|
if !ok || userIdOrig.(string) == "" {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.MiddlewareServicePermission).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorMissingUserId).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpAbort(c, 401, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := uuid.Parse(userIdOrig.(string))
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.MiddlewareServicePermission).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUuidParseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
utils.HttpAbort(c, 500, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userData, err := new(data.User).GetByUserId(c, &userId)
|
||||||
|
if err != nil {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.MiddlewareServicePermission).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorUserNotFound).
|
||||||
|
SetError(err).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpAbort(c, 404, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionLevel = userData.PermissionLevel
|
||||||
|
c.Set("permission_level", userData.PermissionLevel)
|
||||||
|
} else {
|
||||||
|
permissionLevel = permissionLevelPrev.(uint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if permissionLevel < requiredLevel {
|
||||||
|
errorCode := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.MiddlewareServicePermission).
|
||||||
|
SetEndpoint(exception.EndpointMiddlewareService).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorPermissionDenied).
|
||||||
|
Throw(c).
|
||||||
|
String()
|
||||||
|
|
||||||
|
utils.HttpAbort(c, 403, errorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
65
server/server.go
Normal file
65
server/server.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"nixcn-cms/api"
|
||||||
|
"nixcn-cms/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||||
|
|
||||||
|
_ "nixcn-cms/docs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title NixCN CMS API
|
||||||
|
// @version 1.0
|
||||||
|
// @description API Docs based on Gin framework
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.url http://www.swagger.io/support
|
||||||
|
// @contact.email support@swagger.io
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
// @host localhost:8000
|
||||||
|
// @BasePath /app/api/v1
|
||||||
|
// @schemes http https
|
||||||
|
func Start(ctx context.Context) {
|
||||||
|
if !viper.GetBool("server.debug_mode") {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
gin.DisableConsoleColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(otelgin.Middleware(viper.GetString("server.service_name")))
|
||||||
|
r.Use(middleware.GinLogger())
|
||||||
|
|
||||||
|
if viper.GetBool("server.debug_mode") {
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Use(gin.Recovery())
|
||||||
|
|
||||||
|
api.Handler(r.Group("/app/api/v1"))
|
||||||
|
|
||||||
|
// Start http server
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: viper.GetString("server.address"),
|
||||||
|
Handler: r,
|
||||||
|
BaseContext: func(net.Listener) context.Context { return ctx },
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.InfoContext(ctx, "[Server] Starting server on "+viper.GetString("server.address"))
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
slog.ErrorContext(ctx, "[Server] Error starting server!", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
service/service_agenda/service.go
Normal file
11
service/service_agenda/service.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package service_agenda
|
||||||
|
|
||||||
|
type AgendaService interface {
|
||||||
|
Submit(*SubmitPayload) *SubmitResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgendaServiceImpl struct{}
|
||||||
|
|
||||||
|
func NewAgendaService() AgendaService {
|
||||||
|
return &AgendaServiceImpl{}
|
||||||
|
}
|
||||||
131
service/service_agenda/submit.go
Normal file
131
service/service_agenda/submit.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package service_agenda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubmitData struct {
|
||||||
|
EventId uuid.UUID `json:"event_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID `json:"user_id"`
|
||||||
|
Data *SubmitData
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitResponse struct {
|
||||||
|
AgendaId uuid.UUID `json:"agenda_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *SubmitResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AgendaServiceImpl) Submit(payload *SubmitPayload) (result *SubmitResult) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
attendanceData, err := new(data.Attendance).
|
||||||
|
GetAttendance(payload.Context, payload.UserId, payload.Data.EventId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &SubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 403,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &SubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agendaModel := new(data.Agenda).
|
||||||
|
SetAttendanceId(attendanceData.AttendanceId).
|
||||||
|
SetName(payload.Data.Name).
|
||||||
|
SetDescription(payload.Data.Description).
|
||||||
|
SetIsApproved(false)
|
||||||
|
|
||||||
|
err = agendaModel.Create(payload.Context)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &SubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
successException := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceAgenda).
|
||||||
|
SetEndpoint(exception.EndpointAgendaServiceSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
resultData := &SubmitResponse{
|
||||||
|
AgendaId: agendaModel.AgendaId,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &SubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: successException,
|
||||||
|
},
|
||||||
|
Data: resultData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
129
service/service_auth/exchange.go
Normal file
129
service/service_auth/exchange.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/authcode"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExchangeData struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExchangePayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID
|
||||||
|
Data *ExchangeData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExchangeResponse struct {
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExchangeResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *ExchangeResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AuthServiceImpl) Exchange(payload *ExchangePayload) (result *ExchangeResult) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userData, err := new(data.User).
|
||||||
|
GetByUserId(payload.Context, &payload.UserId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthExchangeGetUserIdFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &ExchangeResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, userData.Email)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthExchangeCodeGenFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &ExchangeResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := url.Parse(payload.Data.RedirectUri)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthExchangeInvalidRedirectUri).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &ExchangeResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Query()
|
||||||
|
query.Set("code", code)
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceExchange).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
resultData := &ExchangeResponse{url.String()}
|
||||||
|
|
||||||
|
result = &ExchangeResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: resultData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
194
service/service_auth/magic.go
Normal file
194
service/service_auth/magic.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"nixcn-cms/internal/authcode"
|
||||||
|
"nixcn-cms/internal/email"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/internal/turnstile"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MagicData struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
TurnstileToken string `json:"turnstile_token"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MagicPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *MagicData
|
||||||
|
}
|
||||||
|
|
||||||
|
type MagicResponse struct {
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MagicResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *MagicResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AuthServiceImpl) Magic(payload *MagicPayload) (result *MagicResult) {
|
||||||
|
var ok bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if viper.GetBool("server.debug_mode") {
|
||||||
|
goto BypassCfTurnstile
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = turnstile.VerifyTurnstile(payload.Data.TurnstileToken, payload.Data.ClientIP)
|
||||||
|
if err != nil || !ok {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthMagicTurnstileFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 403,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
BypassCfTurnstile:
|
||||||
|
code, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, payload.Data.Email)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthMagicCodeGenFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
externalUrl := viper.GetString("server.external_url")
|
||||||
|
url, err := url.Parse(externalUrl)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthMagicInvalidExternalUrl).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Path = "/app/api/v1/auth/redirect"
|
||||||
|
query := url.Query()
|
||||||
|
query.Set("code", code)
|
||||||
|
query.Set("redirect_uri", payload.Data.RedirectUri)
|
||||||
|
query.Set("state", payload.Data.State)
|
||||||
|
query.Set("client_id", payload.Data.ClientId)
|
||||||
|
url.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
debugMode := viper.GetBool("server.debug_mode")
|
||||||
|
if debugMode {
|
||||||
|
uriData := struct {
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
}{url.String()}
|
||||||
|
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: &MagicResponse{uriData.Uri},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
emailClient, err := new(email.Client).NewSMTPClient()
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthMagicInvalidEmailConfig).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emailClient.Send(
|
||||||
|
"NixCN CMS <cms@yuri.nix.org.cn>",
|
||||||
|
payload.Data.Email,
|
||||||
|
"NixCN CMS Email Verify",
|
||||||
|
"<p>Click the link below to verify your email. This link will expire in 10 minutes.</p><a href="+url.String()+">"+url.String()+"</a>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceMagic).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &MagicResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
211
service/service_auth/redirect.go
Normal file
211
service/service_auth/redirect.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/authcode"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedirectData struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *RedirectData
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedirectResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AuthServiceImpl) Redirect(payload *RedirectPayload) (result *RedirectResult) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code)
|
||||||
|
if !ok {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRedirectTokenInvalid).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 403,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userData, err := new(data.User).
|
||||||
|
GetByEmail(payload.Context, &authCode.Email)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
userData = &data.User{}
|
||||||
|
userData.UUID = uuid.New()
|
||||||
|
userData.UserId = uuid.New()
|
||||||
|
userData.Email = authCode.Email
|
||||||
|
userData.Username = userData.UserId.String()
|
||||||
|
userData.PermissionLevel = 10
|
||||||
|
if err := userData.Create(payload.Context); err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInternal).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInternal).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData := new(data.Client)
|
||||||
|
client, err := clientData.GetClientByClientId(payload.Context, payload.Data.ClientId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRedirectClientNotFound).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.ValidateRedirectURI(payload.Data.RedirectUri); err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRedirectUriMismatch).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newCode, err := authcode.NewAuthCode(payload.Context, payload.Data.ClientId, authCode.Email)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInternal).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUrl, err := url.Parse(payload.Data.RedirectUri)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRedirectInvalidUri).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := targetUrl.Query()
|
||||||
|
query.Set("code", newCode)
|
||||||
|
if payload.Data.State != "" {
|
||||||
|
query.Set("state", payload.Data.State)
|
||||||
|
}
|
||||||
|
targetUrl.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
result = &RedirectResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRedirect).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: targetUrl.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
99
service/service_auth/refresh.go
Normal file
99
service/service_auth/refresh.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/internal/authtoken"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RefreshData struct {
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *RefreshData
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *TokenResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AuthServiceImpl) Refresh(payload *RefreshPayload) (result *RefreshResult) {
|
||||||
|
JwtTool := authtoken.Token{
|
||||||
|
Application: viper.GetString("server.application"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Refresh Access Token
|
||||||
|
accessToken, err := JwtTool.RefreshAccessToken(payload.Context, payload.Data.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRefresh).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRefreshInvalidToken).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RefreshResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 401,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Renew Refresh Token (Rotation)
|
||||||
|
refreshToken, err := JwtTool.RenewRefreshToken(payload.Context, payload.Data.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRefresh).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthRefreshRenewFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RefreshResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Success Assignment
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceRefresh).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &RefreshResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
Data: &TokenResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
15
service/service_auth/service.go
Normal file
15
service/service_auth/service.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
type AuthService interface {
|
||||||
|
Exchange(*ExchangePayload) *ExchangeResult
|
||||||
|
Magic(*MagicPayload) *MagicResult
|
||||||
|
Redirect(*RedirectPayload) *RedirectResult
|
||||||
|
Token(*TokenPayload) *TokenResult
|
||||||
|
Refresh(*RefreshPayload) *RefreshResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthServiceImpl struct{}
|
||||||
|
|
||||||
|
func NewAuthService() AuthService {
|
||||||
|
return &AuthServiceImpl{}
|
||||||
|
}
|
||||||
118
service/service_auth/token.go
Normal file
118
service/service_auth/token.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package service_auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/authcode"
|
||||||
|
"nixcn-cms/internal/authtoken"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenData struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *TokenData
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *TokenResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *AuthServiceImpl) Token(payload *TokenPayload) (result *TokenResult) {
|
||||||
|
authCode, ok := authcode.VerifyAuthCode(payload.Context, payload.Data.Code)
|
||||||
|
if !ok {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthTokenInvalidToken).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &TokenResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 403,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userData := new(data.User)
|
||||||
|
user, err := userData.GetByEmail(payload.Context, &authCode.Email)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInternal).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &TokenResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
JwtTool := authtoken.Token{
|
||||||
|
Application: viper.GetString("server.application"),
|
||||||
|
}
|
||||||
|
accessToken, refreshToken, err := JwtTool.IssueTokens(payload.Context, authCode.ClientId, user.UserId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.AuthTokenGenFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &TokenResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &TokenResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceAuth).
|
||||||
|
SetEndpoint(exception.EndpointAuthServiceToken).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: &TokenResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
201
service/service_event/attendance_list.go
Normal file
201
service/service_event/attendance_list.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package service_event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/cryptography"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/internal/kyc"
|
||||||
|
"nixcn-cms/service/service_user"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AttendanceListData struct {
|
||||||
|
EventId uuid.UUID `json:"event_id" form:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceListPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *AttendanceListData
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceListResponse struct {
|
||||||
|
AttendanceId string `json:"attendance_id"`
|
||||||
|
UserInfo service_user.UserInfoData `json:"user_info"`
|
||||||
|
KycType string `json:"kyc_type"`
|
||||||
|
KycInfo any `json:"kyc_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttendanceListResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data []AttendanceListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) AttendanceList(payload *AttendanceListPayload) (result *AttendanceListResult) {
|
||||||
|
attList, err := new(data.Attendance).GetAttendanceListByEventId(payload.Context, payload.Data.EventId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventAttendanceListError).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseList := make([]AttendanceListResponse, 0)
|
||||||
|
kycRepo := new(data.Kyc)
|
||||||
|
userRepo := new(data.User)
|
||||||
|
|
||||||
|
for _, item := range *attList {
|
||||||
|
var userInfo service_user.UserInfoData
|
||||||
|
var kycType string
|
||||||
|
var kycInfo any
|
||||||
|
|
||||||
|
userData, err := userRepo.GetByUserId(payload.Context, &item.UserId)
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userData != nil {
|
||||||
|
userInfo = service_user.UserInfoData{
|
||||||
|
UserId: userData.UserId,
|
||||||
|
Email: userData.Email,
|
||||||
|
Username: &userData.Username,
|
||||||
|
Nickname: &userData.Nickname,
|
||||||
|
Subtitle: &userData.Subtitle,
|
||||||
|
Avatar: &userData.Avatar,
|
||||||
|
Bio: &userData.Bio,
|
||||||
|
PermissionLevel: &userData.PermissionLevel,
|
||||||
|
AllowPublic: &userData.AllowPublic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.KycId != uuid.Nil {
|
||||||
|
kycData, err := kycRepo.GetByKycId(payload.Context, &item.KycId)
|
||||||
|
|
||||||
|
if err == nil && kycData != nil {
|
||||||
|
kycType = kycData.Type
|
||||||
|
|
||||||
|
// AES Decrypt
|
||||||
|
decodedKycInfo, err := cryptography.AESCBCDecrypt(string(kycData.KycInfo), []byte(viper.GetString("secrets.kyc_info_key")))
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventAttendanceKycInfoDecryptFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{HttpCode: 500, Exception: exc},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Unmarshal
|
||||||
|
switch kycType {
|
||||||
|
case "cnrid":
|
||||||
|
var kycDetail kyc.CNRidInfo
|
||||||
|
if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorJsonDecodeFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kycInfo = kycDetail
|
||||||
|
|
||||||
|
case "passport":
|
||||||
|
var kycDetail kyc.PassportResp
|
||||||
|
if err := json.Unmarshal(decodedKycInfo, &kycDetail); err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceAttendanceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorJsonDecodeFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kycInfo = kycDetail
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseList = append(responseList, AttendanceListResponse{
|
||||||
|
AttendanceId: item.AttendanceId.String(),
|
||||||
|
UserInfo: userInfo,
|
||||||
|
KycType: kycType,
|
||||||
|
KycInfo: kycInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &AttendanceListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: responseList,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
269
service/service_event/checkin.go
Normal file
269
service/service_event/checkin.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package service_event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckinData struct {
|
||||||
|
EventId uuid.UUID `json:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID
|
||||||
|
Data *CheckinData
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinResponse struct {
|
||||||
|
CheckinCode *string `json:"checkin_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *CheckinResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) Checkin(payload *CheckinPayload) (result *CheckinResult) {
|
||||||
|
attendandeData, err := new(data.Attendance).
|
||||||
|
GetAttendance(payload.Context, payload.UserId, payload.Data.EventId)
|
||||||
|
if err != nil {
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if attendandeData == nil {
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 403,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventData, err := new(data.Event).
|
||||||
|
GetEventById(payload.Context, payload.Data.EventId)
|
||||||
|
|
||||||
|
if err != nil || eventData == nil {
|
||||||
|
code := 500
|
||||||
|
if eventData == nil {
|
||||||
|
code = 404
|
||||||
|
}
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: code,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if attendandeData.KycId == uuid.Nil && eventData.EnableKYC == true {
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attendance := &data.Attendance{UserId: payload.UserId}
|
||||||
|
code, err := attendance.GenCheckinCode(payload.Context, payload.Data.EventId)
|
||||||
|
if err != nil {
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventCheckinGenCodeFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &CheckinResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckin).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: &CheckinResponse{code},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinSubmitData struct {
|
||||||
|
CheckinCode string `json:"checkin_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinSubmitPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
Data *CheckinSubmitData
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinSubmitResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) CheckinSubmit(payload *CheckinSubmitPayload) (result *CheckinSubmitResult) {
|
||||||
|
attendanceData := new(data.Attendance)
|
||||||
|
err := attendanceData.VerifyCheckinCode(payload.Context, payload.Data.CheckinCode)
|
||||||
|
if err != nil {
|
||||||
|
result = &CheckinSubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &CheckinSubmitResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinSubmit).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinQueryData struct {
|
||||||
|
EventId uuid.UUID `json:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinQueryPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID
|
||||||
|
Data *CheckinQueryData
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinQueryResponse struct {
|
||||||
|
CheckinAt *time.Time `json:"checkin_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckinQueryResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *CheckinQueryResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) CheckinQuery(payload *CheckinQueryPayload) (result *CheckinQueryResult) {
|
||||||
|
attendanceData := new(data.Attendance)
|
||||||
|
attendance, err := attendanceData.GetAttendance(payload.Context, payload.UserId, payload.Data.EventId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result = &CheckinQueryResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if attendance == nil {
|
||||||
|
result = &CheckinQueryResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 404,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventCheckinQueryRecordNotFound).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkinAt *time.Time
|
||||||
|
if !attendance.CheckinAt.IsZero() {
|
||||||
|
checkinAt = &attendance.CheckinAt
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &CheckinQueryResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceCheckinQuery).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: &CheckinQueryResponse{checkinAt},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
145
service/service_event/get_event_info.go
Normal file
145
service/service_event/get_event_info.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package service_event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventInfoData struct {
|
||||||
|
EventId uuid.UUID `json:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventInfoPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID
|
||||||
|
Data *EventInfoData
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventInfoResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *data.EventIndexDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) GetEventInfo(payload *EventInfoPayload) (result *EventInfoResult) {
|
||||||
|
event, err := new(data.Event).GetEventById(payload.Context, payload.Data.EventId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventInfoNotFound).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &EventInfoResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 404,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
joinCount, err := new(data.Attendance).CountUsersByEventID(payload.Context, payload.Data.EventId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &EventInfoResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkinCount, err := new(data.Attendance).CountCheckedInUsersByEventID(payload.Context, payload.Data.EventId)
|
||||||
|
if err != nil {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &EventInfoResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var isJoined bool
|
||||||
|
joinedInfo, err := new(data.Attendance).GetAttendance(payload.Context, payload.UserId, payload.Data.EventId)
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
exception := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorDatabase).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
result = &EventInfoResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exception,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if err == gorm.ErrRecordNotFound {
|
||||||
|
isJoined = false
|
||||||
|
} else if joinedInfo.AttendanceId != uuid.Nil {
|
||||||
|
isJoined = true
|
||||||
|
}
|
||||||
|
|
||||||
|
result = &EventInfoResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceInfo).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context),
|
||||||
|
},
|
||||||
|
Data: &data.EventIndexDoc{
|
||||||
|
EventId: event.EventId.String(),
|
||||||
|
Name: event.Name,
|
||||||
|
Type: event.Type,
|
||||||
|
Description: event.Description,
|
||||||
|
StartTime: event.StartTime,
|
||||||
|
EndTime: event.EndTime,
|
||||||
|
Thumbnail: event.Thumbnail,
|
||||||
|
EnableKYC: event.EnableKYC,
|
||||||
|
IsJoined: isJoined,
|
||||||
|
JoinCount: joinCount,
|
||||||
|
CheckinCount: checkinCount,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
161
service/service_event/get_joined_event.go
Normal file
161
service/service_event/get_joined_event.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package service_event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"nixcn-cms/data"
|
||||||
|
"nixcn-cms/internal/exception"
|
||||||
|
"nixcn-cms/service/shared"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JoinedEventListData struct {
|
||||||
|
Limit *string `json:"limit"`
|
||||||
|
Offset *string `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinedEventListPayload struct {
|
||||||
|
Context context.Context
|
||||||
|
UserId uuid.UUID
|
||||||
|
Data *JoinedEventListData
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinedEventListResult struct {
|
||||||
|
Common shared.CommonResult
|
||||||
|
Data *[]data.EventIndexDoc `json:"event_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *EventServiceImpl) GetJoinedEvent(payload *JoinedEventListPayload) (result *JoinedEventListResult) {
|
||||||
|
var limit string
|
||||||
|
if payload.Data.Limit == nil || *payload.Data.Limit == "" {
|
||||||
|
limit = "20"
|
||||||
|
} else {
|
||||||
|
limit = *payload.Data.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset string
|
||||||
|
if payload.Data.Offset == nil || *payload.Data.Offset == "" {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(nil).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &JoinedEventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 400,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offset = *payload.Data.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
limitNum, err := strconv.Atoi(limit)
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &JoinedEventListResult{
|
||||||
|
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetNum, err := strconv.Atoi(offset)
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusUser).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonErrorInvalidInput).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &JoinedEventListResult{
|
||||||
|
Common: shared.CommonResult{HttpCode: 400, Exception: exc},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventList, err := new(data.Event).
|
||||||
|
GetEventsByUserId(payload.Context, payload.UserId, int64(limitNum), int64(offsetNum))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
exc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusServer).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeSpecific).
|
||||||
|
SetOriginal(exception.EventListDatabaseFailed).
|
||||||
|
SetError(err).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &JoinedEventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 500,
|
||||||
|
Exception: exc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventList != nil && len(*eventList) > 0 {
|
||||||
|
attendances, err := new(data.Attendance).GetEventsByUserID(payload.Context, payload.UserId)
|
||||||
|
|
||||||
|
checkinMap := make(map[uuid.UUID]time.Time)
|
||||||
|
if err == nil && attendances != nil {
|
||||||
|
for _, att := range *attendances {
|
||||||
|
checkinMap[att.EventId] = att.CheckinAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range *eventList {
|
||||||
|
item := &(*eventList)[i]
|
||||||
|
|
||||||
|
item.IsJoined = true
|
||||||
|
|
||||||
|
eID, _ := uuid.Parse(item.EventId)
|
||||||
|
|
||||||
|
if checkinTime, signedUp := checkinMap[eID]; signedUp {
|
||||||
|
if !checkinTime.IsZero() && checkinTime.After(time.Unix(0, 0)) {
|
||||||
|
item.IsCheckedIn = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cCount, _ := new(data.Attendance).CountCheckedInUsersByEventID(payload.Context, eID)
|
||||||
|
item.CheckinCount = cCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventList != nil {
|
||||||
|
for i := range *eventList {
|
||||||
|
(*eventList)[i].IsJoined = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successExc := new(exception.Builder).
|
||||||
|
SetStatus(exception.StatusSuccess).
|
||||||
|
SetService(exception.ServiceEvent).
|
||||||
|
SetEndpoint(exception.EndpointEventServiceList).
|
||||||
|
SetType(exception.TypeCommon).
|
||||||
|
SetOriginal(exception.CommonSuccess).
|
||||||
|
Throw(payload.Context)
|
||||||
|
|
||||||
|
return &JoinedEventListResult{
|
||||||
|
Common: shared.CommonResult{
|
||||||
|
HttpCode: 200,
|
||||||
|
Exception: successExc,
|
||||||
|
},
|
||||||
|
Data: eventList,
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user