package application
import (
        "github.com/jessicatarra/greenlight/internal/concurrent"
        "github.com/jessicatarra/greenlight/internal/config"
        "github.com/jessicatarra/greenlight/internal/mailer"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        "github.com/jessicatarra/greenlight/ms/auth/internal/infrastructure/repositories"
        "github.com/pascaldekloe/jwt"
        "strconv"
        "sync"
        "time"
)
type appl struct {
        userRepo       domain.UserRepository
        tokenRepo      domain.TokenRepository
        permissionRepo domain.PermissionRepository
        concurrent     concurrent.Resource
        mailer         mailer.Mailer
        cfg            config.Config
}
func NewAppl(userRepo domain.UserRepository, tokenRepo domain.TokenRepository, permissionRepo domain.PermissionRepository, wg *sync.WaitGroup, cfg config.Config) domain.Appl {
        return &appl{
                userRepo:       userRepo,
                tokenRepo:      tokenRepo,
                permissionRepo: permissionRepo,
                concurrent:     concurrent.NewBackgroundTask(wg),
                mailer:         mailer.New(cfg.Smtp.Host, cfg.Smtp.Port, cfg.Smtp.Username, cfg.Smtp.Password, cfg.Smtp.From),
                cfg:            cfg,
        }
}
func (a *appl) CreateUseCase(input *domain.CreateUserRequest, hashedPassword string) (*domain.User, error) {
        user := &domain.User{Name: input.Name, Email: input.Email, Activated: false}
        err := a.userRepo.InsertNewUser(user, hashedPassword)
        if err != nil {
                return nil, err
        }
        err = a.permissionRepo.AddForUser(user.ID, "movies:read")
        if err != nil {
                return nil, err
        }
        token, err := a.tokenRepo.New(user.ID, 3*24*time.Hour, repositories.ScopeActivation)
        if err != nil {
                return nil, err
        }
        fn := func() error {
                data := map[string]interface{}{
                        "activationToken": token.Plaintext,
                        "userID":          user.ID,
                }
                //print(token.Plaintext)
                err = a.mailer.Send(user.Email, "user_welcome.gohtml", data)
                if err != nil {
                        return err
                }
                return nil
        }
        a.concurrent.BackgroundTask(fn)
        return user, err
}
func (a *appl) ActivateUseCase(tokenPlainText string) (*domain.User, error) {
        user, err := a.userRepo.GetForToken(repositories.ScopeActivation, tokenPlainText)
        if err != nil {
                return nil, err
        }
        user.Activated = true
        err = a.userRepo.UpdateUser(user)
        if err != nil {
                return nil, err
        }
        err = a.tokenRepo.DeleteAllForUser(repositories.ScopeActivation, user.ID)
        if err != nil {
                return nil, err
        }
        return user, err
}
func (a *appl) GetByEmailUseCase(email string) (*domain.User, error) {
        existingUser, err := a.userRepo.GetUserByEmail(email)
        if err != nil {
                return nil, err
        }
        return existingUser, nil
}
func (a *appl) CreateAuthTokenUseCase(userID int64) ([]byte, error) {
        var claims jwt.Claims
        claims.Subject = strconv.FormatInt(userID, 10)
        claims.Issued = jwt.NewNumericTime(time.Now())
        claims.NotBefore = jwt.NewNumericTime(time.Now())
        claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour))
        claims.Issuer = a.cfg.Auth.HttpBaseURL
        claims.Audiences = []string{a.cfg.Auth.HttpBaseURL}
        jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(a.cfg.Jwt.Secret))
        if err != nil {
                return nil, err
        }
        return jwtBytes, nil
}
func (a *appl) ValidateAuthTokenUseCase(token string) (*domain.User, error) {
        claims, err := jwt.HMACCheck([]byte(token), []byte(a.cfg.Jwt.Secret))
        if err != nil {
                return nil, err
        }
        if !claims.Valid(time.Now()) {
                return nil, err
        }
        if claims.Issuer != a.cfg.Auth.HttpBaseURL {
                return nil, err
        }
        if !claims.AcceptAudience(a.cfg.Auth.HttpBaseURL) {
                return nil, err
        }
        userID, err := strconv.Atoi(claims.Subject)
        if err != nil {
                return nil, err
        }
        user, err := a.userRepo.GetUserById(int64(userID))
        if err != nil {
                return nil, err
        }
        return user, nil
}
func (a *appl) UserPermissionUseCase(code string, userID int64) error {
        permissions, err := a.permissionRepo.GetAllForUser(userID)
        if err != nil {
                return err
        }
        if !permissions.Include(code) {
                return domain.ErrPermissionNotIncluded
        }
        return nil
}
		
		package http
import (
        "errors"
        _errors "github.com/jessicatarra/greenlight/internal/errors"
        "github.com/jessicatarra/greenlight/internal/password"
        "github.com/jessicatarra/greenlight/internal/request"
        "github.com/jessicatarra/greenlight/internal/response"
        "github.com/jessicatarra/greenlight/internal/utils/helpers"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        "github.com/julienschmidt/httprouter"
        "net/http"
)
type envelope map[string]interface{}
type Handlers interface {
        createUser(res http.ResponseWriter, req *http.Request)
        activateUser(res http.ResponseWriter, req *http.Request)
        createAuthenticationToken(res http.ResponseWriter, req *http.Request)
}
type handlers struct {
        appl    domain.Appl
        helpers helpers.Helpers
}
func (s service) Handlers(router *httprouter.Router) {
        res := registerHandlers(s.appl)
        router.HandlerFunc(http.MethodPost, "/v1/users", res.createUser)
        router.HandlerFunc(http.MethodPut, "/v1/users/activated", res.activateUser)
        router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", res.createAuthenticationToken)
}
func registerHandlers(appl domain.Appl) Handlers {
        return &handlers{
                appl:    appl,
                helpers: helpers.New(),
        }
}
// @Summary Register User
// @Description Registers a new user.
// @Tags Users
// @Accept json
// @Produce  json
// @Param name body domain.CreateUserRequest true "User registration data"
// @Success 201 {object} domain.User
// @Router /users [post]
func (h *handlers) createUser(res http.ResponseWriter, req *http.Request) {
        var input domain.CreateUserRequest
        err := request.DecodeJSON(res, req, &input)
        if err != nil {
                _errors.BadRequest(res, req, err)
                return
        }
        existingUser, err := h.appl.GetByEmailUseCase(input.Email)
        if err != nil && err.Error() != domain.ErrRecordNotFound.Error() {
                _errors.ServerError(res, req, err)
                return
        }
        ValidateUser(&input, existingUser)
        if input.Validator.HasErrors() {
                _errors.FailedValidation(res, req, input.Validator)
                return
        }
        hashedPassword, err := password.Hash(input.Password)
        if err != nil {
                _errors.ServerError(res, req, err)
                return
        }
        user, err := h.appl.CreateUseCase(&input, hashedPassword)
        if err != nil {
                switch {
                case errors.Is(err, domain.ErrDuplicateEmail):
                        input.Validator.AddError("email a user with this email address already exists")
                        _errors.FailedValidation(res, req, input.Validator)
                default:
                        _errors.ServerError(res, req, err)
                }
                return
        }
        err = response.JSON(res, http.StatusCreated, envelope{"user": user})
        if err != nil {
                _errors.ServerError(res, req, err)
        }
}
// @Summary Activate User
// @Description Activates a user account using a token that was previously sent when successfully register a new user
// @Tags Users
// @Accept json
// @Produce  json
// @Param token query string true "Token for user activation"
// @Success 200 {object} domain.User
// @Router /users/activated [put]
func (h *handlers) activateUser(res http.ResponseWriter, req *http.Request) {
        var input domain.ActivateUserRequest
        qs := req.URL.Query()
        input.TokenPlaintext = h.helpers.ReadString(qs, "token", "")
        ValidateToken(&input)
        if input.Validator.HasErrors() {
                _errors.FailedValidation(res, req, input.Validator)
                return
        }
        user, err := h.appl.ActivateUseCase(input.TokenPlaintext)
        if err != nil {
                _errors.ServerError(res, req, err)
                return
        }
        err = response.JSON(res, http.StatusCreated, envelope{"user": user})
        if err != nil {
                _errors.ServerError(res, req, err)
        }
}
// @Summary Create authentication token
// @Description Creates an authentication token for a user
// @Tags Authentication
// @Accept json
// @Produce json
// @Param request body domain.CreateAuthTokenRequest true "Request body"
// @Success 201 {object} domain.Token "Authentication token"
// @Router /tokens/authentication [post]
func (h *handlers) createAuthenticationToken(res http.ResponseWriter, req *http.Request) {
        var input domain.CreateAuthTokenRequest
        err := request.DecodeJSON(res, req, &input)
        if err != nil {
                _errors.BadRequest(res, req, err)
                return
        }
        existingUser, err := h.appl.GetByEmailUseCase(input.Email)
        if err != nil {
                switch {
                case errors.Is(err, domain.ErrRecordNotFound):
                        _errors.InvalidAuthenticationToken(res, req)
                default:
                        _errors.ServerError(res, req, err)
                }
                return
        }
        ValidateEmailForAuth(&input, existingUser)
        if existingUser != nil {
                passwordMatches, err := password.Matches(input.Password, existingUser.HashedPassword)
                if err != nil {
                        _errors.ServerError(res, req, err)
                        return
                }
                ValidatePasswordForAuth(&input, passwordMatches)
        }
        if input.Validator.HasErrors() {
                _errors.FailedValidation(res, req, input.Validator)
                return
        }
        jwtBytes, err := h.appl.CreateAuthTokenUseCase(existingUser.ID)
        if err != nil {
                _errors.ServerError(res, req, err)
                return
        }
        err = response.JSON(res, http.StatusCreated, envelope{"authentication_token": string(jwtBytes)})
        if err != nil {
                _errors.ServerError(res, req, err)
        }
}
		
		package http
import (
        "github.com/jessicatarra/greenlight/internal/middleware"
        "github.com/julienschmidt/httprouter"
        "net/http"
)
func (s service) Routes() http.Handler {
        router := httprouter.New()
        s.Handlers(router)
        m := middleware.NewSharedMiddleware(&s.cfg, s.logger)
        return m.RecoverPanic(m.RateLimit(m.EnableCORS(m.LogRequest(router))))
}
		
		package http
import (
        "github.com/jessicatarra/greenlight/internal/config"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        "github.com/julienschmidt/httprouter"
        "log/slog"
        "net/http"
)
type Service interface {
        Routes() http.Handler
        Handlers(router *httprouter.Router)
}
type service struct {
        appl   domain.Appl
        cfg    config.Config
        logger *slog.Logger
}
func NewService(appl domain.Appl, cfg config.Config, logger *slog.Logger) Service {
        return &service{
                appl:   appl,
                cfg:    cfg,
                logger: logger,
        }
}
		
		package http
import (
        "github.com/jessicatarra/greenlight/internal/password"
        "github.com/jessicatarra/greenlight/internal/utils/validator"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
)
func ValidateUser(input *domain.CreateUserRequest, existingUser *domain.User) {
        input.Validator.CheckField(input.Name != "", "name", "must be provided")
        input.Validator.CheckField(len(input.Name) <= 500, "name", "must not be more than 500 bytes long")
        ValidateEmail(input, existingUser)
        ValidatePassword(input)
}
func ValidatePassword(input *domain.CreateUserRequest) {
        input.Validator.CheckField(input.Password != "", "Password", "Password is required")
        input.Validator.CheckField(len(input.Password) >= 8, "Password", "Password is too short")
        input.Validator.CheckField(len(input.Password) <= 72, "Password", "Password is too long")
        input.Validator.CheckField(validator.NotIn(input.Password, password.CommonPasswords...), "Password", "Password is too common")
}
func ValidateEmail(input *domain.CreateUserRequest, existingUser *domain.User) {
        input.Validator.CheckField(input.Email != "", "Email", "Email is required")
        input.Validator.CheckField(validator.Matches(input.Email, validator.RgxEmail), "Email", "Must be a valid email address")
        input.Validator.CheckField(existingUser == nil, "Email", "Email is already in use")
}
func ValidateToken(input *domain.ActivateUserRequest) {
        input.Validator.Check(input.TokenPlaintext != "", "token must be provided")
        input.Validator.Check(len(input.TokenPlaintext) == 26, "token must be 26 bytes long")
}
func ValidateEmailForAuth(input *domain.CreateAuthTokenRequest, existingUser *domain.User) {
        input.Validator.CheckField(input.Email != "", "Email", "Email is required")
        input.Validator.CheckField(existingUser != nil, "Email", "Email address could not be found")
}
func ValidatePasswordForAuth(input *domain.CreateAuthTokenRequest, passwordMatches bool) {
        input.Validator.CheckField(input.Password != "", "Password", "Password is required")
        input.Validator.CheckField(passwordMatches, "Password", "Password is incorrect")
}
		
		package repositories
import (
        "context"
        "database/sql"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        "github.com/lib/pq"
        "time"
)
type permissionRepository struct {
        db *sql.DB
}
func NewPermissionRepo(db *sql.DB) domain.PermissionRepository {
        return &permissionRepository{db: db}
}
func (p permissionRepository) GetAllForUser(userID int64) (domain.Permissions, error) {
        query := `
        SELECT permissions.code
        FROM permissions
        INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id
        INNER JOIN users ON users_permissions.user_id = users.id
        WHERE users.id = $1`
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()
        rows, err := p.db.QueryContext(ctx, query, userID)
        if err != nil {
                return nil, err
        }
        defer rows.Close()
        var permissions domain.Permissions
        for rows.Next() {
                var permission string
                err := rows.Scan(&permission)
                if err != nil {
                        return nil, err
                }
                permissions = append(permissions, permission)
        }
        if err = rows.Err(); err != nil {
                return nil, err
        }
        return permissions, nil
}
func (p permissionRepository) AddForUser(userID int64, codes ...string) error {
        query := `
        INSERT INTO users_permissions
        SELECT $1, permissions.id FROM permissions WHERE permissions.code = ANY($2)`
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()
        _, err := p.db.ExecContext(ctx, query, userID, pq.Array(codes))
        return err
}
		
		package repositories
import (
        "context"
        "database/sql"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        "time"
)
const (
        ScopeActivation     = "activation"
        ScopeAuthentication = "authentication"
)
type tokenRepository struct {
        db    *sql.DB
        token domain.TokenInterface
}
func NewTokenRepo(db *sql.DB) domain.TokenRepository {
        return &tokenRepository{db: db, token: domain.NewToken()}
}
func (t *tokenRepository) New(userID int64, ttl time.Duration, scope string) (*domain.Token, error) {
        token, err := t.token.GenerateToken(userID, ttl, scope)
        if err != nil {
                return nil, err
        }
        err = t.Insert(token)
        return token, err
}
func (t *tokenRepository) Insert(token *domain.Token) error {
        query := `
        INSERT INTO tokens (hash, user_id, expiry, scope) 
        VALUES ($1, $2, $3, $4)`
        args := []interface{}{token.Hash, token.UserID, token.Expiry, token.Scope}
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        _, err := t.db.ExecContext(ctx, query, args...)
        return err
}
func (t *tokenRepository) DeleteAllForUser(scope string, userID int64) error {
        query := `
        DELETE FROM tokens 
        WHERE scope = $1 AND user_id = $2`
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        _, err := t.db.ExecContext(ctx, query, scope, userID)
        return err
}
		
		package repositories
import (
        "context"
        "crypto/sha256"
        "database/sql"
        "errors"
        _ "github.com/golang-migrate/migrate/v4/database/postgres"
        "github.com/jessicatarra/greenlight/ms/auth/internal/domain"
        _ "github.com/lib/pq"
        "time"
)
const defaultTimeout = 10 * time.Second
type userRepository struct {
        db *sql.DB
}
func NewUserRepo(db *sql.DB) domain.UserRepository {
        return &userRepository{db: db}
}
func (r *userRepository) InsertNewUser(user *domain.User, hashedPassword string) error {
        query := `
        INSERT INTO users (name, email, password_hash, activated) 
        VALUES ($1, $2, $3, $4)
        RETURNING id, created_at, version`
        args := []interface{}{user.Name, user.Email, hashedPassword, user.Activated}
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        err := r.db.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.CreatedAt, &user.Version)
        if err != nil {
                switch {
                case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
                        return domain.ErrDuplicateEmail
                default:
                        return err
                }
        }
        return nil
}
func (r *userRepository) GetUserByEmail(email string) (*domain.User, error) {
        query := `
        SELECT id, created_at, name, email, password_hash, activated, version
        FROM users
        WHERE email = $1`
        var user domain.User
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        err := r.db.QueryRowContext(ctx, query, email).Scan(
                &user.ID,
                &user.CreatedAt,
                &user.Name,
                &user.Email,
                &user.HashedPassword,
                &user.Activated,
                &user.Version,
        )
        if err != nil {
                switch {
                case errors.Is(err, sql.ErrNoRows):
                        return nil, domain.ErrRecordNotFound
                default:
                        return nil, err
                }
        }
        return &user, nil
}
func (r *userRepository) UpdateUser(user *domain.User) error {
        query := `
        UPDATE users SET name = $1, email = $2, password_hash = $3, activated = $4, version = version + 1
        WHERE id = $5 AND version = $6
        RETURNING version`
        args := []interface{}{
                user.Name,
                user.Email,
                user.HashedPassword,
                user.Activated,
                user.ID,
                user.Version,
        }
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        err := r.db.QueryRowContext(ctx, query, args...).Scan(&user.Version)
        if err != nil {
                switch {
                case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
                        return domain.ErrDuplicateEmail
                case errors.Is(err, sql.ErrNoRows):
                        return domain.ErrEditConflict
                default:
                        return err
                }
        }
        return nil
}
func (r *userRepository) GetForToken(tokenScope string, tokenPlaintext string) (*domain.User, error) {
        tokenHash := sha256.Sum256([]byte(tokenPlaintext))
        query := `
        SELECT users.id, users.created_at, users.name, users.email, users.password_hash, users.activated, users.version
        FROM users
        INNER JOIN tokens
        ON users.id = tokens.user_id
        WHERE tokens.hash = $1
        AND tokens.scope = $2 
        AND tokens.expiry > $3`
        args := []interface{}{tokenHash[:], tokenScope, time.Now()}
        var user domain.User
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        err := r.db.QueryRowContext(ctx, query, args...).Scan(
                &user.ID,
                &user.CreatedAt,
                &user.Name,
                &user.Email,
                &user.HashedPassword,
                &user.Activated,
                &user.Version,
        )
        if err != nil {
                switch {
                case errors.Is(err, sql.ErrNoRows):
                        return nil, domain.ErrRecordNotFound
                default:
                        return nil, err
                }
        }
        return &user, nil
}
func (r *userRepository) GetUserById(id int64) (*domain.User, error) {
        query := `
        SELECT id, created_at, name, email, password_hash, activated, version
        FROM users
        WHERE id = $1`
        var user domain.User
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
        defer cancel()
        err := r.db.QueryRowContext(ctx, query, id).Scan(
                &user.ID,
                &user.CreatedAt,
                &user.Name,
                &user.Email,
                &user.HashedPassword,
                &user.Activated,
                &user.Version,
        )
        if err != nil {
                switch {
                case errors.Is(err, sql.ErrNoRows):
                        return nil, domain.ErrRecordNotFound
                default:
                        return nil, err
                }
        }
        return &user, nil
}