Skip to content

Commit 6086d8a

Browse files
authored
feat(backend): rework for 2024 (#84)
* feat(backend): nocodb client * feat(backend): realign structure, move to chi * feat(backend): implement UpdateTableRecords for nocodb * feat(backend): start to move database implementation to nocodb * feat(backend): rework ticketing to use nocodb * feat(backend): rework mail blast model * ci: move trufflehog as separate job * chore(backend): update lock file * fix(backend): wrong sprintf format * test(backend): fix test for nocodb related
1 parent b9ea8d3 commit 6086d8a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3646
-2463
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,37 @@ on:
77
- master
88

99
jobs:
10+
scan:
11+
name: Secret scan
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 10
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: TruffleHog OSS
21+
uses: trufflesecurity/trufflehog@main
22+
with:
23+
extra_args: --debug --only-verified
24+
1025
ci-backend:
1126
name: Backend
1227
runs-on: ubuntu-latest
1328
timeout-minutes: 20
14-
container: golang:1.21-bookworm
29+
container: golang:1-bookworm
1530
defaults:
1631
run:
1732
working-directory: ./backend
1833
services:
19-
db:
20-
image: postgres:15-bookworm
21-
ports:
22-
- 5432:5432
23-
env:
24-
POSTGRES_PASSWORD: password
25-
POSTGRES_USER: postgres
26-
POSTGRES_DB: conf
27-
options: >-
28-
--health-cmd "pg_isready -U postgres -d conf"
29-
--health-interval 10s
30-
--health-timeout 5s
31-
--health-retries 5
3234
smtp:
3335
image: marlonb/mailcrab:latest
3436
ports:
3537
- 1025:1025
3638
steps:
3739
- name: Checkout code
3840
uses: actions/checkout@v3
39-
with:
40-
fetch-depth: 0
41-
42-
- name: TruffleHog OSS
43-
uses: trufflesecurity/trufflehog@main
44-
with:
45-
path: ./
46-
base: ${{ github.event.repository.default_branch }}
47-
head: HEAD
48-
extra_args: --debug --only-verified
4941

5042
- name: Build
5143
run: go build -buildvcs=false .
@@ -74,16 +66,6 @@ jobs:
7466
steps:
7567
- name: Checkout code
7668
uses: actions/checkout@v3
77-
with:
78-
fetch-depth: 0
79-
80-
- name: TruffleHog OSS
81-
uses: trufflesecurity/trufflehog@main
82-
with:
83-
path: ./
84-
base: ${{ github.event.repository.default_branch }}
85-
head: HEAD
86-
extra_args: --debug --only-verified
8769

8870
- name: Setup pnpm
8971
uses: pnpm/action-setup@v2

backend/Dockerfile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ FROM debian:bookworm-slim AS runtime
1010

1111
WORKDIR /app
1212

13-
RUN apt-get update && \
14-
apt-get install -y curl && \
15-
apt-get clean && \
13+
RUN apt-get clean && \
1614
rm -rf /var/lib/apt/lists/* && \
1715
mkdir -p /app/csv && \
1816
mkdir -p /data
@@ -22,7 +20,7 @@ ARG PORT=8080
2220
COPY --from=build /app/ .
2321

2422
HEALTHCHECK --interval=60s --timeout=40s \
25-
CMD curl -f http://localhost:8080/ping || exit 1
23+
CMD /app/conf-backend --port ${PORT}
2624

2725
EXPOSE ${PORT}
2826

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package administrator
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"encoding/hex"
7+
"fmt"
8+
9+
"conf/administrator/jwt"
10+
"github.com/pquerna/otp/totp"
11+
)
12+
13+
type Administrator struct {
14+
Username string `yaml:"username"`
15+
HashedPassword string `yaml:"hashed_password"`
16+
TotpSecret string `yaml:"totp_secret"`
17+
}
18+
19+
func GenerateSecret(username string) (secret string, url string, err error) {
20+
generate, err := totp.Generate(totp.GenerateOpts{Issuer: "teknumconf", AccountName: username, Rand: rand.Reader})
21+
if err != nil {
22+
return "", "", err
23+
}
24+
25+
return generate.Secret(), generate.URL(), nil
26+
}
27+
28+
type AdministratorDomain struct {
29+
jwt *jwt.JsonWebToken
30+
administrators []Administrator
31+
}
32+
33+
func NewAdministratorDomain(administrators []Administrator) (*AdministratorDomain, error) {
34+
// Generate ed25519 key pairs for access and refresh tokens
35+
accessPublicKey, accessPrivateKey, err := ed25519.GenerateKey(nil)
36+
if err != nil {
37+
return nil, fmt.Errorf("generating fresh access key pair: %w", err)
38+
}
39+
40+
refreshPublicKey, refreshPrivateKey, err := ed25519.GenerateKey(nil)
41+
if err != nil {
42+
return nil, fmt.Errorf("generating fresh refresh key pair: %w", err)
43+
}
44+
45+
var randomIssuer = make([]byte, 18)
46+
_, _ = rand.Read(randomIssuer)
47+
48+
var randomSubject = make([]byte, 16)
49+
_, _ = rand.Read(randomSubject)
50+
51+
var randomAudience = make([]byte, 32)
52+
_, _ = rand.Read(randomAudience)
53+
54+
authJwt := jwt.NewJwt(accessPrivateKey, accessPublicKey, refreshPrivateKey, refreshPublicKey, hex.EncodeToString(randomIssuer), hex.EncodeToString(randomSubject), hex.EncodeToString(randomAudience))
55+
56+
return &AdministratorDomain{
57+
jwt: authJwt,
58+
administrators: administrators,
59+
}, nil
60+
}

backend/administrator/authenticate.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package administrator
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/getsentry/sentry-go"
10+
"github.com/pquerna/otp/totp"
11+
"golang.org/x/crypto/bcrypt"
12+
)
13+
14+
func (a *AdministratorDomain) Authenticate(ctx context.Context, username string, plainPassword string, otpCode string) (string, bool, error) {
15+
span := sentry.StartSpan(ctx, "administrator.authenticate", sentry.WithTransactionName("Authenticate"))
16+
defer span.Finish()
17+
18+
var administrator Administrator
19+
for _, adm := range a.administrators {
20+
if adm.Username == username {
21+
administrator = adm
22+
break
23+
}
24+
}
25+
26+
if administrator.Username == "" {
27+
return "", false, nil
28+
}
29+
30+
hashedPassword, err := hex.DecodeString(administrator.HashedPassword)
31+
if err != nil {
32+
return "", false, fmt.Errorf("invalid hex string")
33+
}
34+
35+
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(plainPassword))
36+
if err != nil {
37+
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
38+
return "", false, nil
39+
}
40+
41+
return "", false, fmt.Errorf("password: %w", err)
42+
}
43+
44+
ok := totp.Validate(otpCode, administrator.TotpSecret)
45+
if !ok {
46+
return "", false, nil
47+
}
48+
49+
token, err := a.jwt.Sign(username)
50+
if err != nil {
51+
return "", false, fmt.Errorf("signing token: %w", err)
52+
}
53+
54+
return token, true, nil
55+
}

backend/administrator/jwt/jwt.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package jwt
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"errors"
7+
"fmt"
8+
"time"
9+
10+
"github.com/golang-jwt/jwt/v4"
11+
)
12+
13+
type JsonWebToken struct {
14+
accessPrivateKey ed25519.PrivateKey
15+
accessPublicKey ed25519.PublicKey
16+
refreshPrivateKey ed25519.PrivateKey
17+
refreshPublicKey ed25519.PublicKey
18+
issuer string
19+
subject string
20+
audience string
21+
}
22+
23+
func NewJwt(accessPrivateKey []byte, accessPublicKey []byte, refreshPrivateKey []byte, refreshPublicKey []byte, issuer string, subject string, audience string) *JsonWebToken {
24+
return &JsonWebToken{
25+
accessPrivateKey: accessPrivateKey,
26+
accessPublicKey: accessPublicKey,
27+
refreshPrivateKey: refreshPrivateKey,
28+
refreshPublicKey: refreshPublicKey,
29+
issuer: issuer,
30+
subject: subject,
31+
audience: audience,
32+
}
33+
}
34+
35+
func (j *JsonWebToken) Sign(userId string) (accessToken string, err error) {
36+
accessRandId := make([]byte, 32)
37+
_, _ = rand.Read(accessRandId)
38+
39+
accessClaims := jwt.MapClaims{
40+
"iss": j.issuer,
41+
"sub": j.subject,
42+
"aud": j.audience,
43+
"exp": time.Now().Add(time.Hour * 1).Unix(),
44+
"nbf": time.Now().Unix(),
45+
"iat": time.Now().Unix(),
46+
"jti": string(accessRandId),
47+
"uid": userId,
48+
}
49+
50+
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodEdDSA, accessClaims).SignedString(j.accessPrivateKey)
51+
if err != nil {
52+
return "", fmt.Errorf("failed to sign access token: %w", err)
53+
}
54+
55+
return accessToken, nil
56+
}
57+
58+
var ErrInvalidSigningMethod = errors.New("invalid signing method")
59+
var ErrExpired = errors.New("token expired")
60+
var ErrInvalid = errors.New("token invalid")
61+
var ErrClaims = errors.New("token claims invalid")
62+
63+
func (j *JsonWebToken) VerifyAccessToken(token string) (userId string, err error) {
64+
if token == "" {
65+
return "", ErrInvalid
66+
}
67+
68+
parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
69+
_, ok := t.Method.(*jwt.SigningMethodEd25519)
70+
if !ok {
71+
return nil, ErrInvalidSigningMethod
72+
}
73+
return j.accessPublicKey, nil
74+
})
75+
if err != nil {
76+
if parsedToken != nil && !parsedToken.Valid {
77+
// Check if the error is a type of jwt.ValidationError
78+
validationError, ok := err.(*jwt.ValidationError)
79+
if ok {
80+
if validationError.Errors&jwt.ValidationErrorExpired != 0 {
81+
return "", ErrExpired
82+
}
83+
84+
if validationError.Errors&jwt.ValidationErrorSignatureInvalid != 0 {
85+
return "", ErrInvalid
86+
}
87+
88+
if validationError.Errors&jwt.ValidationErrorClaimsInvalid != 0 {
89+
return "", ErrClaims
90+
}
91+
92+
return "", fmt.Errorf("failed to parse access token: %w", err)
93+
}
94+
95+
return "", fmt.Errorf("non-validation error during parsing token: %w", err)
96+
}
97+
98+
return "", fmt.Errorf("token is valid or parsedToken is not nil: %w", err)
99+
}
100+
101+
claims, ok := parsedToken.Claims.(jwt.MapClaims)
102+
if !ok {
103+
return "", ErrClaims
104+
}
105+
106+
if !claims.VerifyAudience(j.audience, true) {
107+
return "", ErrInvalid
108+
}
109+
110+
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
111+
return "", ErrExpired
112+
}
113+
114+
if !claims.VerifyIssuer(j.issuer, true) {
115+
return "", ErrInvalid
116+
}
117+
118+
if !claims.VerifyNotBefore(time.Now().Unix(), true) {
119+
return "", ErrInvalid
120+
}
121+
122+
jwtId, ok := claims["jti"].(string)
123+
if !ok {
124+
return "", ErrClaims
125+
}
126+
127+
if jwtId == "" {
128+
return "", ErrClaims
129+
}
130+
131+
userId, ok = claims["uid"].(string)
132+
if !ok {
133+
return "", ErrClaims
134+
}
135+
136+
return userId, nil
137+
}

0 commit comments

Comments
 (0)