Skip to content

Commit

Permalink
feat(backend): rework for 2024 (#84)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
aldy505 authored Apr 20, 2024
1 parent b9ea8d3 commit 6086d8a
Show file tree
Hide file tree
Showing 57 changed files with 3,646 additions and 2,463 deletions.
50 changes: 16 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,37 @@ on:
- master

jobs:
scan:
name: Secret scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
extra_args: --debug --only-verified

ci-backend:
name: Backend
runs-on: ubuntu-latest
timeout-minutes: 20
container: golang:1.21-bookworm
container: golang:1-bookworm
defaults:
run:
working-directory: ./backend
services:
db:
image: postgres:15-bookworm
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: conf
options: >-
--health-cmd "pg_isready -U postgres -d conf"
--health-interval 10s
--health-timeout 5s
--health-retries 5
smtp:
image: marlonb/mailcrab:latest
ports:
- 1025:1025
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug --only-verified

- name: Build
run: go build -buildvcs=false .
Expand Down Expand Up @@ -74,16 +66,6 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug --only-verified

- name: Setup pnpm
uses: pnpm/action-setup@v2
Expand Down
6 changes: 2 additions & 4 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ FROM debian:bookworm-slim AS runtime

WORKDIR /app

RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/csv && \
mkdir -p /data
Expand All @@ -22,7 +20,7 @@ ARG PORT=8080
COPY --from=build /app/ .

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

EXPOSE ${PORT}

Expand Down
60 changes: 60 additions & 0 deletions backend/administrator/administrator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package administrator

import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"fmt"

"conf/administrator/jwt"
"github.com/pquerna/otp/totp"
)

type Administrator struct {
Username string `yaml:"username"`
HashedPassword string `yaml:"hashed_password"`
TotpSecret string `yaml:"totp_secret"`
}

func GenerateSecret(username string) (secret string, url string, err error) {
generate, err := totp.Generate(totp.GenerateOpts{Issuer: "teknumconf", AccountName: username, Rand: rand.Reader})
if err != nil {
return "", "", err
}

return generate.Secret(), generate.URL(), nil
}

type AdministratorDomain struct {
jwt *jwt.JsonWebToken
administrators []Administrator
}

func NewAdministratorDomain(administrators []Administrator) (*AdministratorDomain, error) {
// Generate ed25519 key pairs for access and refresh tokens
accessPublicKey, accessPrivateKey, err := ed25519.GenerateKey(nil)
if err != nil {
return nil, fmt.Errorf("generating fresh access key pair: %w", err)
}

refreshPublicKey, refreshPrivateKey, err := ed25519.GenerateKey(nil)
if err != nil {
return nil, fmt.Errorf("generating fresh refresh key pair: %w", err)
}

var randomIssuer = make([]byte, 18)
_, _ = rand.Read(randomIssuer)

var randomSubject = make([]byte, 16)
_, _ = rand.Read(randomSubject)

var randomAudience = make([]byte, 32)
_, _ = rand.Read(randomAudience)

authJwt := jwt.NewJwt(accessPrivateKey, accessPublicKey, refreshPrivateKey, refreshPublicKey, hex.EncodeToString(randomIssuer), hex.EncodeToString(randomSubject), hex.EncodeToString(randomAudience))

return &AdministratorDomain{
jwt: authJwt,
administrators: administrators,
}, nil
}
55 changes: 55 additions & 0 deletions backend/administrator/authenticate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package administrator

import (
"context"
"encoding/hex"
"errors"
"fmt"

"github.com/getsentry/sentry-go"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
)

func (a *AdministratorDomain) Authenticate(ctx context.Context, username string, plainPassword string, otpCode string) (string, bool, error) {
span := sentry.StartSpan(ctx, "administrator.authenticate", sentry.WithTransactionName("Authenticate"))
defer span.Finish()

var administrator Administrator
for _, adm := range a.administrators {
if adm.Username == username {
administrator = adm
break
}
}

if administrator.Username == "" {
return "", false, nil
}

hashedPassword, err := hex.DecodeString(administrator.HashedPassword)
if err != nil {
return "", false, fmt.Errorf("invalid hex string")
}

err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(plainPassword))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return "", false, nil
}

