diff --git a/internal/session/repository.go b/internal/session/repository.go new file mode 100644 index 0000000..9645ea4 --- /dev/null +++ b/internal/session/repository.go @@ -0,0 +1,16 @@ +package session + +import ( + "context" + "errors" + + "source.toby3d.me/website/indieauth/internal/domain" +) + +type Repository interface { + Create(ctx context.Context, session *domain.Session) error + GetAndDelete(ctx context.Context, code string) (*domain.Session, error) + GC() +} + +var ErrNotExist = errors.New("session not exist") diff --git a/internal/session/repository/memory/memory_session.go b/internal/session/repository/memory/memory_session.go new file mode 100644 index 0000000..9f69a20 --- /dev/null +++ b/internal/session/repository/memory/memory_session.go @@ -0,0 +1,89 @@ +package memory + +import ( + "context" + "path" + "sync" + "time" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/session" +) + +type ( + Session struct { + CreatedAt time.Time + *domain.Session + } + + memorySessionRepository struct { + store *sync.Map + config *domain.Config + } +) + +const DefaultPathPrefix string = "sessions" + +func NewMemorySessionRepository(config *domain.Config, store *sync.Map) session.Repository { + return &memorySessionRepository{ + config: config, + store: store, + } +} + +func (repo *memorySessionRepository) Create(_ context.Context, state *domain.Session) error { + repo.store.Store(path.Join(DefaultPathPrefix, state.Code), &Session{ + CreatedAt: time.Now().UTC(), + Session: state, + }) + + return nil +} + +func (repo *memorySessionRepository) GetAndDelete(_ context.Context, code string) (*domain.Session, error) { + src, ok := repo.store.LoadAndDelete(path.Join(DefaultPathPrefix, code)) + if !ok { + return nil, session.ErrNotExist + } + + result, ok := src.(*Session) + if !ok { + return nil, session.ErrNotExist + } + + return result.Session, nil +} + +func (repo *memorySessionRepository) GC() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for ts := range ticker.C { + ts := ts + + repo.store.Range(func(key, value interface{}) bool { + k, ok := key.(string) + if !ok { + return false + } + + matched, err := path.Match(DefaultPathPrefix+"/*", k) + if err != nil || !matched { + return false + } + + val, ok := value.(*Session) + if !ok { + return false + } + + if val.CreatedAt.Add(repo.config.Code.Expiry).After(ts) { + return false + } + + repo.store.Delete(key) + + return false + }) + } +} diff --git a/internal/session/usecase.go b/internal/session/usecase.go new file mode 100644 index 0000000..bb92733 --- /dev/null +++ b/internal/session/usecase.go @@ -0,0 +1,11 @@ +package session + +import ( + "context" + + "source.toby3d.me/website/indieauth/internal/domain" +) + +type UseCase interface { + Exchange(ctx context.Context, code string) (*domain.Session, error) +} diff --git a/internal/session/usecase/session_ucase.go b/internal/session/usecase/session_ucase.go new file mode 100644 index 0000000..acde5a2 --- /dev/null +++ b/internal/session/usecase/session_ucase.go @@ -0,0 +1,28 @@ +package usecase + +import ( + "context" + "fmt" + + "source.toby3d.me/website/indieauth/internal/domain" + "source.toby3d.me/website/indieauth/internal/session" +) + +type sessionUseCase struct { + sessions session.Repository +} + +func NewSessionUseCase(sessions session.Repository) session.UseCase { + return &sessionUseCase{ + sessions: sessions, + } +} + +func (useCase *sessionUseCase) Exchange(ctx context.Context, code string) (*domain.Session, error) { + session, err := useCase.sessions.GetAndDelete(ctx, code) + if err != nil { + return nil, fmt.Errorf("cannot find session in store: %w", err) + } + + return session, nil +}