Compare commits
330 Commits
ded24e4ad5
...
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
|
@@ -1,9 +1,2 @@
|
||||
SERVER_ADDRESS=:8000
|
||||
SERVER_DEBUG_MODE=true
|
||||
SERVER_FILE_LOGGER=false
|
||||
SERVER_JWT_SECRET=test
|
||||
DATABASE_TYPE=postgres
|
||||
DATABASE_HOST=127.0.0.1
|
||||
DATABASE_NAME=postgres
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=postgres
|
||||
TZ=Asia/Shanghai
|
||||
LOG_LEVEL=debug
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,3 +46,9 @@ go.work.sum
|
||||
.DS_Store
|
||||
__MACOSX
|
||||
._*
|
||||
|
||||
# go gen
|
||||
*_gen.go
|
||||
|
||||
# test files
|
||||
.test/
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
"tab_size": 4,
|
||||
"format_on_save": "on",
|
||||
"languages": {
|
||||
"Nix": {
|
||||
"tab_size": 2,
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2,
|
||||
"language_servers": [
|
||||
"typescript-language-server",
|
||||
"!vtsls",
|
||||
@@ -16,6 +20,7 @@
|
||||
],
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2,
|
||||
"language_servers": [
|
||||
"typescript-language-server",
|
||||
"!vtsls",
|
||||
@@ -24,6 +29,7 @@
|
||||
],
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2,
|
||||
"language_servers": [
|
||||
"typescript-language-server",
|
||||
"!vtsls",
|
||||
|
||||
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
|
||||
|
||||
## 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)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
use flake . --impure
|
||||
26
client/.gitignore
vendored
26
client/.gitignore
vendored
@@ -1,26 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.direnv
|
||||
1209
client/bun.lock
1209
client/bun.lock
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
import pluginQuery from '@tanstack/eslint-plugin-query';
|
||||
|
||||
export default antfu({
|
||||
gitignore: true,
|
||||
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts'],
|
||||
react: true,
|
||||
stylistic: {
|
||||
semi: true,
|
||||
quotes: 'single',
|
||||
indent: 2,
|
||||
},
|
||||
typescript: {
|
||||
tsconfigPath: 'tsconfig.json',
|
||||
},
|
||||
}, ...pluginQuery.configs['flat/recommended']);
|
||||
61
client/flake.lock
generated
61
client/flake.lock
generated
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765779637,
|
||||
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"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
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
description = "Basic flake for devShell";
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>client</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"name": "client",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-router": "^1.141.6",
|
||||
"@tanstack/react-router-devtools": "^1.141.6",
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^6.7.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.13",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@tanstack/router-plugin": "^1.141.7",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^16.5.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "bun run lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<p>Hello world</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { App };
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
|
||||
export function Time() {
|
||||
const { data: time } = useSuspenseQuery({
|
||||
queryKey: ['time'],
|
||||
queryFn: async () => axios.get<{ datetime: string }>('https://worldtimeapi.org/api/timezone/Asia/Shanghai')
|
||||
.then(res => res.data.datetime)
|
||||
.then(isoString => new Date(isoString).toLocaleTimeString()),
|
||||
});
|
||||
return (
|
||||
<p>
|
||||
Current time:
|
||||
{time}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { Theme } from '@/hooks/useTheme';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ThemeProviderContext } from '@/hooks/useTheme';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'dark',
|
||||
storageKey = 'vite-ui-theme',
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
// eslint-disable-next-line react/no-unstable-context-value
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
interface ThemeProviderState {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: 'system',
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
export const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function useTheme() {
|
||||
const context = use(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-serif: 'Lora', serif;
|
||||
--radius: 0.5rem;
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--spacing: var(--spacing);
|
||||
--letter-spacing: var(--letter-spacing);
|
||||
--shadow-offset-y: var(--shadow-offset-y);
|
||||
--shadow-offset-x: var(--shadow-offset-x);
|
||||
--shadow-spread: var(--shadow-spread);
|
||||
--shadow-blur: var(--shadow-blur);
|
||||
--shadow-opacity: var(--shadow-opacity);
|
||||
--color-shadow-color: var(--shadow-color);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
--background: oklch(0.9816 0.0017 247.8390);
|
||||
--foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--primary: oklch(0.5502 0.1193 263.8209);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.7499 0.0898 239.3977);
|
||||
--secondary-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--muted: oklch(0.9417 0.0052 247.8790);
|
||||
--muted-foreground: oklch(0.5575 0.0165 244.8933);
|
||||
--accent: oklch(0.9417 0.0052 247.8790);
|
||||
--accent-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--destructive: oklch(0.5915 0.2020 21.2388);
|
||||
--border: oklch(0.9109 0.0070 247.9014);
|
||||
--input: oklch(1.0000 0 0);
|
||||
--ring: oklch(0.5502 0.1193 263.8209);
|
||||
--chart-1: oklch(0.5502 0.1193 263.8209);
|
||||
--chart-2: oklch(0.7499 0.0898 239.3977);
|
||||
--chart-3: oklch(0.4711 0.0998 264.0792);
|
||||
--chart-4: oklch(0.6689 0.0699 240.3096);
|
||||
--chart-5: oklch(0.5107 0.1098 263.6921);
|
||||
--sidebar: oklch(0.9632 0.0034 247.8585);
|
||||
--sidebar-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--sidebar-primary: oklch(0.5502 0.1193 263.8209);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.9417 0.0052 247.8790);
|
||||
--sidebar-accent-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--sidebar-border: oklch(0.9109 0.0070 247.9014);
|
||||
--sidebar-ring: oklch(0.5502 0.1193 263.8209);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Lora', serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--shadow-color: #000000;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-blur: 0.5rem;
|
||||
--shadow-spread: 0rem;
|
||||
--shadow-offset-x: 0rem;
|
||||
--shadow-offset-y: 0.1rem;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03);
|
||||
--shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.03);
|
||||
--shadow-sm: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 1px 2px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-md: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 2px 4px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-lg: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 4px 6px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.05), 0rem 8px 10px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.13);
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.2270 0.0120 270.8402);
|
||||
--foreground: oklch(0.9067 0 0);
|
||||
--card: oklch(0.2630 0.0127 258.3724);
|
||||
--card-foreground: oklch(0.9067 0 0);
|
||||
--popover: oklch(0.2630 0.0127 258.3724);
|
||||
--popover-foreground: oklch(0.9067 0 0);
|
||||
--primary: oklch(0.5774 0.1248 263.3770);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.7636 0.0866 239.8852);
|
||||
--secondary-foreground: oklch(0.2621 0.0095 248.1897);
|
||||
--muted: oklch(0.3006 0.0156 264.3078);
|
||||
--muted-foreground: oklch(0.7137 0.0192 261.3246);
|
||||
--accent: oklch(0.3006 0.0156 264.3078);
|
||||
--accent-foreground: oklch(0.9067 0 0);
|
||||
--destructive: oklch(0.5915 0.2020 21.2388);
|
||||
--border: oklch(0.3451 0.0133 248.2124);
|
||||
--input: oklch(0.2630 0.0127 258.3724);
|
||||
--ring: oklch(0.5502 0.1193 263.8209);
|
||||
--chart-1: oklch(0.5502 0.1193 263.8209);
|
||||
--chart-2: oklch(0.7499 0.0898 239.3977);
|
||||
--chart-3: oklch(0.4711 0.0998 264.0792);
|
||||
--chart-4: oklch(0.6689 0.0699 240.3096);
|
||||
--chart-5: oklch(0.5107 0.1098 263.6921);
|
||||
--sidebar: oklch(0.2270 0.0120 270.8402);
|
||||
--sidebar-foreground: oklch(0.9067 0 0);
|
||||
--sidebar-primary: oklch(0.5502 0.1193 263.8209);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.3006 0.0156 264.3078);
|
||||
--sidebar-accent-foreground: oklch(0.9067 0 0);
|
||||
--sidebar-border: oklch(0.3451 0.0133 248.2124);
|
||||
--sidebar-ring: oklch(0.5502 0.1193 263.8209);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--radius: 0.5rem;
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Lora', serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--shadow-color: #000000;
|
||||
--shadow-opacity: 0.3;
|
||||
--shadow-blur: 0.5rem;
|
||||
--shadow-spread: 0rem;
|
||||
--shadow-offset-x: 0rem;
|
||||
--shadow-offset-y: 0.1rem;
|
||||
--letter-spacing: 0em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15);
|
||||
--shadow-xs: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.15);
|
||||
--shadow-sm: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 1px 2px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 1px 2px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-md: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 2px 4px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-lg: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 4px 6px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.30), 0rem 8px 10px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-2xl: 0rem 0.1rem 0.5rem 0rem hsl(0 0% 0% / 0.75);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { AxiosError } from 'axios';
|
||||
import axios from 'axios';
|
||||
|
||||
export const axiosClient = axios.create({
|
||||
baseURL: '/api/',
|
||||
});
|
||||
|
||||
axiosClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token !== null) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
axiosClient.interceptors.response.use(undefined, async (error: AxiosError) => {
|
||||
if (error.response && error?.response.status === 401) {
|
||||
// TODO: refresh token
|
||||
if (error.config) {
|
||||
return axiosClient(error.config);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { createRouter, RouterProvider } from '@tanstack/react-router';
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({ routeTree });
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById('root')!;
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -1,22 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import '@/index.css';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function RootLayout() {
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createRootRoute({ component: RootLayout });
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Suspense } from 'react';
|
||||
import { Time } from '@/components/Time';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Index,
|
||||
});
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!</h3>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Time />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"moduleDetection": "force",
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vite/client"],
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"moduleDetection": "force",
|
||||
"module": "ESNext",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"],
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: 'react',
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,44 @@
|
||||
server:
|
||||
application: example
|
||||
address: :8000
|
||||
external_url: https://example.com
|
||||
debug_mode: false
|
||||
file_logger: false
|
||||
jwt_secret: someting
|
||||
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
|
||||
|
||||
@@ -26,12 +26,16 @@ func Init() {
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
conf := &config{}
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// Dont generate config when using dev mode
|
||||
log.Fatalln("Can't read config!")
|
||||
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.Fatal(err)
|
||||
log.Fatalln("[Condig] Can't unmarshal config!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,70 @@ 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 {
|
||||
Address string `yaml:"address"`
|
||||
DebugMode string `yaml:"debug_mode"`
|
||||
FileLogger string `yaml:"file_logger"`
|
||||
JwtSecret string `yaml:"jwt_secret"`
|
||||
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"`
|
||||
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")
|
||||
}
|
||||
40
data/data.go
40
data/data.go
@@ -1,15 +1,21 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"nixcn-cms/data/drivers"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"log/slog"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var Database *drivers.DBClient
|
||||
var Database *gorm.DB
|
||||
var Redis redis.UniversalClient
|
||||
|
||||
func Init() {
|
||||
func Init(ctx context.Context) {
|
||||
// Init database
|
||||
dbType := viper.GetString("database.type")
|
||||
exDSN := drivers.ExternalDSN{
|
||||
@@ -20,20 +26,38 @@ func Init() {
|
||||
}
|
||||
|
||||
if dbType != "postgres" {
|
||||
log.Fatal("[Database] Only support postgras db!")
|
||||
slog.ErrorContext(ctx, "[Database] Only support postgras db!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Conect to db
|
||||
db, err := drivers.Postgres(exDSN)
|
||||
if err != nil {
|
||||
log.Fatal("[Database] Error connecting to db!")
|
||||
slog.ErrorContext(ctx, "[Database] Error connecting to db!", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
err = db.DB.AutoMigrate(&User{})
|
||||
err = db.AutoMigrate(&User{}, &Event{}, &Attendance{}, &Client{}, &Kyc{})
|
||||
if err != nil {
|
||||
log.Error("[Database] Error migrating database: ", err)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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) {
|
||||
@@ -16,9 +21,28 @@ func SplitHostPort(url string) (host, port string) {
|
||||
return split[0], split[1]
|
||||
}
|
||||
|
||||
func Postgres(dsn ExternalDSN) (*DBClient, error) {
|
||||
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{})
|
||||
return &DBClient{db}, err
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ExternalDSN struct {
|
||||
Host string
|
||||
Name string
|
||||
@@ -11,6 +7,15 @@ type ExternalDSN struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
type DBClient struct {
|
||||
*gorm.DB
|
||||
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
|
||||
}
|
||||
162
data/user.go
162
data/user.go
@@ -1,38 +1,154 @@
|
||||
package data
|
||||
|
||||
import "github.com/google/uuid"
|
||||
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:"size:8;uniqueindex;not null"`
|
||||
Email string `json:"email" gorm:"uniqueindex;not null"`
|
||||
Nickname string `json:"nickname" gorm:"not null"`
|
||||
Type string `json:"type" gorm:"not null"`
|
||||
Subtitle string `json:"subtitle" gorm:"not null"`
|
||||
Avatar string `json:"avatar" gorm:"not null"`
|
||||
Checkin bool `json:"checkin" gorm:"not null"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (self *User) GetByEmail(email string) error {
|
||||
if err := Database.Where("email = ?", email).First(&self).Error; err != nil {
|
||||
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) GetByUserId(userId string) error {
|
||||
if err := Database.Where("user_id = ?", userId).First(&self).Error; 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) SetCheckinState(email string, state bool) error {
|
||||
if err := Database.Where("email = ?", email).First(&self).Error; err != nil {
|
||||
return err
|
||||
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
|
||||
}
|
||||
self.Checkin = state
|
||||
Database.Save(&self)
|
||||
return nil
|
||||
|
||||
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
|
||||
122
devenv.lock
122
devenv.lock
@@ -3,10 +3,11 @@
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1766087669,
|
||||
"lastModified": 1772840537,
|
||||
"narHash": "sha256-Axb80AW0e6Xd/UmtjFnTTPOqxzkI4tzGa8ykk8nKQoo=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "c03eed645ea94da7afbee29da76436b7ce33a5cb",
|
||||
"rev": "b6208f517a7359d6046c89b5ac0de20cfca3b4c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -19,25 +20,11 @@
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1765121682,
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1765121682,
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -51,10 +38,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -67,37 +55,17 @@
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765911976,
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks_2": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"gitignore": "gitignore_2",
|
||||
"nixpkgs": [
|
||||
"go-overlay",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765911976,
|
||||
"lastModified": 1765016596,
|
||||
"narHash": "sha256-rhSqPNxDVow7OQKi4qS5H8Au0P4S3AYbawBSmJNUtBQ=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||
"rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -107,26 +75,6 @@
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1762808025,
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"go-overlay",
|
||||
@@ -135,10 +83,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1762808025,
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -150,16 +99,17 @@
|
||||
"go-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"git-hooks": "git-hooks_2",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766126609,
|
||||
"lastModified": 1772865556,
|
||||
"narHash": "sha256-IIYNduS/rTlLzDxDyHqXd8JHl9F7UDf3EiEdtwupop4=",
|
||||
"owner": "purpleclay",
|
||||
"repo": "go-overlay",
|
||||
"rev": "959f32b00fd3d462d4d570bd118b4be03c3f2019",
|
||||
"rev": "8e54102a2301c8003f7a15f2536c6530d3035c4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -169,11 +119,15 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs-src": "nixpkgs-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764580874,
|
||||
"lastModified": 1772749504,
|
||||
"narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "dcf61356c3ab25f1362b4a4428a6d871e84f1d1d",
|
||||
"rev": "08543693199362c1fddb8f52126030d0d374ba2e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -183,20 +137,34 @@
|
||||
"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",
|
||||
"git-hooks": "git-hooks",
|
||||
"go-overlay": "go-overlay",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pre-commit-hooks": [
|
||||
"git-hooks"
|
||||
]
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
@@ -211,4 +179,4 @@
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
}
|
||||
77
devenv.nix
77
devenv.nix
@@ -1,12 +1,16 @@
|
||||
{ pkgs, config, ... }:
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
env.GREET = "devenv";
|
||||
process.managers.process-compose = {
|
||||
settings.log_level = "info";
|
||||
};
|
||||
|
||||
packages = [
|
||||
pkgs.git
|
||||
pkgs.bun
|
||||
pkgs.just
|
||||
packages = with pkgs; [
|
||||
git
|
||||
just
|
||||
watchexec
|
||||
fvm
|
||||
podman
|
||||
];
|
||||
|
||||
dotenv = {
|
||||
@@ -17,40 +21,41 @@
|
||||
];
|
||||
};
|
||||
|
||||
languages.go = {
|
||||
enable = true;
|
||||
version = "1.25.5";
|
||||
languages = {
|
||||
go = {
|
||||
enable = true;
|
||||
version = "1.26.0";
|
||||
};
|
||||
};
|
||||
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
dataDir = "${config.env.DEVENV_STATE}/caddy";
|
||||
config = ''
|
||||
{
|
||||
debug
|
||||
}
|
||||
:8080 {
|
||||
root * ${config.env.DEVENV_ROOT}/.outputs/static
|
||||
file_server
|
||||
reverse_proxy /api/v1/* http://127.0.0.1:8000
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
services.postgres = {
|
||||
enable = true;
|
||||
createDatabase = true;
|
||||
listen_addresses = "127.0.0.1";
|
||||
initialDatabases = [
|
||||
{
|
||||
name = "postgres";
|
||||
user = "postgres";
|
||||
pass = "postgres";
|
||||
}
|
||||
];
|
||||
services = {
|
||||
redis = {
|
||||
enable = true;
|
||||
};
|
||||
postgres = {
|
||||
enable = true;
|
||||
createDatabase = true;
|
||||
listen_addresses = "127.0.0.1";
|
||||
initialDatabases = [
|
||||
{
|
||||
name = "postgres";
|
||||
user = "postgres";
|
||||
pass = "postgres";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
111
go.mod
111
go.mod
@@ -2,60 +2,137 @@ 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.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.11.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.29.0 // 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-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/uuid v1.6.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/joho/godotenv v1.5.1 // 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/sirupsen/logrus v1.9.3 // 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/spf13/viper v1.21.0 // 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.uber.org/mock v0.6.0 // 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/crypto v0.46.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.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gorm.io/driver/postgres v1.6.0 // indirect
|
||||
gorm.io/gorm v1.31.1 // 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
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func JWTAuth() gin.HandlerFunc {
|
||||
var JwtSecret = []byte(viper.GetString("server.jwt_secret"))
|
||||
return func(c *gin.Context) {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "missing Authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid Authorization header format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := parts[1]
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return JwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid or expired token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "invalid token claims",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateToken(userID uuid.UUID, application string) (string, error) {
|
||||
var JwtSecret = []byte(viper.GetString("server.jwt_secret"))
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: application,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(JwtSecret)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"nixcn-cms/config"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
config.Init()
|
||||
}
|
||||
|
||||
func generateTestToken(userID uuid.UUID, expire time.Duration) string {
|
||||
var JwtSecret = []byte(viper.GetString("server.jwt_secret"))
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expire)),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenStr, _ := token.SignedString(JwtSecret)
|
||||
return tokenStr
|
||||
}
|
||||
func TestJWTAuth_MissingToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(JWTAuth())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
func TestJWTAuth_InvalidToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(JWTAuth())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid.token.here")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
func TestJWTAuth_ValidToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(JWTAuth())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
c.JSON(200, gin.H{
|
||||
"user_id": userID,
|
||||
})
|
||||
})
|
||||
|
||||
uuid, _ := uuid.NewUUID()
|
||||
token := generateTestToken(uuid, time.Hour)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
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"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user