return "", false, fmt.Errorf("password: %w", err)
}

ok := totp.Validate(otpCode, administrator.TotpSecret)
if !ok {
return "", false, nil
}

token, err := a.jwt.Sign(username)
if err != nil {
return "", false, fmt.Errorf("signing token: %w", err)
}

return token, true, nil
}
137 changes: 137 additions & 0 deletions backend/administrator/jwt/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package jwt

import (
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"time"

"github.com/golang-jwt/jwt/v4"
)

type JsonWebToken struct {
accessPrivateKey ed25519.PrivateKey
accessPublicKey ed25519.PublicKey
refreshPrivateKey ed25519.PrivateKey
refreshPublicKey ed25519.PublicKey
issuer string
subject string
audience string
}

func NewJwt(accessPrivateKey []byte, accessPublicKey []byte, refreshPrivateKey []byte, refreshPublicKey []byte, issuer string, subject string, audience string) *JsonWebToken {
return &JsonWebToken{
accessPrivateKey: accessPrivateKey,
accessPublicKey: accessPublicKey,
refreshPrivateKey: refreshPrivateKey,
refreshPublicKey: refreshPublicKey,
issuer: issuer,
subject: subject,
audience: audience,
}
}

func (j *JsonWebToken) Sign(userId string) (accessToken string, err error) {
accessRandId := make([]byte, 32)
_, _ = rand.Read(accessRandId)

accessClaims := jwt.MapClaims{
"iss": j.issuer,
"sub": j.subject,
"aud": j.audience,
"exp": time.Now().Add(time.Hour * 1).Unix(),
"nbf": time.Now().Unix(),
"iat": time.Now().Unix(),
"jti": string(accessRandId),
"uid": userId,
}

accessToken, err = jwt.NewWithClaims(jwt.SigningMethodEdDSA, accessClaims).SignedString(j.accessPrivateKey)
if err != nil {
return "", fmt.Errorf("failed to sign access token: %w", err)
}

return accessToken, nil
}

var ErrInvalidSigningMethod = errors.New("invalid signing method")
var ErrExpired = errors.New("token expired")
var ErrInvalid = errors.New("token invalid")
var ErrClaims = errors.New("token claims invalid")

func (j *JsonWebToken) VerifyAccessToken(token string) (userId string, err error) {
if token == "" {
return "", ErrInvalid
}

parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
_, ok := t.Method.(*jwt.SigningMethodEd25519)
if !ok {
return nil, ErrInvalidSigningMethod
}
return j.accessPublicKey, nil
})
if err != nil {
if parsedToken != nil && !parsedToken.Valid {
// Check if the error is a type of jwt.ValidationError
validationError, ok := err.(*jwt.ValidationError)
if ok {
if validationError.Errors&jwt.ValidationErrorExpired != 0 {
return "", ErrExpired
}

if validationError.Errors&jwt.ValidationErrorSignatureInvalid != 0 {
return "", ErrInvalid
}

if validationError.Errors&jwt.ValidationErrorClaimsInvalid != 0 {
return "", ErrClaims
}

return "", fmt.Errorf("failed to parse access token: %w", err)
}

return "", fmt.Errorf("non-validation error during parsing token: %w", err)
}

return "", fmt.Errorf("token is valid or parsedToken is not nil: %w", err)
}

claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return "", ErrClaims
}

if !claims.VerifyAudience(j.audience, true) {
return "", ErrInvalid
}

if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
return "", ErrExpired
}

if !claims.VerifyIssuer(j.issuer, true) {
return "", ErrInvalid
}

if !claims.VerifyNotBefore(time.Now().Unix(), true) {
return "", ErrInvalid
}

jwtId, ok := claims["jti"].(string)
if !ok {
return "", ErrClaims
}

if jwtId == "" {
return "", ErrClaims
}

userId, ok = claims["uid"].(string)
if !ok {
return "", ErrClaims
}

return userId, nil
}
Loading

0 comments on commit 6086d8a

Please sign in to comment.