Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
feeccd3
initial scheduling service
Fiery-132 Nov 12, 2025
e139a3c
rename `executed` field to `has_executed`
Fiery-132 Nov 13, 2025
776e159
added password auth to scheduler
Fiery-132 Nov 13, 2025
4a49c9b
don't expose password in scheduler service logs
Fiery-132 Nov 13, 2025
f827c46
add rate limiting to scheduler service
Fiery-132 Nov 13, 2025
85c90b8
add proof of concept for user auth via jwt in scheduler service
Fiery-132 Nov 14, 2025
a649bff
also store id_token in session token for later decryption
Fiery-132 Nov 14, 2025
cd67184
move code out of main.go file in scheduler service
Fiery-132 Nov 14, 2025
a91cab9
read port from .env when starting scheduler service
Fiery-132 Nov 14, 2025
a9fce91
remove global context variable in scheduler
Fiery-132 Nov 14, 2025
79ed9d0
add function for retrieving decrypted jwt
Fiery-132 Nov 14, 2025
da6bc38
store user identifier with scheduled tasks
Fiery-132 Nov 14, 2025
34891b6
add GET endpoint to scheduler for retrieving tasks per user
Fiery-132 Nov 14, 2025
d46ddeb
use query params for scheduler GET auth
Fiery-132 Nov 14, 2025
bcc48f7
remove hardcoded values from scheduler auth
Fiery-132 Nov 14, 2025
60adb77
improve JWT validation and subject handling in scheduler
Fiery-132 Nov 15, 2025
6cb187c
add refresh token logic to website auth
Fiery-132 Nov 24, 2025
2f0a8ee
implement scheduled publishing for news articles
Fiery-132 Nov 24, 2025
fe24c4a
fix uninitialised global vars in scheduler auth
Fiery-132 Nov 25, 2025
aa13dc0
fix jwt subject logic in scheduler
Fiery-132 Nov 25, 2025
276dd7a
add scheduled news view to news page
Fiery-132 Nov 25, 2025
b7fec5c
only fetch non-executed tasks on GET requests
Fiery-132 Dec 1, 2025
fbc3bbf
improve error handling for fetching scheduled news
Fiery-132 Dec 1, 2025
23a29c2
prevent marking failed scheduled tasks as executed
Fiery-132 Dec 1, 2025
d60f40d
redirect to news page on succesful scheduling
Fiery-132 Dec 1, 2025
e20a8f4
extract news scheduling logic to generic function
Fiery-132 Dec 1, 2025
1e49a53
add cleanup logic to scheduler rate limiters
Fiery-132 Dec 1, 2025
b08acb8
add error handling when fetching user's scheduled news
Fiery-132 Dec 1, 2025
0ad97b0
extend JWT and Session types
Fiery-132 Dec 1, 2025
eba5a5a
remove unused public env var
Fiery-132 Dec 1, 2025
e58f57f
remove fatal log when reading env in scheduler
Fiery-132 Dec 1, 2025
9c56b77
log out user if tokens are invalid
Fiery-132 Dec 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ POSTGRES_URL_NON_POOLING="postgresql://postgres:postgres@localhost:5432/dsek?sch
# AUTH
# Auth.js uses the following environment variables.
AUTH_SECRET="4e0b5eed97d12748be91415ac2716b9e91deb57198c7b3662afe7f1649089b54" # required, generate a 32-bit random string, for example with openssl rand -hex 32
AUTH_TRUST_HOST=true # set to true for authentication in build environment
AUTH_TRUST_HOST=true # set to true for authentication in build environment
AUTH_AUTHENTIK_CLIENT_ID="SvRybUTCGqhNiw2Y3gn1wqt0YxpjW2sv9fbPsUaP"
AUTH_AUTHENTIK_CLIENT_SECRET="" # public dev client, only allows redirect to localhost
PUBLIC_AUTH_AUTHENTIK_ISSUER="https://auth.dsek.se/application/o/dev/"
PUBLIC_AUTH_AUTHENTIK_TOKEN_ENDPOINT="https://auth.dsek.se/application/o/token/"

# AUTHENTIK
# Used to connect to the Authentik server to keep the
# Used to connect to the Authentik server to keep the
# roles and permissions in sync with the webpage.
AUTHENTIK_API_TOKEN=
AUTHENTIK_ENDPOINT=https://auth.dsek.se/api/v3
Expand All @@ -28,8 +29,8 @@ AUTHENTIK_ENABLED=false # set to false to avoid syncing with authentik
# FILE STORAGE
# Used to connect to the MinIO file server.
# Different types of files are stored in different buckets.
MINIO_ROOT_USER= # <|
MINIO_ROOT_PASSWORD= # <| Do not forget to put the values here! Otherwise it wont work!
MINIO_ROOT_USER= # <|
MINIO_ROOT_PASSWORD= # <| Do not forget to put the values here! Otherwise it wont work!
PUBLIC_MINIO_ENDPOINT=files-sandbox.dsek.se
PUBLIC_MINIO_PORT=443
PUBLIC_MINIO_USE_SSL=true
Expand Down Expand Up @@ -94,4 +95,8 @@ BOOKKEEPING_EMAIL_TO_ADDRESS=bookkeeping@example.com
BOOKKEEPING_EMAIL_FROM_ADDRESS=automatic-expensing@dsek.se
BOOKKEEPING_CC_TO_ADDRESS=skattm@dsek.se # comma separated list

SYNC_PASSWORD=password123
SYNC_PASSWORD=password123

# SCHEDULER
SCHEDULER_ENDPOINT=http://localhost:8080/schedule
SCHEDULER_PASSWORD=supersecretpassword
13 changes: 13 additions & 0 deletions scheduler-service/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
POSTGRES_HOST=localhost
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_PORT=5431

PASSWORD=supersecretpassword

SERVER_PORT=8080

JWT_ISSUER=https://auth.dsek.se/application/o/dev/
JWT_AUDIENCE=SvRybUTCGqhNiw2Y3gn1wqt0YxpjW2sv9fbPsUaP
JWKS_ENDPOINT=https://auth.dsek.se/application/o/dev/jwks/
96 changes: 96 additions & 0 deletions scheduler-service/authMiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"context"
"log"
"log/slog"
"net/http"
"os"
"strings"
"time"

"github.com/lestrrat-go/httprc/v3"
"github.com/lestrrat-go/httprc/v3/tracesink"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)

var cachedJWKS jwk.Set

func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cachedJWKS == nil {
if err := createJWKCache(); err != nil {
log.Printf("Failed to create JWK cache: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)

return
}
}

parseOptions := []jwt.ParseOption{
jwt.WithKeySet(cachedJWKS),
jwt.WithIssuer(JWTIssuer),
jwt.WithAudience(JWTAudience),
}

if _, err := jwt.Parse([]byte(getTokenFromHeader(r)), parseOptions...); err != nil {
log.Printf("Failed to parse JWT: %s", err)
http.Error(w, "Unauthorized", http.StatusUnauthorized)

return
}

next.ServeHTTP(w, r)
})
}

func getTokenFromHeader(r *http.Request) string {
stringToken := r.Header.Get("Authorization")

const bearerPrefix = "Bearer "
if !strings.HasPrefix(stringToken, bearerPrefix) {
return ""
}

stringToken = strings.TrimPrefix(stringToken, bearerPrefix)

return stringToken
}

func createJWKCache() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

jwkCache, err := jwk.NewCache(
ctx,
httprc.NewClient(
httprc.WithTraceSink(tracesink.NewSlog(slog.New(slog.NewJSONHandler(os.Stderr, nil)))),
),
)
if err != nil {
log.Printf("Failed to create JWK cache: %s", err)

return err
}

if err = jwkCache.Register(
ctx,
JWKSEndpoint,
jwk.WithMaxInterval(24*time.Hour*7),
jwk.WithMinInterval(5*time.Minute),
); err != nil {
log.Printf("Failed to register JWK endpoint: %s", err)

return err
}

cachedJWKS, err = jwkCache.CachedSet(JWKSEndpoint)
if err != nil {
log.Printf("Failed to get cached JWK set: %s", err)

return err
}

return nil
}
21 changes: 21 additions & 0 deletions scheduler-service/databaseHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"
"os"

"gorm.io/driver/postgres"
"gorm.io/gorm"
)

func openDatabaseConnection(db **gorm.DB) error {
host, user, password, name, port := os.Getenv("POSTGRES_HOST"), os.Getenv("POSTGRES_USER"), os.Getenv("POSTGRES_PASSWORD"), os.Getenv("POSTGRES_DB"), os.Getenv("POSTGRES_PORT")
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", host, user, password, name, port)

var err error
if *db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}); err != nil {
return err
}

return nil
}
35 changes: 35 additions & 0 deletions scheduler-service/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module github.com/Dsek-LTH/scheduler

go 1.25.4

require (
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/httprc/v3 v3.0.1
github.com/lestrrat-go/jwx/v3 v3.0.12
golang.org/x/time v0.14.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)

require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/goccy/go-json v0.10.3 // 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/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
)
68 changes: 68 additions & 0 deletions scheduler-service/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
50 changes: 50 additions & 0 deletions scheduler-service/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"context"
"fmt"
"log"
"net/http"
"os"

"github.com/joho/godotenv"
"gorm.io/gorm"
)

var (
db *gorm.DB
JWKSEndpoint string
JWTIssuer string
JWTAudience string
)

func main() {
if err := godotenv.Load(); err != nil {
log.Println("Error loading .env file")
}

JWKSEndpoint = os.Getenv("JWKS_ENDPOINT")
JWTIssuer = os.Getenv("JWT_ISSUER")
JWTAudience = os.Getenv("JWT_AUDIENCE")

if err := openDatabaseConnection(&db); err != nil {
log.Fatal("Failed to connect to database:", err)
}

if err := db.AutoMigrate(&ScheduledTask{}); err != nil {
log.Fatal("Failed to migrate database:", err)
}

if scheduledTasks, err := gorm.G[ScheduledTask](db).Where("has_executed = ?", false).Find(context.Background()); err != nil {
log.Println("Error fetching scheduled tasks:", err)
} else {
for _, task := range scheduledTasks {
go scheduleTaskExecution(context.Background(), task)
}
}

http.HandleFunc("/schedule", handleRequest)

log.Printf("Server running on :%s", os.Getenv("SERVER_PORT"))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("SERVER_PORT")), nil))
}
69 changes: 69 additions & 0 deletions scheduler-service/rateLimitMiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"log"
"net"
"net/http"
"sync"
"time"

"golang.org/x/time/rate"
)

var (
limiters = make(map[string]*trackedLimiter)
mu sync.Mutex
)

type trackedLimiter struct {
*rate.Limiter
lastSeen time.Time
}

func cleanupLimiters(expiry time.Duration) {
now := time.Now()
for ip, limiter := range limiters {
if now.Sub(limiter.lastSeen) > expiry {
delete(limiters, ip)
}
}
}

func getLimiter(ip string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()

cleanupLimiters(10 * time.Minute)
log.Println("Current limiters:", len(limiters))

lim, exists := limiters[ip]
if !exists {
lim = &trackedLimiter{
Limiter: rate.NewLimiter(1, 5),
lastSeen: time.Now(),
}
limiters[ip] = lim
} else {
lim.lastSeen = time.Now()
}

return lim.Limiter
}

func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}

limiter := getLimiter(host)
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)

return
}

next.ServeHTTP(w, r)
})
}
Loading