feat: add tenant and user management with migrations, handlers, and tests
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRefreshSessionNotFound = errors.New("refresh session not found")
|
||||
ErrRefreshTokenMismatch = errors.New("refresh token mismatch")
|
||||
)
|
||||
|
||||
var rotateRefreshScript = redis.NewScript(`
|
||||
local current = redis.call("GET", KEYS[1])
|
||||
if not current then
|
||||
return 0
|
||||
end
|
||||
if current ~= ARGV[1] then
|
||||
return -1
|
||||
end
|
||||
redis.call("DEL", KEYS[1])
|
||||
redis.call("PSETEX", KEYS[2], ARGV[3], ARGV[2])
|
||||
return 1
|
||||
`)
|
||||
|
||||
type SessionStore struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewSessionStore(rdb *redis.Client) *SessionStore {
|
||||
return &SessionStore{rdb: rdb}
|
||||
}
|
||||
|
||||
func (s *SessionStore) SaveRefresh(ctx context.Context, jti string, tokenHash string, ttl time.Duration) error {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Set(ctx, key, tokenHash, ttl).Err()
|
||||
}
|
||||
|
||||
func (s *SessionStore) GetRefresh(ctx context.Context, jti string) (string, error) {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (s *SessionStore) DeleteRefresh(ctx context.Context, jti string) error {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJTI, newHash string, ttl time.Duration) error {
|
||||
oldKey := fmt.Sprintf("refresh:%s", oldJTI)
|
||||
newKey := fmt.Sprintf("refresh:%s", newJTI)
|
||||
|
||||
result, err := rotateRefreshScript.Run(
|
||||
ctx,
|
||||
s.rdb,
|
||||
[]string{oldKey, newKey},
|
||||
oldHash,
|
||||
newHash,
|
||||
ttl.Milliseconds(),
|
||||
).Int()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch result {
|
||||
case 1:
|
||||
return nil
|
||||
case 0:
|
||||
return ErrRefreshSessionNotFound
|
||||
case -1:
|
||||
return ErrRefreshTokenMismatch
|
||||
default:
|
||||
return fmt.Errorf("unexpected rotate refresh result: %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionStore) Blacklist(ctx context.Context, accessJTI string, ttl time.Duration) error {
|
||||
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||||
return s.rdb.Set(ctx, key, "1", ttl).Err()
|
||||
}
|
||||
|
||||
func (s *SessionStore) IsBlacklisted(ctx context.Context, accessJTI string) (bool, error) {
|
||||
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||||
n, err := s.rdb.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user