feat: merge auth&tester
This commit is contained in:
parent
0a2dea6c23
commit
441af4c6a2
72 changed files with 4910 additions and 2378 deletions
183
internal/sessions/repository/valkey_repository.go
Normal file
183
internal/sessions/repository/valkey_repository.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ValkeyRepository struct {
|
||||
db valkey.Client
|
||||
}
|
||||
|
||||
func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
|
||||
return &ValkeyRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
const SessionLifetime = time.Minute * 40
|
||||
|
||||
func (r *ValkeyRepository) CreateSession(ctx context.Context, session *models.Session) error {
|
||||
const op = "ValkeyRepository.CreateSession"
|
||||
|
||||
data, err := session.JSON()
|
||||
if err != nil {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "cannot marshal session")
|
||||
}
|
||||
|
||||
resp := r.db.Do(ctx, r.db.
|
||||
B().Set().
|
||||
Key(session.Key()).
|
||||
Value(string(data)).
|
||||
Exat(session.ExpiresAt).
|
||||
Build(),
|
||||
)
|
||||
|
||||
err = resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrInternal, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
if #result[2] == 0 then
|
||||
return nil
|
||||
else
|
||||
return redis.call('GET', result[2][1])
|
||||
end`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "ValkeyRepository.ReadSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(readSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
if err := resp.Error(); err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return nil, pkg.Wrap(pkg.ErrNotFound, err, op, "reading session")
|
||||
}
|
||||
return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
session := &models.Session{}
|
||||
|
||||
err := resp.DecodeJSON(session)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted")
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
const (
|
||||
updateSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('EXPIRE', result[2][1], ARGV[2]) == 1`
|
||||
)
|
||||
|
||||
var (
|
||||
sessionLifetimeString = strconv.Itoa(int(SessionLifetime.Seconds()))
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.UpdateSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(updateSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const deleteSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
|
||||
return #result[2] > 0 and redis.call('DEL', result[2][1]) == 1`
|
||||
|
||||
func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "ValkeyRepository.DeleteSession"
|
||||
|
||||
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteSessionScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
deleteUserSessionsScript = `local cursor = 0
|
||||
local dels = 0
|
||||
repeat
|
||||
local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1])
|
||||
for _,key in ipairs(result[2]) do
|
||||
redis.call('DEL', key)
|
||||
dels = dels + 1
|
||||
end
|
||||
cursor = tonumber(result[1])
|
||||
until cursor == 0
|
||||
return dels`
|
||||
)
|
||||
|
||||
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "ValkeyRepository.DeleteAllSessions"
|
||||
|
||||
userIdHash := (&models.Session{UserId: userId}).UserIdHash()
|
||||
|
||||
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
|
||||
ctx,
|
||||
r.db,
|
||||
nil,
|
||||
[]string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
|
||||
)
|
||||
|
||||
err := resp.Error()
|
||||
if err != nil {
|
||||
if valkey.IsValkeyNil(err) {
|
||||
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
|
||||
}
|
||||
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
312
internal/sessions/repository/valkey_repository_test.go
Normal file
312
internal/sessions/repository/valkey_repository_test.go
Normal file
|
@ -0,0 +1,312 @@
|
|||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"github.com/valkey-io/valkey-go/mock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValkeyRepository_CreateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "SET" {
|
||||
return false
|
||||
}
|
||||
if cmd[1] != session.Key() {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != "EXAT" {
|
||||
return false
|
||||
}
|
||||
if cmd[4] != strconv.FormatInt(session.ExpiresAt.Unix(), 10) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.CreateSession(ctx, session)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_ReadSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
UserId: 1,
|
||||
Role: models.RoleAdmin,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(repository.SessionLifetime),
|
||||
UserAgent: "Mozilla/5.0",
|
||||
Ip: "127.0.0.1",
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
d, err := session.JSON()
|
||||
require.NoError(t, err)
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(string(d))))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(res.CreatedAt.Unix(), res.ExpiresAt.UnixNano())
|
||||
fmt.Println(session.CreatedAt.Unix(), session.ExpiresAt.UnixNano())
|
||||
require.EqualExportedValues(t, session, res)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
res, err := sessionRepo.ReadSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
require.Empty(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_UpdateSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.UpdateSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
Id: uuid.NewString(),
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteSession(ctx, session.Id)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValkeyRepository_DeleteAllSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
client := mock.NewClient(ctrl)
|
||||
sessionRepo := repository.NewValkeyRepository(client)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
fmt.Println(cmd)
|
||||
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher)
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
session := &models.Session{
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
matcher := mock.MatchFn(func(cmd []string) bool {
|
||||
if cmd[0] != "EVALSHA" {
|
||||
return false
|
||||
}
|
||||
if cmd[2] != "0" {
|
||||
return false
|
||||
}
|
||||
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
|
||||
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
|
||||
require.ErrorIs(t, err, pkg.ErrNotFound)
|
||||
require.ErrorIs(t, err, valkey.Nil)
|
||||
})
|
||||
}
|
14
internal/sessions/usecase.go
Normal file
14
internal/sessions/usecase.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
78
internal/sessions/usecase/usecase.go
Normal file
78
internal/sessions/usecase/usecase.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/config"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
|
||||
"git.sch9.ru/new_gate/ms-tester/pkg"
|
||||
)
|
||||
|
||||
type SessionsUC struct {
|
||||
sessionsRepo sessions.ValkeyRepository
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func NewUseCase(
|
||||
sessionRepo sessions.ValkeyRepository,
|
||||
cfg config.Config,
|
||||
) *SessionsUC {
|
||||
return &SessionsUC{
|
||||
sessionsRepo: sessionRepo,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) CreateSession(ctx context.Context, creation *models.Session) error {
|
||||
const op = "UseCase.CreateSession"
|
||||
|
||||
err := u.sessionsRepo.CreateSession(ctx, creation)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot create session")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
|
||||
func (u *SessionsUC) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
|
||||
const op = "UseCase.ReadSession"
|
||||
|
||||
session, err := u.sessionsRepo.ReadSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, pkg.Wrap(nil, err, op, "cannot read session")
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) UpdateSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.UpdateSession"
|
||||
|
||||
err := u.sessionsRepo.UpdateSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot update session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteSession(ctx context.Context, sessionId string) error {
|
||||
const op = "UseCase.DeleteSession"
|
||||
|
||||
err := u.sessionsRepo.DeleteSession(ctx, sessionId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SessionsUC) DeleteAllSessions(ctx context.Context, userId int32) error {
|
||||
const op = "UseCase.DeleteAllSessions"
|
||||
|
||||
err := u.sessionsRepo.DeleteAllSessions(ctx, userId)
|
||||
if err != nil {
|
||||
return pkg.Wrap(nil, err, op, "cannot delete all sessions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
14
internal/sessions/valkey_repository.go
Normal file
14
internal/sessions/valkey_repository.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package sessions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.sch9.ru/new_gate/ms-tester/internal/models"
|
||||
)
|
||||
|
||||
type ValkeyRepository interface {
|
||||
CreateSession(ctx context.Context, creation *models.Session) error
|
||||
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
|
||||
UpdateSession(ctx context.Context, sessionId string) error
|
||||
DeleteSession(ctx context.Context, sessionId string) error
|
||||
DeleteAllSessions(ctx context.Context, userId int32) error
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue