mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: password resets (resolve #133)
This commit is contained in:
parent
e6e134678a
commit
6ad33e3c3b
@ -49,7 +49,13 @@ const (
|
|||||||
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
|
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
|
||||||
)
|
)
|
||||||
|
|
||||||
var emailProviders = []string{"mailwhale"}
|
const (
|
||||||
|
MailProviderMailWhale = "mailwhale"
|
||||||
|
)
|
||||||
|
|
||||||
|
var emailProviders = []string{
|
||||||
|
MailProviderMailWhale,
|
||||||
|
}
|
||||||
|
|
||||||
var cfg *Config
|
var cfg *Config
|
||||||
var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
const (
|
const (
|
||||||
IndexTemplate = "index.tpl.html"
|
IndexTemplate = "index.tpl.html"
|
||||||
LoginTemplate = "login.tpl.html"
|
LoginTemplate = "login.tpl.html"
|
||||||
ImprintTemplate = "imprint.tpl.html"
|
ImprintTemplate = "imprint.tpl.html"
|
||||||
SignupTemplate = "signup.tpl.html"
|
SignupTemplate = "signup.tpl.html"
|
||||||
SettingsTemplate = "settings.tpl.html"
|
SetPasswordTemplate = "set-password.tpl.html"
|
||||||
SummaryTemplate = "summary.tpl.html"
|
ResetPasswordTemplate = "reset-password.tpl.html"
|
||||||
|
SettingsTemplate = "settings.tpl.html"
|
||||||
|
SummaryTemplate = "summary.tpl.html"
|
||||||
)
|
)
|
||||||
|
5
main.go
5
main.go
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/muety/wakapi/migrations"
|
"github.com/muety/wakapi/migrations"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
"github.com/muety/wakapi/routes/api"
|
"github.com/muety/wakapi/routes/api"
|
||||||
|
"github.com/muety/wakapi/services/mail"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"log"
|
"log"
|
||||||
@ -51,6 +52,7 @@ var (
|
|||||||
languageMappingService services.ILanguageMappingService
|
languageMappingService services.ILanguageMappingService
|
||||||
summaryService services.ISummaryService
|
summaryService services.ISummaryService
|
||||||
aggregationService services.IAggregationService
|
aggregationService services.IAggregationService
|
||||||
|
mailService services.IMailService
|
||||||
keyValueService services.IKeyValueService
|
keyValueService services.IKeyValueService
|
||||||
miscService services.IMiscService
|
miscService services.IMiscService
|
||||||
)
|
)
|
||||||
@ -134,6 +136,7 @@ func main() {
|
|||||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||||
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
|
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
|
||||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||||
|
mailService = mail.NewMailService()
|
||||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||||
miscService = services.NewMiscService(userService, summaryService, keyValueService)
|
miscService = services.NewMiscService(userService, summaryService, keyValueService)
|
||||||
|
|
||||||
@ -159,7 +162,7 @@ func main() {
|
|||||||
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
||||||
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
|
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
|
||||||
homeHandler := routes.NewHomeHandler(keyValueService)
|
homeHandler := routes.NewHomeHandler(keyValueService)
|
||||||
loginHandler := routes.NewLoginHandler(userService)
|
loginHandler := routes.NewLoginHandler(userService, mailService)
|
||||||
imprintHandler := routes.NewImprintHandler(keyValueService)
|
imprintHandler := routes.NewImprintHandler(keyValueService)
|
||||||
|
|
||||||
// Setup Routers
|
// Setup Routers
|
||||||
|
@ -30,6 +30,7 @@ type User struct {
|
|||||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||||
WakatimeApiKey string `json:"-"`
|
WakatimeApiKey string `json:"-"`
|
||||||
|
ResetToken string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
@ -44,6 +45,16 @@ type Signup struct {
|
|||||||
PasswordRepeat string `schema:"password_repeat"`
|
PasswordRepeat string `schema:"password_repeat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetPasswordRequest struct {
|
||||||
|
Password string `schema:"password"`
|
||||||
|
PasswordRepeat string `schema:"password_repeat"`
|
||||||
|
Token string `schema:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetPasswordRequest struct {
|
||||||
|
Email string `schema:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
type CredentialsReset struct {
|
type CredentialsReset struct {
|
||||||
PasswordOld string `schema:"password_old"`
|
PasswordOld string `schema:"password_old"`
|
||||||
PasswordNew string `schema:"password_new"`
|
PasswordNew string `schema:"password_new"`
|
||||||
@ -69,6 +80,11 @@ func (c *CredentialsReset) IsValid() bool {
|
|||||||
c.PasswordNew == c.PasswordRepeat
|
c.PasswordNew == c.PasswordRepeat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SetPasswordRequest) IsValid() bool {
|
||||||
|
return ValidatePassword(c.Password) &&
|
||||||
|
c.Password == c.PasswordRepeat
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Signup) IsValid() bool {
|
func (s *Signup) IsValid() bool {
|
||||||
return ValidateUsername(s.Username) &&
|
return ValidateUsername(s.Username) &&
|
||||||
ValidateEmail(s.Email) &&
|
ValidateEmail(s.Email) &&
|
||||||
|
@ -6,6 +6,11 @@ type LoginViewModel struct {
|
|||||||
TotalUsers int
|
TotalUsers int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetPasswordViewModel struct {
|
||||||
|
LoginViewModel
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel {
|
func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel {
|
||||||
s.Success = m
|
s.Success = m
|
||||||
return s
|
return s
|
||||||
|
@ -51,6 +51,8 @@ type IUserRepository interface {
|
|||||||
GetById(string) (*models.User, error)
|
GetById(string) (*models.User, error)
|
||||||
GetByIds([]string) ([]*models.User, error)
|
GetByIds([]string) ([]*models.User, error)
|
||||||
GetByApiKey(string) (*models.User, error)
|
GetByApiKey(string) (*models.User, error)
|
||||||
|
GetByEmail(string) (*models.User, error)
|
||||||
|
GetByResetToken(string) (*models.User, error)
|
||||||
GetAll() ([]*models.User, error)
|
GetAll() ([]*models.User, error)
|
||||||
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
||||||
GetByLastActiveAfter(time.Time) ([]*models.User, error)
|
GetByLastActiveAfter(time.Time) ([]*models.User, error)
|
||||||
|
@ -42,6 +42,28 @@ func (r *UserRepository) GetByApiKey(key string) (*models.User, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByResetToken(resetToken string) (*models.User, error) {
|
||||||
|
if resetToken == "" {
|
||||||
|
return nil, errors.New("invalid input")
|
||||||
|
}
|
||||||
|
u := &models.User{}
|
||||||
|
if err := r.db.Where(&models.User{ResetToken: resetToken}).First(u).Error; err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
|
||||||
|
if email == "" {
|
||||||
|
return nil, errors.New("invalid input")
|
||||||
|
}
|
||||||
|
u := &models.User{}
|
||||||
|
if err := r.db.Where(&models.User{Email: email}).First(u).Error; err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *UserRepository) GetAll() ([]*models.User, error) {
|
func (r *UserRepository) GetAll() ([]*models.User, error) {
|
||||||
var users []*models.User
|
var users []*models.User
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
|
@ -20,6 +20,7 @@ type HomeHandler struct {
|
|||||||
|
|
||||||
var loginDecoder = schema.NewDecoder()
|
var loginDecoder = schema.NewDecoder()
|
||||||
var signupDecoder = schema.NewDecoder()
|
var signupDecoder = schema.NewDecoder()
|
||||||
|
var resetPasswordDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
func NewHomeHandler(keyValueService services.IKeyValueService) *HomeHandler {
|
func NewHomeHandler(keyValueService services.IKeyValueService) *HomeHandler {
|
||||||
return &HomeHandler{
|
return &HomeHandler{
|
||||||
|
125
routes/login.go
125
routes/login.go
@ -2,6 +2,7 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
@ -9,18 +10,21 @@ import (
|
|||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginHandler struct {
|
type LoginHandler struct {
|
||||||
config *conf.Config
|
config *conf.Config
|
||||||
userSrvc services.IUserService
|
userSrvc services.IUserService
|
||||||
|
mailSrvc services.IMailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLoginHandler(userService services.IUserService) *LoginHandler {
|
func NewLoginHandler(userService services.IUserService, mailService services.IMailService) *LoginHandler {
|
||||||
return &LoginHandler{
|
return &LoginHandler{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
|
mailSrvc: mailService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +34,10 @@ func (h *LoginHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
router.Path("/logout").Methods(http.MethodPost).HandlerFunc(h.PostLogout)
|
router.Path("/logout").Methods(http.MethodPost).HandlerFunc(h.PostLogout)
|
||||||
router.Path("/signup").Methods(http.MethodGet).HandlerFunc(h.GetSignup)
|
router.Path("/signup").Methods(http.MethodGet).HandlerFunc(h.GetSignup)
|
||||||
router.Path("/signup").Methods(http.MethodPost).HandlerFunc(h.PostSignup)
|
router.Path("/signup").Methods(http.MethodPost).HandlerFunc(h.PostSignup)
|
||||||
|
router.Path("/set-password").Methods(http.MethodGet).HandlerFunc(h.GetSetPassword)
|
||||||
|
router.Path("/set-password").Methods(http.MethodPost).HandlerFunc(h.PostSetPassword)
|
||||||
|
router.Path("/reset-password").Methods(http.MethodGet).HandlerFunc(h.GetResetPassword)
|
||||||
|
router.Path("/reset-password").Methods(http.MethodPost).HandlerFunc(h.PostResetPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -167,6 +175,121 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) GetResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) GetSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
values, _ := url.ParseQuery(r.URL.RawQuery)
|
||||||
|
token := values.Get("token")
|
||||||
|
if token == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid or missing token"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := &view.SetPasswordViewModel{
|
||||||
|
LoginViewModel: *h.buildViewModel(r),
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
var setRequest models.SetPasswordRequest
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := signupDecoder.Decode(&setRequest, r.PostForm); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userSrvc.GetUserByResetToken(setRequest.Token)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid token"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !setRequest.IsValid() {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = setRequest.Password
|
||||||
|
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
user.Password = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.userSrvc.Update(user); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("%s/login?success=%s", h.config.Server.BasePath, "password updated successfully"), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
var resetRequest models.ResetPasswordRequest
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := resetPasswordDecoder.Decode(&resetRequest, r.PostForm); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
|
||||||
|
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
go func(user *models.User) {
|
||||||
|
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
|
||||||
|
if err := h.mailSrvc.SendPasswordResetMail(user, link); err != nil {
|
||||||
|
logbuch.Error("failed to send password reset mail to %s", user.ID)
|
||||||
|
} else {
|
||||||
|
logbuch.Info("sent password reset mail to %s", user.ID)
|
||||||
|
}
|
||||||
|
}(u)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logbuch.Warn("password reset requested for unregistered address '%s'", resetRequest.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "an e-mail was sent to you in case your e-mail address was registered"), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
|
func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
|
||||||
numUsers, _ := h.userSrvc.Count()
|
numUsers, _ := h.userSrvc.Count()
|
||||||
|
|
||||||
|
23
services/mail/mail.go
Normal file
23
services/mail/mail.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMailService() services.IMailService {
|
||||||
|
config := conf.Get()
|
||||||
|
if config.Mail.Provider == conf.MailProviderMailWhale {
|
||||||
|
return NewMailWhaleService(config.Mail.MailWhale)
|
||||||
|
}
|
||||||
|
return &NoopMailService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoopMailService struct{}
|
||||||
|
|
||||||
|
func (n NoopMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
||||||
|
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to %s", recipient.ID)
|
||||||
|
return nil
|
||||||
|
}
|
@ -64,6 +64,8 @@ type ISummaryService interface {
|
|||||||
type IUserService interface {
|
type IUserService interface {
|
||||||
GetUserById(string) (*models.User, error)
|
GetUserById(string) (*models.User, error)
|
||||||
GetUserByKey(string) (*models.User, error)
|
GetUserByKey(string) (*models.User, error)
|
||||||
|
GetUserByEmail(string) (*models.User, error)
|
||||||
|
GetUserByResetToken(string) (*models.User, error)
|
||||||
GetAll() ([]*models.User, error)
|
GetAll() ([]*models.User, error)
|
||||||
GetActive() ([]*models.User, error)
|
GetActive() ([]*models.User, error)
|
||||||
Count() (int64, error)
|
Count() (int64, error)
|
||||||
@ -73,6 +75,7 @@ type IUserService interface {
|
|||||||
ResetApiKey(*models.User) (*models.User, error)
|
ResetApiKey(*models.User) (*models.User, error)
|
||||||
SetWakatimeApiKey(*models.User, string) (*models.User, error)
|
SetWakatimeApiKey(*models.User, string) (*models.User, error)
|
||||||
MigrateMd5Password(*models.User, *models.Login) (*models.User, error)
|
MigrateMd5Password(*models.User, *models.Login) (*models.User, error)
|
||||||
|
GenerateResetToken(*models.User) (*models.User, error)
|
||||||
FlushCache()
|
FlushCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,14 @@ func (srv *UserService) GetUserByKey(key string) (*models.User, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||||
|
return srv.repository.GetByEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *UserService) GetUserByResetToken(resetToken string) (*models.User, error) {
|
||||||
|
return srv.repository.GetByResetToken(resetToken)
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *UserService) GetAll() ([]*models.User, error) {
|
func (srv *UserService) GetAll() ([]*models.User, error) {
|
||||||
return srv.repository.GetAll()
|
return srv.repository.GetAll()
|
||||||
}
|
}
|
||||||
@ -110,6 +118,10 @@ func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Logi
|
|||||||
return srv.repository.UpdateField(user, "password", user.Password)
|
return srv.repository.UpdateField(user, "password", user.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *UserService) GenerateResetToken(user *models.User) (*models.User, error) {
|
||||||
|
return srv.repository.UpdateField(user, "reset_token", uuid.NewV4())
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *UserService) Delete(user *models.User) error {
|
func (srv *UserService) Delete(user *models.User) error {
|
||||||
srv.cache.Flush()
|
srv.cache.Flush()
|
||||||
return srv.repository.Delete(user)
|
return srv.repository.Delete(user)
|
||||||
|
@ -7,8 +7,12 @@
|
|||||||
|
|
||||||
{{ template "header.tpl.html" . }}
|
{{ template "header.tpl.html" . }}
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<h1 class="font-semibold text-xl text-white m-0 border-b-4 border-green-700">Login</h1>
|
<div class="flex items-center justify-between max-w-lg flex-grow">
|
||||||
|
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">← Go back</a></div>
|
||||||
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Login</h1></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ template "alerts.tpl.html" . }}
|
{{ template "alerts.tpl.html" . }}
|
||||||
@ -28,11 +32,16 @@
|
|||||||
type="password" id="password"
|
type="password" id="password"
|
||||||
name="password" placeholder="******" minlength="6" required>
|
name="password" placeholder="******" minlength="6" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between items-center">
|
||||||
<a href="signup">
|
<a href="reset-password" class="text-gray-500 text-sm">
|
||||||
<button type="button" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Sign up</button>
|
Forgot password?
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Log in</button>
|
<div class="flex space-x-2">
|
||||||
|
<a href="signup">
|
||||||
|
<button type="button" class="py-1 px-3 rounded border border-green-700 text-white text-sm">Sign up</button>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Log in</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
42
views/reset-password.tpl.html
Normal file
42
views/reset-password.tpl.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
{{ template "head.tpl.html" . }}
|
||||||
|
|
||||||
|
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
|
||||||
|
|
||||||
|
{{ template "header.tpl.html" . }}
|
||||||
|
|
||||||
|
<div class="w-full flex justify-center">
|
||||||
|
<div class="flex items-center justify-between max-w-lg flex-grow">
|
||||||
|
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">← Go back</a></div>
|
||||||
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Reset Password</h1></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "alerts.tpl.html" . }}
|
||||||
|
|
||||||
|
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||||
|
<div class="flex-grow max-w-lg mt-10">
|
||||||
|
<form action="reset-password" method="post">
|
||||||
|
<p class="text-sm text-white mb-8">If you forgot your password, enter the e-mail address you associated with Wakapi to reset it and choose a new one. You will receive an e-mail afterwards. Make sure to check your spam folder if it does not arrive after a few minutes.</p>
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="email">E-Mail</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="email" id="email"
|
||||||
|
name="email" placeholder="Enter your e-mail address" minlength="1" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ template "footer.tpl.html" . }}
|
||||||
|
|
||||||
|
{{ template "foot.tpl.html" . }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
44
views/set-password.tpl.html
Normal file
44
views/set-password.tpl.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
{{ template "head.tpl.html" . }}
|
||||||
|
|
||||||
|
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
|
||||||
|
|
||||||
|
{{ template "header.tpl.html" . }}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<h1 class="font-semibold text-xl text-white m-0 border-b-4 border-green-700">Choose a new password</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "alerts.tpl.html" . }}
|
||||||
|
|
||||||
|
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||||
|
<div class="flex-grow max-w-lg mt-10">
|
||||||
|
<form action="set-password" method="post">
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="password" id="password"
|
||||||
|
name="password" placeholder="Choose a password" minlength="6" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-8">
|
||||||
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password_repeat">And again ...</label>
|
||||||
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
|
type="password" id="password_repeat"
|
||||||
|
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
<input type="hidden" name="token" value="{{ .Token }}">
|
||||||
|
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ template "footer.tpl.html" . }}
|
||||||
|
|
||||||
|
{{ template "foot.tpl.html" . }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<div class="flex items-center justify-between max-w-2xl flex-grow">
|
<div class="flex items-center justify-between max-w-2xl flex-grow">
|
||||||
<div><a href="" class="text-gray-500 text-sm">← Go back</a></div>
|
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">← Go back</a></div>
|
||||||
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
|
||||||
<div> </div>
|
<div> </div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
|
|
||||||
{{ template "head.tpl.html" . }}
|
{{ template "head.tpl.html" . }}
|
||||||
|
|
||||||
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-lg mx-auto justify-center">
|
||||||
|
|
||||||
|
{{ template "header.tpl.html" . }}
|
||||||
|
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
<div class="flex items-center justify-between max-w-lg flex-grow">
|
||||||
<div><a href="" class="text-gray-500 text-sm">← Go back</a></div>
|
<div><a onclick="window.history.back()" class="text-gray-500 text-sm cursor-pointer">← Go back</a></div>
|
||||||
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Sign Up</h1></div>
|
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Sign Up</h1></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user