changing layout
This commit is contained in:
parent
17b5f751e2
commit
b1c6aa659b
37 changed files with 695 additions and 1018 deletions
|
|
@ -1,97 +0,0 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/example/cms/internal/domain"
|
||||
"github.com/example/cms/internal/identity"
|
||||
"github.com/example/cms/internal/repository"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IUserService interface {
|
||||
Login(context.Context, identity.Identity) (domain.User, string, error)
|
||||
Authenticate(context.Context, string) (domain.Session, error)
|
||||
}
|
||||
type IAdminAuthService interface {
|
||||
Login(context.Context, string) (domain.Admin, string, error)
|
||||
Authenticate(context.Context, string) (domain.Session, error)
|
||||
}
|
||||
type AuthService struct {
|
||||
users repository.IUserRepository
|
||||
admins repository.IAdminRepository
|
||||
userSessions repository.ISessionRepository
|
||||
adminSessions repository.ISessionRepository
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func NewAuthService(u repository.IUserRepository, a repository.IAdminRepository, sessions repository.ISessionRepository, ttl time.Duration) *AuthService {
|
||||
return NewAuthServiceWithSeparateSessions(u, a, sessions, sessions, ttl)
|
||||
}
|
||||
|
||||
func NewAuthServiceWithSeparateSessions(u repository.IUserRepository, a repository.IAdminRepository, userSessions, adminSessions repository.ISessionRepository, ttl time.Duration) *AuthService {
|
||||
return &AuthService{users: u, admins: a, userSessions: userSessions, adminSessions: adminSessions, ttl: ttl}
|
||||
}
|
||||
func (s *AuthService) Login(ctx context.Context, id identity.Identity) (domain.User, string, error) {
|
||||
if strings.TrimSpace(id.Email) == "" || strings.TrimSpace(id.Subject) == "" {
|
||||
return domain.User{}, "", errors.New("identity email and subject are required")
|
||||
}
|
||||
u, e := s.users.FindOrCreateByIdentity(ctx, domain.User{Email: id.Email, Name: id.Name, Provider: "google", ProviderSubject: id.Subject, CreatedAt: time.Now()})
|
||||
if e != nil {
|
||||
return domain.User{}, "", e
|
||||
}
|
||||
t, e := newToken()
|
||||
if e != nil {
|
||||
return domain.User{}, "", e
|
||||
}
|
||||
if e = s.userSessions.Create(ctx, domain.Session{Token: t, UserID: &u.ID, ExpiresAt: time.Now().Add(s.ttl), CreatedAt: time.Now()}); e != nil {
|
||||
return domain.User{}, "", e
|
||||
}
|
||||
return u, t, nil
|
||||
}
|
||||
func (s *AuthService) Authenticate(ctx context.Context, t string) (domain.Session, error) {
|
||||
return s.userSessions.FindValid(ctx, t)
|
||||
}
|
||||
func (s *AuthService) AdminLogin(ctx context.Context, email string) (domain.Admin, string, error) {
|
||||
a, e := s.admins.FindByEmail(ctx, email)
|
||||
if e != nil {
|
||||
return domain.Admin{}, "", errors.New("invalid admin credentials")
|
||||
}
|
||||
t, e := newToken()
|
||||
if e != nil {
|
||||
return domain.Admin{}, "", e
|
||||
}
|
||||
if e = s.adminSessions.Create(ctx, domain.Session{Token: t, AdminID: &a.ID, ExpiresAt: time.Now().Add(s.ttl), CreatedAt: time.Now()}); e != nil {
|
||||
return domain.Admin{}, "", e
|
||||
}
|
||||
return a, t, nil
|
||||
}
|
||||
func (s *AuthService) AdminAuthenticate(ctx context.Context, t string) (domain.Session, error) {
|
||||
return s.adminSessions.FindValid(ctx, t)
|
||||
}
|
||||
func (s *AuthService) UserByID(ctx context.Context, id uint) (domain.User, error) {
|
||||
if lookup, ok := s.users.(repository.IUserLookup); ok {
|
||||
return lookup.FindByID(ctx, id)
|
||||
}
|
||||
return domain.User{}, errors.New("user lookup unavailable")
|
||||
}
|
||||
func (s *AuthService) CanEdit(ctx context.Context, session domain.Session) bool {
|
||||
if session.AdminID != nil {
|
||||
return true
|
||||
}
|
||||
if session.UserID == nil {
|
||||
return false
|
||||
}
|
||||
u, err := s.UserByID(ctx, *session.UserID)
|
||||
return err == nil && (u.Editor || u.Role == "editor" || u.Role == "admin")
|
||||
}
|
||||
func newToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, e := rand.Read(b); e != nil {
|
||||
return "", e
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/example/cms/internal/domain"
|
||||
"github.com/example/cms/internal/identity"
|
||||
)
|
||||
|
||||
type fakeUsers struct{}
|
||||
|
||||
func (fakeUsers) FindOrCreateByIdentity(_ context.Context, u domain.User) (domain.User, error) {
|
||||
u.ID = 7
|
||||
return u, nil
|
||||
}
|
||||
|
||||
type fakeAdmins struct{}
|
||||
|
||||
func (fakeAdmins) FindByEmail(context.Context, string) (domain.Admin, error) {
|
||||
return domain.Admin{ID: 2, Email: "admin@example.com"}, nil
|
||||
}
|
||||
|
||||
type fakeSessions struct{ created domain.Session }
|
||||
|
||||
func (f *fakeSessions) Create(_ context.Context, s domain.Session) error { f.created = s; return nil }
|
||||
func (f *fakeSessions) FindValid(context.Context, string) (domain.Session, error) {
|
||||
return f.created, nil
|
||||
}
|
||||
|
||||
func TestAuthServiceCreatesUserSession(t *testing.T) {
|
||||
sessions := &fakeSessions{}
|
||||
s := NewAuthService(fakeUsers{}, fakeAdmins{}, sessions, time.Hour)
|
||||
u, token, err := s.Login(context.Background(), identity.Identity{Subject: "sub", Email: "user@example.com"})
|
||||
if err != nil || token == "" || u.ID != 7 || sessions.created.UserID == nil {
|
||||
t.Fatalf("login failed: user=%+v token=%q session=%+v err=%v", u, token, sessions.created, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,20 +11,21 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// IContentService exposes the content use cases. Read operations are anonymous;
|
||||
// Update is only reachable once the web layer verified the SSO proxy identity.
|
||||
type IContentService interface {
|
||||
List(context.Context, int, int, string, string, []string) ([]domain.Content, error)
|
||||
ListCategories(context.Context) ([]domain.Category, error)
|
||||
ListBadges(context.Context) ([]domain.Badge, error)
|
||||
Get(context.Context, string) (domain.Content, error)
|
||||
Update(context.Context, domain.Content, domain.Session) (domain.Content, error)
|
||||
Update(context.Context, domain.Content) (domain.Content, error)
|
||||
}
|
||||
type ContentService struct {
|
||||
repo repository.IContentRepository
|
||||
auth *AuthService
|
||||
}
|
||||
|
||||
func NewContentService(r repository.IContentRepository, a *AuthService) *ContentService {
|
||||
return &ContentService{repo: r, auth: a}
|
||||
func NewContentService(r repository.IContentRepository) *ContentService {
|
||||
return &ContentService{repo: r}
|
||||
}
|
||||
func (s *ContentService) List(ctx context.Context, limit, offset int, category, status string, badges []string) ([]domain.Content, error) {
|
||||
if limit < 1 {
|
||||
|
|
@ -50,10 +51,7 @@ func (s *ContentService) Get(ctx context.Context, slug string) (domain.Content,
|
|||
}
|
||||
return s.repo.FindBySlug(ctx, slug)
|
||||
}
|
||||
func (s *ContentService) Update(ctx context.Context, c domain.Content, session domain.Session) (domain.Content, error) {
|
||||
if s.auth == nil || !s.auth.CanEdit(ctx, session) {
|
||||
return domain.Content{}, errors.New("forbidden")
|
||||
}
|
||||
func (s *ContentService) Update(ctx context.Context, c domain.Content) (domain.Content, error) {
|
||||
if !validSlug(c.Slug) || strings.TrimSpace(c.Title) == "" || strings.TrimSpace(c.Body) == "" {
|
||||
return domain.Content{}, errors.New("invalid content")
|
||||
}
|
||||
|
|
@ -63,15 +61,11 @@ func (s *ContentService) Update(ctx context.Context, c domain.Content, session d
|
|||
}
|
||||
c.ID = old.ID
|
||||
c.CreatedAt = old.CreatedAt
|
||||
c.CreatedBy = old.CreatedBy
|
||||
c.UpdatedAt = time.Now()
|
||||
c.Badges = normalizeBadges(c.Badges)
|
||||
if session.UserID != nil {
|
||||
c.UpdatedBy = *session.UserID
|
||||
}
|
||||
|
||||
snapshot, _ := json.Marshal(c)
|
||||
h := domain.ContentHistory{ContentID: c.ID, Operation: "updated", CreatedAt: c.UpdatedAt, Snapshot: string(snapshot), ActorID: session.UserID, ActorAdminID: session.AdminID}
|
||||
h := domain.ContentHistory{ContentID: c.ID, Operation: "updated", CreatedAt: c.UpdatedAt, Snapshot: string(snapshot)}
|
||||
if err = s.repo.Update(ctx, c, h); err != nil {
|
||||
return domain.Content{}, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue