Skip to content

Commit

Permalink
feat: redis (#859)
Browse files Browse the repository at this point in the history
Co-authored-by: garrettladley <[email protected]>
Co-authored-by: Garrett Ladley <[email protected]>
  • Loading branch information
3 people authored May 28, 2024
1 parent 70fa435 commit 86f8051
Show file tree
Hide file tree
Showing 38 changed files with 1,099 additions and 210 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ frontend/mobile/android/
tmp/
ios
android
.idea/modules.xml
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
- [PostgreSQL](https://www.postgresql.org/)
- Install through brew: `brew install postgresql@15`
- It requires you to add all the exports to path so read the end of the installation carefully!
- [Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/)
- Install through brew: `brew tap redis-stack/redis-stack`
- Then install: `brew install redis-stack`
- [Trunk](https://marketplace.visualstudio.com/items?itemName=Trunk.io) (Recommended!)
- Visual Studio Code extension for linting/formatting
- [migrate](https://github.com/golang-migrate/migrate)
Expand Down
31 changes: 9 additions & 22 deletions backend/auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,35 +276,22 @@ func copyCustomClaims(claims *jwt.MapClaims, customClaims map[string]interface{}
}
}

func GenerateRefreshCookie(value string) *fiber.Cookie {
return &fiber.Cookie{
func SetResponseTokens(c *fiber.Ctx, tokens *Token) {
c.Set("Authorization", fmt.Sprintf("Bearer %s", tokens.AccessToken))
c.Cookie(&fiber.Cookie{
Name: "refresh_token",
Value: value,
Value: string(tokens.RefreshToken),
Expires: time.Now().Add(constants.REFRESH_TOKEN_EXPIRY),
HTTPOnly: true,
}
})
}

func SetResponseTokens(c *fiber.Ctx, tokens *Token) error {
// Set the tokens in the response
// should also blacklist the old refresh and access tokens

c.Set("Authorization", fmt.Sprintf("Bearer %s", tokens.AccessToken))
func ExpireResponseTokens(c *fiber.Ctx) {
c.Set("Authorization", "")
c.Cookie(&fiber.Cookie{
Name: "refresh_token",
Value: string(tokens.RefreshToken),
Expires: time.Now().Add(constants.REFRESH_TOKEN_EXPIRY),
Value: "",
Expires: time.Now().Add(-time.Hour),
HTTPOnly: true,
})

return nil
}

// func ExpireCookie(name string) *fiber.Cookie {
// return &fiber.Cookie{
// Name: name,
// Value: "",
// Expires: time.Now().Add(-time.Hour),
// HTTPOnly: true,
// }
// }
13 changes: 13 additions & 0 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type Settings struct {
Application ApplicationSettings
Database DatabaseSettings
Redis []RedisSettings
SuperUser SuperUserSettings
Auth AuthSettings
AWS AWSSettings
Expand All @@ -24,6 +25,7 @@ type Settings struct {
type intermediateSettings struct {
Application ApplicationSettings `yaml:"application"`
Database intermediateDatabaseSettings `yaml:"database"`
Redis []intermediateRedisSettings `yaml:"redis"`
SuperUser intermediateSuperUserSettings `yaml:"superuser"`
Auth intermediateAuthSettings `yaml:"authsecret"`
Calendar intermediateCalendarSettings `yaml:"calendar"`
Expand All @@ -35,6 +37,16 @@ func (int *intermediateSettings) into() (*Settings, error) {
return nil, err
}

redisSettings := make([]RedisSettings, len(int.Redis))
for i, r := range int.Redis {
redisInstance, err := r.into()
if err != nil {
return nil, err
}

redisSettings[i] = *redisInstance
}

superUserSettings, err := int.SuperUser.into()
if err != nil {
return nil, err
Expand All @@ -53,6 +65,7 @@ func (int *intermediateSettings) into() (*Settings, error) {
return &Settings{
Application: int.Application,
Database: *databaseSettings,
Redis: redisSettings,
SuperUser: *superUserSettings,
Auth: *authSettings,
Calendar: *calendarSettings,
Expand Down
41 changes: 41 additions & 0 deletions backend/config/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package config

import (
"errors"

m "github.com/garrettladley/mattress"
)

type RedisSettings struct {
Username string `yaml:"username"`
Host string `yaml:"host"`
Port uint `yaml:"port"`
Password *m.Secret[string] `yaml:"password"`
DB int `yaml:"db"`
// TLSConfig *TLSConfig `yaml:"tlsconfig"`
}

func (int *intermediateRedisSettings) into() (*RedisSettings, error) {
password, err := m.NewSecret(int.Password)
if err != nil {
return nil, errors.New("failed to create secret from password")
}

return &RedisSettings{
Username: int.Username,
Host: int.Host,
Port: int.Port,
Password: password,
DB: int.DB,
// TLSConfig: int.TLSConfig.into(),
}, nil
}

type intermediateRedisSettings struct {
Username string `yaml:"username"`
Host string `yaml:"host"`
Port uint `yaml:"port"`
Password string `yaml:"password"`
DB int `yaml:"db"`
// TLSConfig *intermediateTLSConfig `yaml:"tlsconfig"`
}
6 changes: 4 additions & 2 deletions backend/constants/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package constants
import "time"

const (
ACCESS_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30 // temporary TODO: change to 60 minutes
REFRESH_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30
ACCESS_TOKEN_EXPIRY time.Duration = time.Hour * 12
REFRESH_TOKEN_EXPIRY time.Duration = time.Hour * 24 * 30
OTP_LENGTH int = 6
OTP_EXPIRY time.Duration = time.Minute * 5
CSRF_TOKEN_LENGTH int = 32
)

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

import "time"

const (
REDIS_MAX_IDLE_CONNECTIONS int = 10
REDIS_MAX_OPEN_CONNECTIONS int = 100
TOKEN_BLACKLIST_SKEY string = "token_blacklist"
TOKEN_BLACKLIST_KEY string = "blacklisted"
RATE_LIMIT_DURATION time.Duration = 5 * time.Minute
RATE_LIMIT_MAX_REQUESTS int = 5
REDIS_TIMEOUT time.Duration = 200 * time.Millisecond
)
39 changes: 39 additions & 0 deletions backend/database/store/active_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package store

import (
"context"
"time"
)

type ActiveTokenInterface interface {
StoreRefreshToken(ctx context.Context, token string, userID string, expiry time.Duration) error
IsActive(ctx context.Context, token string) (bool, error)
GetRefreshToken(ctx context.Context, token string) (string, error)
DeleteRefreshToken(ctx context.Context, token string) error
}

type ActiveToken struct {
StoreClient StoreClientInterface
}

func NewActiveToken(storeClient StoreClientInterface) *ActiveToken {
return &ActiveToken{
StoreClient: storeClient,
}
}

func (a *ActiveToken) StoreRefreshToken(ctx context.Context, token string, userID string, expiry time.Duration) error {
return a.StoreClient.Set(ctx, token, userID, expiry)
}

func (a *ActiveToken) GetRefreshToken(ctx context.Context, token string) (string, error) {
return a.StoreClient.Get(ctx, token)
}

func (a *ActiveToken) IsActive(ctx context.Context, token string) (bool, error) {
return a.StoreClient.Exists(ctx, token)
}

func (a *ActiveToken) DeleteRefreshToken(ctx context.Context, token string) error {
return a.StoreClient.Del(ctx, token)
}
36 changes: 36 additions & 0 deletions backend/database/store/blacklist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package store

import (
"context"
"time"

"github.com/GenerateNU/sac/backend/constants"
)

type BlacklistInterface interface {
BlacklistToken(ctx context.Context, token string, expiry time.Duration) error
IsTokenBlacklisted(ctx context.Context, token string) (bool, error)
}

type Blacklist struct {
storeClient StoreClientInterface
}

func NewBlacklist(storeClient StoreClientInterface) *Blacklist {
return &Blacklist{
storeClient: storeClient,
}
}

func (b *Blacklist) BlacklistToken(ctx context.Context, token string, expiry time.Duration) error {
err := b.storeClient.SetAdd(ctx, constants.TOKEN_BLACKLIST_SKEY, token)
if err != nil {
return err
}

return b.storeClient.Set(ctx, token, constants.TOKEN_BLACKLIST_KEY, expiry)
}

func (b *Blacklist) IsTokenBlacklisted(ctx context.Context, token string) (bool, error) {
return b.storeClient.SetIsMember(ctx, constants.TOKEN_BLACKLIST_SKEY, token)
}
50 changes: 50 additions & 0 deletions backend/database/store/limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package store

import (
"context"
"time"
)

// LimiterInterface is an implementation of https://github.com/gofiber/storage
type LimiterInterface interface {
Get(key string) ([]byte, error)
Set(key string, val []byte, exp time.Duration) error
Delete(key string) error
Reset() error
Close() error
}

type Limiter struct {
StoreClient StoreClientInterface
}

func NewLimiter(storeClient StoreClientInterface) *Limiter {
return &Limiter{
StoreClient: storeClient,
}
}

func (l *Limiter) Get(key string) ([]byte, error) {
value, err := l.StoreClient.Get(context.Background(), key)
if err != nil {
return nil, err
}

return []byte(value), nil
}

func (l *Limiter) Set(key string, val []byte, exp time.Duration) error {
return l.StoreClient.Set(context.Background(), key, string(val), exp)
}

func (l *Limiter) Delete(key string) error {
return l.StoreClient.Del(context.Background(), key)
}

func (l *Limiter) Reset() error {
return l.StoreClient.FlushAll(context.Background())
}

func (l *Limiter) Close() error {
return l.StoreClient.Close(context.Background())
}
94 changes: 94 additions & 0 deletions backend/database/store/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package store

import (
"bytes"
"fmt"
"log/slog"
"os/exec"

"github.com/GenerateNU/sac/backend/config"
)

type Stores struct {
Limiter LimiterInterface
Blacklist BlacklistInterface
ActiveToken ActiveTokenInterface
}

func NewStores(limiter LimiterInterface, blacklist BlacklistInterface, activeToken ActiveTokenInterface) *Stores {
return &Stores{
Limiter: limiter,
Blacklist: blacklist,
ActiveToken: activeToken,
}
}

func ConfigureRedis(settings config.Settings) *Stores {
var activeTokens *ActiveToken
var blacklist *Blacklist
var limiter *Limiter

for _, service := range settings.Redis {
client := NewRedisClient(service.Username, service.Host, service.Port, service.Password, service.DB)
switch service.Username {
case "redis_active_tokens":
activeTokens = NewActiveToken(client)
case "redis_blacklist":
blacklist = NewBlacklist(client)
case "redis_limiter":
limiter = NewLimiter(client)
default:
slog.Error("unknown redis service", service.Username, "skipping...")
}
}

stores := NewStores(limiter, blacklist, activeTokens)

MustEstablishConn()

return stores
}

// TODO: this will be a part of a larger function that will check all services
func MustEstablishConn() {
isRunning, err := isDockerComposeRunning()
if err != nil {
panic(err)
}

if !*isRunning {
if err := restartServices(); err != nil {
panic(err)
}
}
}

func isDockerComposeRunning() (*bool, error) {
cmd := exec.Command("docker-compose", "ps", "-q")

var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("error checking if docker-compose is running: %s", err.Error())
}

result := out.Len() > 0
return &result, nil
}

func restartServices() error {
cmd := exec.Command("docker-compose", "up", "-d")

var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out

err := cmd.Run()
if err != nil {
return fmt.Errorf("error restarting services: %s \nconsole output: %s", err.Error(), out.String())
}

slog.Info("Services restarted successfully.")
return nil
}
Loading

0 comments on commit 86f8051

Please sign in to comment.