362 lines
8.0 KiB
Go
362 lines
8.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"errors"
|
|
"net/mail"
|
|
"strings"
|
|
"time"
|
|
|
|
"me-fit/db"
|
|
"me-fit/template/auth"
|
|
mailTemplate "me-fit/template/mail"
|
|
"me-fit/types"
|
|
"me-fit/utils"
|
|
|
|
"github.com/a-h/templ"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
var (
|
|
ErrInvaidCredentials = errors.New("invalid email or password")
|
|
ErrInvalidPassword = errors.New("password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character")
|
|
ErrInvalidEmail = errors.New("invalid email")
|
|
ErrAccountExists = errors.New("account already exists")
|
|
ErrSessionIdInvalid = errors.New("session ID is invalid")
|
|
)
|
|
|
|
type User struct {
|
|
Id uuid.UUID
|
|
Email string
|
|
EmailVerified bool
|
|
}
|
|
|
|
func NewUser(user *db.User) *User {
|
|
return &User{
|
|
Id: user.Id,
|
|
Email: user.Email,
|
|
EmailVerified: user.EmailVerified,
|
|
}
|
|
}
|
|
|
|
type Session struct {
|
|
Id string
|
|
CreatedAt time.Time
|
|
User *User
|
|
}
|
|
|
|
func NewSession(session *db.Session, user *User) *Session {
|
|
return &Session{
|
|
Id: session.Id,
|
|
CreatedAt: session.CreatedAt,
|
|
User: user,
|
|
}
|
|
}
|
|
|
|
type AuthService interface {
|
|
SignIn(email string, password string) (*Session, error)
|
|
SignUp(email string, password string) (*User, error)
|
|
SendVerificationMail(userId uuid.UUID, email string)
|
|
VerifyUserEmail(token string) error
|
|
SignOut(sessionId string) error
|
|
DeleteAccount(user *User) error
|
|
ChangePassword(user *User, currPass, newPass string) error
|
|
ForgotPassword(email string) error
|
|
ForgotPasswordResponse(token string, newPass string) error
|
|
|
|
GetUserFromSessionId(sessionId string) (*User, error)
|
|
}
|
|
|
|
type AuthServiceImpl struct {
|
|
dbAuth db.AuthDb
|
|
randomGenerator RandomService
|
|
clock ClockService
|
|
mailService MailService
|
|
serverSettings *types.ServerSettings
|
|
}
|
|
|
|
func NewAuthServiceImpl(dbAuth db.AuthDb, randomGenerator RandomService, clock ClockService, mailService MailService, serverSettings *types.ServerSettings) *AuthServiceImpl {
|
|
return &AuthServiceImpl{
|
|
dbAuth: dbAuth,
|
|
randomGenerator: randomGenerator,
|
|
clock: clock,
|
|
mailService: mailService,
|
|
serverSettings: serverSettings,
|
|
}
|
|
}
|
|
|
|
func (service AuthServiceImpl) SignIn(email string, password string) (*Session, error) {
|
|
user, err := service.dbAuth.GetUser(email)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
return nil, ErrInvaidCredentials
|
|
} else {
|
|
return nil, types.ErrInternal
|
|
}
|
|
}
|
|
|
|
hash := GetHashPassword(password, user.Salt)
|
|
|
|
if subtle.ConstantTimeCompare(hash, user.Password) == 0 {
|
|
return nil, ErrInvaidCredentials
|
|
}
|
|
|
|
session, err := service.createSession(user.Id)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
return NewSession(session, NewUser(user)), nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) createSession(userId uuid.UUID) (*db.Session, error) {
|
|
sessionId, err := service.randomGenerator.String(32)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
err = service.dbAuth.DeleteOldSessions(userId)
|
|
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
session := db.NewSession(sessionId, userId, service.clock.Now())
|
|
|
|
err = service.dbAuth.InsertSession(session)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) SignUp(email string, password string) (*User, error) {
|
|
_, err := mail.ParseAddress(email)
|
|
if err != nil {
|
|
return nil, ErrInvalidEmail
|
|
}
|
|
|
|
if !isPasswordValid(password) {
|
|
return nil, ErrInvalidPassword
|
|
}
|
|
|
|
userId, err := service.randomGenerator.UUID()
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
salt, err := service.randomGenerator.Bytes(16)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
hash := GetHashPassword(password, salt)
|
|
|
|
dbUser := db.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
|
|
|
|
err = service.dbAuth.InsertUser(dbUser)
|
|
if err != nil {
|
|
if err == db.ErrUserExists {
|
|
return nil, ErrAccountExists
|
|
} else {
|
|
return nil, types.ErrInternal
|
|
}
|
|
}
|
|
|
|
return NewUser(dbUser), nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) SendVerificationMail(userId uuid.UUID, email string) {
|
|
var token string
|
|
|
|
token, err := service.dbAuth.GetEmailVerificationToken(userId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if token == "" {
|
|
token, err := service.randomGenerator.String(32)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = service.dbAuth.InsertEmailVerificationToken(userId, token)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
var w strings.Builder
|
|
err = mailTemplate.Register(service.serverSettings.BaseUrl, token).Render(context.Background(), &w)
|
|
if err != nil {
|
|
utils.LogError("Could not render welcome email", err)
|
|
return
|
|
}
|
|
|
|
service.mailService.SendMail(email, "Welcome to ME-FIT", w.String())
|
|
}
|
|
|
|
func (service AuthServiceImpl) VerifyUserEmail(token string) error {
|
|
|
|
if token == "" {
|
|
return types.ErrInternal
|
|
}
|
|
|
|
return service.dbAuth.VerifyEmail(token)
|
|
}
|
|
|
|
func (service AuthServiceImpl) SignOut(sessionId string) error {
|
|
|
|
return service.dbAuth.DeleteSession(sessionId)
|
|
}
|
|
|
|
func (service AuthServiceImpl) GetUserFromSessionId(sessionId string) (*User, error) {
|
|
if sessionId == "" {
|
|
return nil, ErrSessionIdInvalid
|
|
}
|
|
|
|
session, err := service.dbAuth.GetSession(sessionId)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
user, err := service.dbAuth.GetUserById(session.UserId)
|
|
if err != nil {
|
|
return nil, types.ErrInternal
|
|
}
|
|
|
|
if session.CreatedAt.Add(time.Duration(8 * time.Hour)).Before(service.clock.Now()) {
|
|
return nil, nil
|
|
} else {
|
|
return NewUser(user), nil
|
|
}
|
|
}
|
|
|
|
// TODO
|
|
|
|
func UserInfoComp(user *User) templ.Component {
|
|
|
|
if user != nil {
|
|
return auth.UserComp(user.Email)
|
|
} else {
|
|
return auth.UserComp("")
|
|
}
|
|
}
|
|
|
|
func (service AuthServiceImpl) DeleteAccount(user *User) error {
|
|
|
|
err := service.dbAuth.DeleteUser(user.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go service.mailService.SendMail(user.Email, "Account deleted", "Your account has been deleted")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) ChangePassword(user *User, currPass, newPass string) error {
|
|
|
|
if !isPasswordValid(newPass) {
|
|
return ErrInvalidPassword
|
|
}
|
|
|
|
if currPass == newPass {
|
|
return ErrInvalidPassword
|
|
}
|
|
|
|
_, err := service.SignIn(user.Email, currPass)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userDb, err := service.dbAuth.GetUserById(user.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newHash := GetHashPassword(newPass, userDb.Salt)
|
|
|
|
err = service.dbAuth.UpdateUserPassword(user.Id, newHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) ForgotPassword(email string) error {
|
|
|
|
token, err := service.randomGenerator.String(32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = service.dbAuth.InsertForgotPasswordToken(email, token)
|
|
|
|
if err != db.ErrNotFound {
|
|
var mail strings.Builder
|
|
err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token).Render(context.Background(), &mail)
|
|
if err != nil {
|
|
utils.LogError("Could not render reset password email", err)
|
|
return types.ErrInternal
|
|
}
|
|
service.mailService.SendMail(email, "Reset Password", mail.String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service AuthServiceImpl) ForgotPasswordResponse(tokenStr string, newPass string) error {
|
|
|
|
if !isPasswordValid(newPass) {
|
|
return ErrInvalidPassword
|
|
}
|
|
|
|
token, err := service.dbAuth.GetToken(tokenStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = service.dbAuth.DeleteToken(tokenStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := service.dbAuth.GetUserById(token.UserId)
|
|
if err != nil {
|
|
utils.LogError("Could not get user from token", err)
|
|
return types.ErrInternal
|
|
}
|
|
|
|
passHash := GetHashPassword(newPass, user.Salt)
|
|
|
|
user.Password = passHash
|
|
err = service.dbAuth.UpdateUser(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetHashPassword(password string, salt []byte) []byte {
|
|
return argon2.IDKey([]byte(password), salt, 1, 64*1024, 1, 16)
|
|
}
|
|
|
|
func isPasswordValid(password string) bool {
|
|
|
|
if len(password) < 8 ||
|
|
!strings.ContainsAny(password, "0123456789") ||
|
|
!strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") ||
|
|
!strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") ||
|
|
!strings.ContainsAny(password, "!@#$%^&*()_+-=[]{}\\|;:'\",.<>/?") {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|