Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: redis #859

Merged
merged 13 commits into from
May 28, 2024
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 @@ -292,35 +292,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 @@ -22,6 +23,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 @@ -33,6 +35,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 @@ -51,6 +63,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"`
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
}

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"`
}
4 changes: 2 additions & 2 deletions backend/constants/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ 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
)

var SPECIAL_CHARACTERS = []rune{' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'} // see https://owasp.org/www-community/password-special-characters
6 changes: 6 additions & 0 deletions backend/constants/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package constants

const (
REDIS_MAX_IDLE_CONNECTIONS int = 10
REDIS_MAX_OPEN_CONNECTIONS int = 100
)
36 changes: 36 additions & 0 deletions backend/database/store/active_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package store

import "time"

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

type ActiveToken struct {
StoreClient StoreClientInterface
}

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

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

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

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

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

import "time"

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

type Blacklist struct {
StoreClient StoreClientInterface
}

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

func (b *Blacklist) BlacklistToken(token string, expiry time.Duration) error {
err := b.StoreClient.SAdd("token_blacklist", token)
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

return b.StoreClient.Set(token, "blacklisted", expiry)
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
}

func (b *Blacklist) IsTokenBlacklisted(token string) (bool, error) {
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
return b.StoreClient.SIsMember("token_blacklist", token)
}
47 changes: 47 additions & 0 deletions backend/database/store/limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package store

import "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(storeCleint StoreClientInterface) *Limiter {
return &Limiter{
StoreClient: storeCleint,
}
}

func (l *Limiter) Get(key string) ([]byte, error) {
value, err := l.StoreClient.Get(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(key, val, exp)
}

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

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

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

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

"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(fmt.Sprintf("unknown redis service: %s", service.Username))
}
}

stores := NewStores(limiter, blacklist, activeTokens)

EstablishConn(settings)

return stores
}

func EstablishConn(settings config.Settings) {
services := settings.Redis

for _, service := range services {
if !isContainerRunning(service.Username) {
restartServices()
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

func isContainerRunning(name string) bool {
sanitizedName := strings.ReplaceAll(name, " ", "")
cmd := exec.Command("docker", "ps", "-f", fmt.Sprintf("name=%s", sanitizedName), "-q") //nolint:gosec
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
slog.Error(fmt.Sprintf("error checking if service %s is running: %v\noutput: %s\n", name, err, out.String()))
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
return false
}
return out.Len() > 0
}

func restartServices() {
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
cmd := exec.Command("docker-compose", "up", "-d")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
if err != nil {
slog.Error(fmt.Sprintf("error restarting services: %v\noutput: %s\n", err, out.String()))
DOOduneye marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading