| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package user
- import (
- "fmt"
- "net/http"
- "net/url"
- "time"
- "github.com/porter-dev/porter/api/server/handlers"
- "github.com/porter-dev/porter/api/server/shared"
- "github.com/porter-dev/porter/api/server/shared/apierrors"
- "github.com/porter-dev/porter/api/server/shared/config"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/internal/models"
- "github.com/porter-dev/porter/internal/notifier"
- "github.com/porter-dev/porter/internal/random"
- "github.com/porter-dev/porter/internal/repository"
- "golang.org/x/crypto/bcrypt"
- "gorm.io/gorm"
- )
- type UserPasswordInitiateResetHandler struct {
- handlers.PorterHandlerReader
- }
- func NewUserPasswordInitiateResetHandler(
- config *config.Config,
- decoderValidator shared.RequestDecoderValidator,
- writer shared.ResultWriter,
- ) *UserPasswordInitiateResetHandler {
- return &UserPasswordInitiateResetHandler{
- PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
- }
- }
- func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- request := &types.InitiateResetUserPasswordRequest{}
- ok := c.DecodeAndValidate(w, r, request)
- if !ok {
- return
- }
- // check that the email exists; return 200 status code even if it doesn't
- user, err := c.Repo().User().ReadUserByEmail(request.Email)
- if err == gorm.ErrRecordNotFound {
- w.WriteHeader(http.StatusOK)
- return
- } else if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- if err := checkUserRestrictions(c.Config().ServerConf, user.Email); err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
- return
- }
- // if the user is a Github user, send them a Github email
- if user.GithubUserID != 0 {
- err := c.Config().UserNotifier.SendGithubRelinkEmail(
- ¬ifier.SendGithubRelinkEmailOpts{
- Email: user.Email,
- URL: fmt.Sprintf("%s/api/oauth/login/github", c.Config().ServerConf.ServerURL),
- },
- )
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- w.WriteHeader(http.StatusOK)
- return
- }
- pwReset, rawToken, err := CreatePWResetTokenForEmail(
- c.Repo().PWResetToken(),
- c.HandleAPIError,
- w,
- r,
- request,
- )
- if err != nil {
- return
- }
- queryVals := url.Values{
- "token": []string{rawToken},
- "email": []string{request.Email},
- "token_id": []string{fmt.Sprintf("%d", pwReset.ID)},
- }
- err = c.Config().UserNotifier.SendPasswordResetEmail(
- ¬ifier.SendPasswordResetEmailOpts{
- Email: user.Email,
- URL: fmt.Sprintf("%s/password/reset/finalize?%s", c.Config().ServerConf.ServerURL, queryVals.Encode()),
- },
- )
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- w.WriteHeader(http.StatusOK)
- return
- }
- type UserPasswordVerifyResetHandler struct {
- handlers.PorterHandlerReader
- }
- func NewUserPasswordVerifyResetHandler(
- config *config.Config,
- decoderValidator shared.RequestDecoderValidator,
- writer shared.ResultWriter,
- ) *UserPasswordVerifyResetHandler {
- return &UserPasswordVerifyResetHandler{
- PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
- }
- }
- func (c *UserPasswordVerifyResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- request := &types.VerifyResetUserPasswordRequest{}
- ok := c.DecodeAndValidate(w, r, request)
- if !ok {
- return
- }
- VerifyToken(
- c.Repo().PWResetToken(),
- c.HandleAPIError,
- w,
- r,
- &request.VerifyTokenFinalizeRequest,
- request.Email,
- )
- }
- type UserPasswordFinalizeResetHandler struct {
- handlers.PorterHandlerReader
- }
- func NewUserPasswordFinalizeResetHandler(
- config *config.Config,
- decoderValidator shared.RequestDecoderValidator,
- writer shared.ResultWriter,
- ) *UserPasswordFinalizeResetHandler {
- return &UserPasswordFinalizeResetHandler{
- PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
- }
- }
- func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- request := &types.FinalizeResetUserPasswordRequest{}
- ok := c.DecodeAndValidate(w, r, request)
- if !ok {
- return
- }
- token, err := VerifyToken(
- c.Repo().PWResetToken(),
- c.HandleAPIError,
- w,
- r,
- &request.VerifyTokenFinalizeRequest,
- request.Email,
- )
- if err != nil {
- return
- }
- // check that the email exists
- user, err := c.Repo().User().ReadUserByEmail(request.Email)
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- err = fmt.Errorf("finalize password reset failed: email does not exist")
- c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
- } else {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- }
- return
- }
- hashedPW, err := bcrypt.GenerateFromPassword([]byte(request.NewPassword), 8)
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- user.Password = string(hashedPW)
- user, err = c.Repo().User().UpdateUser(user)
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- // invalidate the token
- token.IsValid = false
- _, err = c.Repo().PWResetToken().UpdatePWResetToken(token)
- if err != nil {
- c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
- return
- }
- w.WriteHeader(http.StatusOK)
- return
- }
- func VerifyToken(
- pwResetRepo repository.PWResetTokenRepository,
- handleErr func(w http.ResponseWriter, r *http.Request, err apierrors.RequestError),
- w http.ResponseWriter,
- r *http.Request,
- request *types.VerifyTokenFinalizeRequest,
- email string,
- ) (*models.PWResetToken, error) {
- token, err := pwResetRepo.ReadPWResetToken(request.TokenID)
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- err = fmt.Errorf("verify token failed: token does not exist")
- handleErr(w, r, apierrors.NewErrForbidden(err))
- return nil, err
- } else {
- handleErr(w, r, apierrors.NewErrInternal(err))
- }
- return nil, err
- }
- // make sure the token is still valid and has not expired
- if !token.IsValid || token.IsExpired() {
- err = fmt.Errorf("verify token failed: expired %t, valid %t", token.IsExpired(), token.IsValid)
- handleErr(w, r, apierrors.NewErrForbidden(err))
- return nil, err
- }
- // check that the email matches
- if token.Email != email {
- err = fmt.Errorf("verify token failed: token email does not match request email")
- handleErr(w, r, apierrors.NewErrForbidden(err))
- return nil, err
- }
- // make sure the token is correct
- if err := bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(request.Token)); err != nil {
- err = fmt.Errorf("verify token failed: %s", err)
- handleErr(w, r, apierrors.NewErrForbidden(err))
- return nil, err
- }
- return token, nil
- }
- func CreatePWResetTokenForEmail(
- pwResetRepo repository.PWResetTokenRepository,
- handleErr func(w http.ResponseWriter, r *http.Request, err apierrors.RequestError),
- w http.ResponseWriter,
- r *http.Request,
- request *types.InitiateResetUserPasswordRequest,
- ) (*models.PWResetToken, string, error) {
- // convert the form to a project model
- expiry := time.Now().Add(30 * time.Minute)
- rawToken, err := random.StringWithCharset(32, "")
- if err != nil {
- handleErr(w, r, apierrors.NewErrInternal(err))
- return nil, "", err
- }
- hashedToken, err := bcrypt.GenerateFromPassword([]byte(rawToken), 8)
- if err != nil {
- handleErr(w, r, apierrors.NewErrInternal(err))
- return nil, "", err
- }
- pwReset := &models.PWResetToken{
- Email: request.Email,
- IsValid: true,
- Expiry: &expiry,
- Token: string(hashedToken),
- }
- // handle write to the database
- pwReset, err = pwResetRepo.CreatePWResetToken(pwReset)
- if err != nil {
- handleErr(w, r, apierrors.NewErrInternal(err))
- return nil, "", err
- }
- return pwReset, rawToken, nil
- }
|