Compare commits
	
		
			4 Commits
		
	
	
		
			2f7300db0f
			...
			21c5f42aff
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 21c5f42aff | |||
| 6edfc529ea | |||
| d256bfb438 | |||
| a6d54242bb | 
@ -25,6 +25,12 @@ type config struct {
 | 
				
			|||||||
		secret string
 | 
							secret string
 | 
				
			||||||
		key    string
 | 
							key    string
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						smtp struct {
 | 
				
			||||||
 | 
							host     string
 | 
				
			||||||
 | 
							port     int
 | 
				
			||||||
 | 
							username string
 | 
				
			||||||
 | 
							password string
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type application struct {
 | 
					type application struct {
 | 
				
			||||||
@ -69,6 +75,10 @@ func main() {
 | 
				
			|||||||
		"root:example@tcp(localhost:3306)/widgets?parseTime=true&tls=false",
 | 
							"root:example@tcp(localhost:3306)/widgets?parseTime=true&tls=false",
 | 
				
			||||||
		"Application environment {development|production}",
 | 
							"Application environment {development|production}",
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						flag.StringVar(&cfg.smtp.host, "smtphost", "0.0.0.0", "smtp host")
 | 
				
			||||||
 | 
						flag.IntVar(&cfg.smtp.port, "smtpport", 1025, "smtp host")
 | 
				
			||||||
 | 
						flag.StringVar(&cfg.smtp.username, "smtpuser", "user", "smtp user")
 | 
				
			||||||
 | 
						flag.StringVar(&cfg.smtp.password, "smtppwd", "password", "smtp password")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.Parse()
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -405,3 +405,23 @@ func (app *application) VirtualTerminalPaymentSucceeded(w http.ResponseWriter, r
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	app.writeJSON(w, http.StatusOK, txn)
 | 
						app.writeJSON(w, http.StatusOK, txn)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) SendPasswordResetEmail(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						var payload struct {
 | 
				
			||||||
 | 
							Email string `json:"email"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := app.readJSON(w, r, &payload)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.badRequest(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var data struct {
 | 
				
			||||||
 | 
							Link string
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data.Link = "http://www.vinchent.xyz"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// send mail
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										78
									
								
								cmd/api/mailer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								cmd/api/mailer.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"embed"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"text/template"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mail "github.com/xhit/go-simple-mail/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//go:embed templates
 | 
				
			||||||
 | 
					var emailTemplateFS embed.FS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) SendMail(from, to, subject, tmpl string, data interface{}) error {
 | 
				
			||||||
 | 
						templateToRender := fmt.Sprintf("templates/%s.html.tmpl", tmpl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t, err := template.New("email-html").ParseFS(emailTemplateFS, templateToRender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var tpl bytes.Buffer
 | 
				
			||||||
 | 
						if err = t.ExecuteTemplate(&tpl, "body", data); err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						formattedMessage := tpl.String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						templateToRender = fmt.Sprintf("templates/%s.plain.tmpl", tmpl)
 | 
				
			||||||
 | 
						t, err = template.New("email-plain").ParseFS(emailTemplateFS, templateToRender)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err = t.ExecuteTemplate(&tpl, "body", data); err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						plainMessage := tpl.String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.infoLog.Println(formattedMessage, plainMessage)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// send the mail
 | 
				
			||||||
 | 
						server := mail.NewSMTPClient()
 | 
				
			||||||
 | 
						server.Host = app.config.smtp.host
 | 
				
			||||||
 | 
						server.Port = app.config.smtp.port
 | 
				
			||||||
 | 
						// NOTE: not needed for MailHog
 | 
				
			||||||
 | 
						// server.Username = app.config.smtp.username
 | 
				
			||||||
 | 
						// server.Password = app.config.smtp.password
 | 
				
			||||||
 | 
						// server.Encryption = mail.EncryptionTLS
 | 
				
			||||||
 | 
						server.KeepAlive = false
 | 
				
			||||||
 | 
						server.ConnectTimeout = 10 * time.Second
 | 
				
			||||||
 | 
						server.SendTimeout = 10 * time.Second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						smtpClient, err := server.Connect()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						email := mail.NewMSG()
 | 
				
			||||||
 | 
						email.SetFrom(from).AddTo(to).SetSubject(subject)
 | 
				
			||||||
 | 
						email.SetBody(mail.TextHTML, formattedMessage)
 | 
				
			||||||
 | 
						email.AddAlternative(mail.TextPlain, plainMessage)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = email.Send(smtpClient)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.infoLog.Println("send mail")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -33,6 +33,7 @@ func (app *application) routes() http.Handler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		mux.Post("/virtual-terminal-succeeded", app.VirtualTerminalPaymentSucceeded)
 | 
							mux.Post("/virtual-terminal-succeeded", app.VirtualTerminalPaymentSucceeded)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						mux.Post("/api/forgot-password", app.SendPasswordResetEmail)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return mux
 | 
						return mux
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								cmd/api/templates/password-reset.html.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cmd/api/templates/password-reset.html.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								cmd/api/templates/password-reset.plain.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cmd/api/templates/password-reset.plain.gohtml
									
									
									
									
									
										Normal file
									
								
							@ -289,3 +289,38 @@ func (app *application) LoginPage(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		app.errorLog.Println(err)
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) PostLoginPage(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						app.Session.RenewToken(r.Context())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := r.ParseForm()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						email := r.Form.Get("email")
 | 
				
			||||||
 | 
						password := r.Form.Get("password")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						id, err := app.DB.Authenticate(email, password)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Redirect(w, r, "/login", http.StatusSeeOther)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.Session.Put(r.Context(), "userID", id)
 | 
				
			||||||
 | 
						http.Redirect(w, r, "/", http.StatusSeeOther)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) Logout(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						app.Session.Destroy(r.Context())
 | 
				
			||||||
 | 
						app.Session.RenewToken(r.Context())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						http.Redirect(w, r, "/login", http.StatusSeeOther)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) ForgotPassword(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						if err := app.renderTemplate(w, r, "forgot-password", &templateData{}); err != nil {
 | 
				
			||||||
 | 
							app.errorLog.Println(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/alexedwards/scs/mysqlstore"
 | 
				
			||||||
	"github.com/alexedwards/scs/v2"
 | 
						"github.com/alexedwards/scs/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -91,16 +92,17 @@ func main() {
 | 
				
			|||||||
	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
 | 
						infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
 | 
				
			||||||
	errorLog := log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
 | 
						errorLog := log.New(os.Stdout, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set up session
 | 
					 | 
				
			||||||
	session = scs.New()
 | 
					 | 
				
			||||||
	session.Lifetime = 24 * time.Hour
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	conn, err := driver.OpenDB(cfg.db.dsn)
 | 
						conn, err := driver.OpenDB(cfg.db.dsn)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		errorLog.Fatal(err)
 | 
							errorLog.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer conn.Close()
 | 
						defer conn.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// set up session
 | 
				
			||||||
 | 
						session = scs.New()
 | 
				
			||||||
 | 
						session.Lifetime = 24 * time.Hour
 | 
				
			||||||
 | 
						session.Store = mysqlstore.New(conn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tc := make(map[string]*template.Template)
 | 
						tc := make(map[string]*template.Template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app := &application{
 | 
						app := &application{
 | 
				
			||||||
 | 
				
			|||||||
@ -7,3 +7,12 @@ import (
 | 
				
			|||||||
func SessionLoad(next http.Handler) http.Handler {
 | 
					func SessionLoad(next http.Handler) http.Handler {
 | 
				
			||||||
	return session.LoadAndSave(next)
 | 
						return session.LoadAndSave(next)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) Auth(next http.Handler) http.Handler {
 | 
				
			||||||
 | 
						return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							if !app.Session.Exists(r.Context(), "userID") {
 | 
				
			||||||
 | 
								http.Redirect(w, r, "/login", http.StatusSeeOther)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							next.ServeHTTP(w, r)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -11,9 +11,14 @@ func (app *application) routes() http.Handler {
 | 
				
			|||||||
	mux.Use(SessionLoad)
 | 
						mux.Use(SessionLoad)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.Get("/", app.Home)
 | 
						mux.Get("/", app.Home)
 | 
				
			||||||
	mux.Get("/virtual-terminal", app.VirtualTerminal)
 | 
					
 | 
				
			||||||
	mux.Post("/virtual-terminal-payment-succeeded", app.VirtualTerminalPaymentSucceeded)
 | 
						mux.Route("/admin", func(mux chi.Router) {
 | 
				
			||||||
	mux.Get("/virtual-terminal-receipt", app.VirtualTerminalReceipt)
 | 
							mux.Use(app.Auth)
 | 
				
			||||||
 | 
							mux.Get("/virtual-terminal", app.VirtualTerminal)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// mux.Post("/virtual-terminal-payment-succeeded", app.VirtualTerminalPaymentSucceeded)
 | 
				
			||||||
 | 
						// mux.Get("/virtual-terminal-receipt", app.VirtualTerminalReceipt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mux.Get("/widget/{id}", app.ChargeOnce)
 | 
						mux.Get("/widget/{id}", app.ChargeOnce)
 | 
				
			||||||
	mux.Get("/receipt", app.Receipt)
 | 
						mux.Get("/receipt", app.Receipt)
 | 
				
			||||||
@ -24,6 +29,9 @@ func (app *application) routes() http.Handler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// auth routes
 | 
						// auth routes
 | 
				
			||||||
	mux.Get("/login", app.LoginPage)
 | 
						mux.Get("/login", app.LoginPage)
 | 
				
			||||||
 | 
						mux.Post("/login", app.PostLoginPage)
 | 
				
			||||||
 | 
						mux.Get("/logout", app.Logout)
 | 
				
			||||||
 | 
						mux.Get("/forgot-password", app.ForgotPassword)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fileServer := http.FileServer(http.Dir("./static"))
 | 
						fileServer := http.FileServer(http.Dir("./static"))
 | 
				
			||||||
	mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
 | 
						mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
 | 
				
			||||||
 | 
				
			|||||||
@ -48,7 +48,7 @@
 | 
				
			|||||||
                            </ul>
 | 
					                            </ul>
 | 
				
			||||||
                        </li>
 | 
					                        </li>
 | 
				
			||||||
                        <li id="vt-link" class="nav-item d-none">
 | 
					                        <li id="vt-link" class="nav-item d-none">
 | 
				
			||||||
                            <a class="nav-link" href="/virtual-terminal">Virtual Terminal</a>
 | 
					                            <a class="nav-link" href="/admin/virtual-terminal">Virtual Terminal</a>
 | 
				
			||||||
                        </li>
 | 
					                        </li>
 | 
				
			||||||
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
 | 
					                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
 | 
				
			||||||
                            <li id="login-link" class="nav-item d-none">
 | 
					                            <li id="login-link" class="nav-item d-none">
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								cmd/web/templates/forgot-password.page.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								cmd/web/templates/forgot-password.page.gohtml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					{{template "base" .}}
 | 
				
			||||||
 | 
					{{define "title"}}
 | 
				
			||||||
 | 
					Forgot Password
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "content"}}
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
					    <div class="col-md-6 offset-md-3">
 | 
				
			||||||
 | 
					        <div class="alert alert-danger text-center d-none" id="forgot-messages"></div>
 | 
				
			||||||
 | 
					        <form action=""
 | 
				
			||||||
 | 
					              method="post"
 | 
				
			||||||
 | 
					              name="forgot-form"
 | 
				
			||||||
 | 
					              id="forgot-form"
 | 
				
			||||||
 | 
					              class="d-blick needs-validation"
 | 
				
			||||||
 | 
					              autocomplete="off"
 | 
				
			||||||
 | 
					              novalidate="">
 | 
				
			||||||
 | 
					            <h2 class="mt-2 mb-3 text-center">Forgot Password</h2>
 | 
				
			||||||
 | 
					            <hr>
 | 
				
			||||||
 | 
					            <div class="mb-3">
 | 
				
			||||||
 | 
					                <label for="-email" class="form-label">Email</label>
 | 
				
			||||||
 | 
					                <input type="text"
 | 
				
			||||||
 | 
					                       id="email"
 | 
				
			||||||
 | 
					                       name="email"
 | 
				
			||||||
 | 
					                       autocomplete="email-new"
 | 
				
			||||||
 | 
					                       required=""
 | 
				
			||||||
 | 
					                       class="form-control">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <hr>
 | 
				
			||||||
 | 
					            <a href="javascript:void(0)" id="reset-btn" class="btn btn-primary">Reset Password</a>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					{{define "js"}}
 | 
				
			||||||
 | 
					<script type="module">
 | 
				
			||||||
 | 
					    import {forgot} from "/static/js/login.js"
 | 
				
			||||||
 | 
					    document.getElementById("reset-btn").addEventListener("click", () => {
 | 
				
			||||||
 | 
					        forgot({{.API}});
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
@ -6,7 +6,7 @@ Login
 | 
				
			|||||||
<div class="row">
 | 
					<div class="row">
 | 
				
			||||||
    <div class="col-md-6 offset-md-3">
 | 
					    <div class="col-md-6 offset-md-3">
 | 
				
			||||||
        <div class="alert alert-danger text-center d-none" id="login-messages"></div>
 | 
					        <div class="alert alert-danger text-center d-none" id="login-messages"></div>
 | 
				
			||||||
        <form action=""
 | 
					        <form action="/login"
 | 
				
			||||||
              method="post"
 | 
					              method="post"
 | 
				
			||||||
              name="login-form"
 | 
					              name="login-form"
 | 
				
			||||||
              id="login-form"
 | 
					              id="login-form"
 | 
				
			||||||
@ -35,6 +35,9 @@ Login
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <hr>
 | 
					            <hr>
 | 
				
			||||||
            <a href="javascript:void(0)" id="login-btn" class="btn btn-primary">Login</a>
 | 
					            <a href="javascript:void(0)" id="login-btn" class="btn btn-primary">Login</a>
 | 
				
			||||||
 | 
					            <p class="mt-2">
 | 
				
			||||||
 | 
					                <small><a href="/forgot-password">Forgot password</a></small>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
        </form>
 | 
					        </form>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -80,7 +80,7 @@ Virtual Terminal
 | 
				
			|||||||
            <strong>Bank Return Code</strong>: <span id="bank-return-code"></span>
 | 
					            <strong>Bank Return Code</strong>: <span id="bank-return-code"></span>
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
        <p>
 | 
					        <p>
 | 
				
			||||||
            <a href="/virtual-terminal" class="btn btn-primary">
 | 
					            <a href="/admin/virtual-terminal" class="btn btn-primary">
 | 
				
			||||||
                Charge another card
 | 
					                Charge another card
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							@ -3,12 +3,18 @@ module myapp
 | 
				
			|||||||
go 1.22.5
 | 
					go 1.22.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
 | 
						github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885
 | 
				
			||||||
	github.com/alexedwards/scs/v2 v2.8.0
 | 
						github.com/alexedwards/scs/v2 v2.8.0
 | 
				
			||||||
	github.com/go-chi/chi/v5 v5.1.0
 | 
						github.com/go-chi/chi/v5 v5.1.0
 | 
				
			||||||
	github.com/go-chi/cors v1.2.1
 | 
						github.com/go-chi/cors v1.2.1
 | 
				
			||||||
	github.com/go-sql-driver/mysql v1.8.1
 | 
						github.com/go-sql-driver/mysql v1.8.1
 | 
				
			||||||
	github.com/stripe/stripe-go/v79 v79.6.0
 | 
						github.com/stripe/stripe-go/v79 v79.6.0
 | 
				
			||||||
 | 
						github.com/xhit/go-simple-mail/v2 v2.16.0
 | 
				
			||||||
	golang.org/x/crypto v0.26.0
 | 
						golang.org/x/crypto v0.26.0
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require filippo.io/edwards25519 v1.1.0 // indirect
 | 
					require (
 | 
				
			||||||
 | 
						filippo.io/edwards25519 v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/go-test/deep v1.1.1 // indirect
 | 
				
			||||||
 | 
						github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								go.sum
									
									
									
									
									
								
							@ -1,5 +1,7 @@
 | 
				
			|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 | 
					filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 | 
				
			||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 | 
					filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 | 
				
			||||||
 | 
					github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885 h1:C7QAamNjR5yz6di4KJWAKcnxueKBgq4L/JGXhlnu35w=
 | 
				
			||||||
 | 
					github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885/go.mod h1:p8jK3D80sw1PFrCSdlcJF1O75bp55HqbgDyyCLM0FrE=
 | 
				
			||||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
 | 
					github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
 | 
				
			||||||
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
 | 
					github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 | 
					github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 | 
				
			||||||
@ -8,8 +10,11 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
 | 
				
			|||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 | 
					github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 | 
				
			||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 | 
					github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 | 
				
			||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 | 
					github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 | 
				
			||||||
 | 
					github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
				
			||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 | 
					github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 | 
				
			||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 | 
					github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 | 
				
			||||||
 | 
					github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 | 
				
			||||||
 | 
					github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
@ -17,6 +22,10 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 | 
				
			|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
					github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
github.com/stripe/stripe-go/v79 v79.6.0 h1:qSBV2f2rpLEEZTdTlVLzdmQJZNmfoo2E3hUEkFT8GBc=
 | 
					github.com/stripe/stripe-go/v79 v79.6.0 h1:qSBV2f2rpLEEZTdTlVLzdmQJZNmfoo2E3hUEkFT8GBc=
 | 
				
			||||||
github.com/stripe/stripe-go/v79 v79.6.0/go.mod h1:cuH6X0zC8peY6f1AubHwgJ/fJSn2dh5pfiCr6CjyKVU=
 | 
					github.com/stripe/stripe-go/v79 v79.6.0/go.mod h1:cuH6X0zC8peY6f1AubHwgJ/fJSn2dh5pfiCr6CjyKVU=
 | 
				
			||||||
 | 
					github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
 | 
				
			||||||
 | 
					github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
 | 
				
			||||||
 | 
					github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
 | 
				
			||||||
 | 
					github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
 | 
				
			||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 | 
					golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 | 
				
			||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 | 
					golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 | 
				
			||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
					golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,11 @@ package models
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DBModel is the type for database connection values
 | 
					// DBModel is the type for database connection values
 | 
				
			||||||
@ -254,3 +257,26 @@ func (m *DBModel) GetUserByEmail(email string) (User, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return u, nil
 | 
						return u, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *DBModel) Authenticate(email, password string) (int, error) {
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var id int
 | 
				
			||||||
 | 
						var hashedPassword string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						row := m.DB.QueryRowContext(ctx, "SELECT id, password from users WHERE email = ?", email)
 | 
				
			||||||
 | 
						err := row.Scan(&id, &hashedPassword)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
 | 
				
			||||||
 | 
						if err == bcrypt.ErrMismatchedHashAndPassword {
 | 
				
			||||||
 | 
							return 0, errors.New("incorrect password")
 | 
				
			||||||
 | 
						} else if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return id, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					DROP TABLE sessions;
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					CREATE TABLE sessions (
 | 
				
			||||||
 | 
						token CHAR(43) PRIMARY KEY,
 | 
				
			||||||
 | 
						data BLOB NOT NULL,
 | 
				
			||||||
 | 
						expiry TIMESTAMP(6) NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX sessions_expiry_idx ON sessions (expiry);
 | 
				
			||||||
@ -14,6 +14,6 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
				
			|||||||
function logout() {
 | 
					function logout() {
 | 
				
			||||||
    localStorage.removeItem("token");
 | 
					    localStorage.removeItem("token");
 | 
				
			||||||
    localStorage.removeItem("token_expiry");
 | 
					    localStorage.removeItem("token_expiry");
 | 
				
			||||||
    location.href = "/login";
 | 
					    location.href = "/logout";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -36,7 +36,8 @@ export function val(api) {
 | 
				
			|||||||
                localStorage.setItem("token", response.authentication_token.token);
 | 
					                localStorage.setItem("token", response.authentication_token.token);
 | 
				
			||||||
                localStorage.setItem("token_expiry", response.authentication_token.expiry);
 | 
					                localStorage.setItem("token_expiry", response.authentication_token.expiry);
 | 
				
			||||||
                showSuccess("login-messages", "Login successful.")
 | 
					                showSuccess("login-messages", "Login successful.")
 | 
				
			||||||
                location.href = "/";
 | 
					                // location.href = "/";
 | 
				
			||||||
 | 
					                document.getElementById("login-form").submit()
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                showError("login-messages", response.message)
 | 
					                showError("login-messages", response.message)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -44,3 +45,38 @@ export function val(api) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function forgot(api) {
 | 
				
			||||||
 | 
					    let form = document.getElementById("forgot-form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (form.checkValidity() === false) {
 | 
				
			||||||
 | 
					        // this.event.preventDefault();
 | 
				
			||||||
 | 
					        // this.event.stopPropagation();
 | 
				
			||||||
 | 
					        form.classList.add("was-validated");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    form.classList.add("was-validated");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let payload = {
 | 
				
			||||||
 | 
					        email: document.getElementById("email").value,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestOptions = {
 | 
				
			||||||
 | 
					        method: 'post',
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					            'Accept': 'application/json',
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        body: JSON.stringify(payload),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch(api + "/api/forgot-password", requestOptions)
 | 
				
			||||||
 | 
					        .then(response => response.json())
 | 
				
			||||||
 | 
					        .then(response => {
 | 
				
			||||||
 | 
					            console.log(response)
 | 
				
			||||||
 | 
					            if (response.error === false) {
 | 
				
			||||||
 | 
					                showSuccess("forgot-messages", "Password reset email sent")
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                showError("forgot-messages", response.message)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user