Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1639dd787f | |||
| 9c299bc709 | |||
| 166049f4ff | |||
| 4f815151eb |
@ -1,13 +0,0 @@
|
|||||||
# Cryptographic key for generating secure CSRF protection tokens
|
|
||||||
LENSLOCKED_CSRF_KEY=
|
|
||||||
|
|
||||||
# Postgresql DB c nnection settings
|
|
||||||
# Make sure to enable ssl when deploying to production
|
|
||||||
LENSLOCKED_DB_STRING="host=localhost port=5432 user=changeme dbname=lenslocked sslmode=disable"
|
|
||||||
|
|
||||||
# SMTP Email settings
|
|
||||||
LENSLOCKED_EMAIL_HOST=
|
|
||||||
LENSLOCKED_EMAIL_PORT=
|
|
||||||
LENSLOCKED_EMAIL_USERNAME=
|
|
||||||
LENSLOCKED_EMAIL_PASSWORD=
|
|
||||||
LENSLOCKED_EMAIL_FROM=
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
tmp
|
tmp
|
||||||
*.env
|
|
||||||
|
|
||||||
|
|||||||
41
cmd/bcrypt/bcrypt.go
Normal file
41
cmd/bcrypt/bcrypt.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "hash":
|
||||||
|
hash(os.Args[2])
|
||||||
|
case "compare":
|
||||||
|
compare(os.Args[2], os.Args[3])
|
||||||
|
default:
|
||||||
|
fmt.Printf("Invalid command: %v\n", os.Args[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(password string) {
|
||||||
|
fmt.Printf("Hash the password %q\n", password)
|
||||||
|
hashedBytes, err := bcrypt.GenerateFromPassword(
|
||||||
|
[]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error hashing: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash := string(hashedBytes)
|
||||||
|
fmt.Println(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compare(password, hash string) {
|
||||||
|
fmt.Printf("Compare the password %q with the hash %q\n", password, hash)
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Password is invalid: %v\n", password)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Password is correct!")
|
||||||
|
}
|
||||||
144
cmd/exp/exp.go
Normal file
144
cmd/exp/exp.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddUser(db *sql.DB, name string, email string) (error, int) {
|
||||||
|
row := db.QueryRow(`
|
||||||
|
INSERT INTO users(name, email)
|
||||||
|
VALUES($1, $2)
|
||||||
|
RETURNING id;
|
||||||
|
`, name, email)
|
||||||
|
var id int
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return err, id
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryUser(db *sql.DB, id int) (error, string, string) {
|
||||||
|
row := db.QueryRow(`
|
||||||
|
SELECT name, email
|
||||||
|
FROM users
|
||||||
|
WHERE id=$1;`, id)
|
||||||
|
var name, email string
|
||||||
|
err := row.Scan(&name, &email)
|
||||||
|
return err, name, email
|
||||||
|
}
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
ID int
|
||||||
|
UID int
|
||||||
|
Amount int
|
||||||
|
Desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddOrder(db *sql.DB, uid int, amount int, desc string) (error, int) {
|
||||||
|
row := db.QueryRow(`
|
||||||
|
insert into orders(user_id, amount, description)
|
||||||
|
values($1, $2, $3)
|
||||||
|
returning id;
|
||||||
|
`, uid, amount, desc)
|
||||||
|
var id int
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return err, id
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryOrders(db *sql.DB, uid int) ([]Order, error) {
|
||||||
|
var orders []Order
|
||||||
|
rows, err := db.Query(`
|
||||||
|
select id, amount, description from orders where user_id=$1
|
||||||
|
`, uid)
|
||||||
|
if err != nil {
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var order Order
|
||||||
|
order.UID = uid
|
||||||
|
err := rows.Scan(&order.ID, &order.Amount, &order.Desc)
|
||||||
|
if err != nil {
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
orders = append(orders, order)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
return orders, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := sql.Open("pgx", "host=localhost port=5432 user=baloo dbname=lenslocked sslmode=disable")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Connected!")
|
||||||
|
_, err = db.Exec(`
|
||||||
|
create table if not exists users (
|
||||||
|
id serial primary key,
|
||||||
|
name text,
|
||||||
|
email text not null
|
||||||
|
);
|
||||||
|
create table if not exists orders (
|
||||||
|
id serial primary key,
|
||||||
|
user_id int not null,
|
||||||
|
amount int,
|
||||||
|
description text
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Tables created")
|
||||||
|
|
||||||
|
var name string
|
||||||
|
var email string
|
||||||
|
|
||||||
|
// Create User
|
||||||
|
/*
|
||||||
|
name = "Jon Rob Baker"
|
||||||
|
email = "demo@user.com"
|
||||||
|
err, id := AddUser(db, name, email)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("User ", id, " created")
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Query User
|
||||||
|
err, name, email = QueryUser(db, 5)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
fmt.Println("Error, no rows!")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("User information: name=%s, email=%s\n", name, email)
|
||||||
|
|
||||||
|
uid := 5
|
||||||
|
|
||||||
|
// Create fake orders
|
||||||
|
/*
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
amount := i * 100
|
||||||
|
desc := fmt.Sprintf("Fake order #%d", i)
|
||||||
|
err, _ := AddOrder(db, uid, amount, desc)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Created fake orders")
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Query Orders
|
||||||
|
orders, err := QueryOrders(db, uid)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Orders: ", orders)
|
||||||
|
}
|
||||||
26
cmd/tplfn/tplfn.go
Normal file
26
cmd/tplfn/tplfn.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tplstr := "hello {{myFunc}}\n"
|
||||||
|
|
||||||
|
fmap := template.FuncMap{}
|
||||||
|
fmap["myFunc"] = func() (string, error) {
|
||||||
|
return "world", fmt.Errorf("hi")
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl, err := template.New("demo").Funcs(fmap).Parse(tplstr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Error creating template: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tpl.Execute(os.Stdout, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,71 +0,0 @@
|
|||||||
package userctx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.kealoha.me/lks/lenslocked/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type key string
|
|
||||||
|
|
||||||
const userKey key = "User"
|
|
||||||
|
|
||||||
func WithUser(ctx context.Context, user *models.User) context.Context {
|
|
||||||
return context.WithValue(ctx, userKey, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func User(ctx context.Context) *models.User {
|
|
||||||
val := ctx.Value(userKey)
|
|
||||||
user, ok := val.(*models.User)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserMiddleware struct {
|
|
||||||
SS *models.SessionService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (umw UserMiddleware) SetUser(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
seshCookie, err := r.Cookie("session")
|
|
||||||
if err != nil {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := umw.SS.User(seshCookie.Value)
|
|
||||||
if err != nil {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = WithUser(ctx, user)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (umw UserMiddleware) RequireUserfn(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := User(r.Context())
|
|
||||||
if user == nil {
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (umw UserMiddleware) RequireUser(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := User(r.Context())
|
|
||||||
if user == nil {
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -3,10 +3,7 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
userctx "git.kealoha.me/lks/lenslocked/context"
|
|
||||||
"git.kealoha.me/lks/lenslocked/models"
|
"git.kealoha.me/lks/lenslocked/models"
|
||||||
"git.kealoha.me/lks/lenslocked/templates"
|
"git.kealoha.me/lks/lenslocked/templates"
|
||||||
"git.kealoha.me/lks/lenslocked/views"
|
"git.kealoha.me/lks/lenslocked/views"
|
||||||
@ -14,16 +11,10 @@ import (
|
|||||||
|
|
||||||
type Users struct {
|
type Users struct {
|
||||||
Templates struct {
|
Templates struct {
|
||||||
Signup Template
|
Signup Template
|
||||||
Signin Template
|
Signin Template
|
||||||
ForgotPass Template
|
|
||||||
ResetUrlSent Template
|
|
||||||
ResetPass Template
|
|
||||||
}
|
}
|
||||||
UserService *models.UserService
|
UserService *models.UserService
|
||||||
SessionService *models.SessionService
|
|
||||||
PassResetService *models.PasswordResetService
|
|
||||||
EmailService *models.EmailService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Users) GetSignup(w http.ResponseWriter, r *http.Request) {
|
func (u Users) GetSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -43,22 +34,7 @@ func (u Users) PostSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session, err := u.SessionService.Create(user.ID)
|
fmt.Fprintf(w, "User created: %+v", user)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
Value: session.Token,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &cookie)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/user", http.StatusFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Users) GetSignin(w http.ResponseWriter, r *http.Request) {
|
func (u Users) GetSignin(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -82,161 +58,38 @@ func (u Users) PostSignin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := u.SessionService.Create(user.ID)
|
// Bad cookie
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := http.Cookie{
|
cookie := http.Cookie{
|
||||||
Name: "session",
|
Name: "bad",
|
||||||
Value: session.Token,
|
Value: user.Email,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
}
|
}
|
||||||
http.SetCookie(w, &cookie)
|
http.SetCookie(w, &cookie)
|
||||||
|
|
||||||
fmt.Fprintf(w, "Current user: %s\n", user.Email)
|
fmt.Fprintf(w, "User authenticated: %+v", user)
|
||||||
//http.Redirect(w, r, "/user", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Users) GetSignout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sessionCookie, err := r.Cookie("session")
|
|
||||||
if err != nil {
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = u.SessionService.Delete(sessionCookie.Value)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
MaxAge: -1,
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &c)
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Users) GetForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var data struct {
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
data.Email = r.FormValue("email")
|
|
||||||
u.Templates.ForgotPass.Execute(w, r, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Users) PostForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var data struct {
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
data.Email = r.FormValue("email")
|
|
||||||
pwReset, err := u.PassResetService.Create(data.Email)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vals := url.Values{
|
|
||||||
"token": {pwReset.Token},
|
|
||||||
}
|
|
||||||
// TODO: Make the URL here configurable and use https
|
|
||||||
resetURL := "http://" + r.Host + "/reset-pw?" + vals.Encode()
|
|
||||||
fmt.Println(resetURL)
|
|
||||||
err = u.EmailService.SendPasswordReset(data.Email, resetURL)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
u.Templates.ResetUrlSent.Execute(w, r, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Users) GetResetPass(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var data struct {
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
data.Token = r.FormValue("token")
|
|
||||||
u.Templates.ResetPass.Execute(w, r, data)
|
|
||||||
}
|
|
||||||
func (u Users) PostResetPass(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var data struct {
|
|
||||||
Token, Password string
|
|
||||||
}
|
|
||||||
data.Token = r.FormValue("token")
|
|
||||||
data.Password = r.FormValue("password")
|
|
||||||
|
|
||||||
user, err := u.PassResetService.Consume(data.Token)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
// TODO: Distinguish between server errors and invalid token errors.
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = u.UserService.UpdatePassword(user.ID, data.Password)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the user in now that they have reset their password.
|
|
||||||
// Any errors from this point onward should redirect to the sign in page.
|
|
||||||
session, err := u.SessionService.Create(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//setCookie(w, CookieSession, session.Token)
|
|
||||||
cookie := http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
Value: session.Token,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &cookie)
|
|
||||||
http.Redirect(w, r, "/users/me", http.StatusFound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u Users) CurrentUser(w http.ResponseWriter, r *http.Request) {
|
func (u Users) CurrentUser(w http.ResponseWriter, r *http.Request) {
|
||||||
user := userctx.User(r.Context())
|
email, err := r.Cookie("bad")
|
||||||
if user == nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
fmt.Fprint(w, "The bad cookie could not be read.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "Current user: %s\n", user.Email)
|
fmt.Fprintf(w, "Bad cookie: %s\n", email.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTemplates(user_service *models.UserService, session_service *models.SessionService, email_service *models.EmailService, signup, signin, forgotPass, resetUrlSent, resetPass Template) Users {
|
func WithTemplates(user_service *models.UserService, signup Template, signin Template) Users {
|
||||||
u := Users{}
|
u := Users{}
|
||||||
|
|
||||||
u.Templates.Signup = signup
|
u.Templates.Signup = signup
|
||||||
u.Templates.Signin = signin
|
u.Templates.Signin = signin
|
||||||
u.Templates.ForgotPass = forgotPass
|
|
||||||
u.Templates.ResetUrlSent = resetUrlSent
|
|
||||||
u.Templates.ResetPass = resetPass
|
|
||||||
|
|
||||||
u.UserService = user_service
|
u.UserService = user_service
|
||||||
u.SessionService = session_service
|
|
||||||
u.EmailService = email_service
|
|
||||||
u.PassResetService = &models.PasswordResetService{
|
|
||||||
DB: u.UserService.DB,
|
|
||||||
Duration: time.Hour / 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func Default(user_service *models.UserService, session_service *models.SessionService, email_service *models.EmailService) Users {
|
func Default(user_service *models.UserService, templatePath ...string) Users {
|
||||||
signup_tpl := views.Must(views.FromFS(templates.FS, "signup.gohtml", "tailwind.gohtml"))
|
signup_tpl := views.Must(views.FromFS(templates.FS, "signup.gohtml", "tailwind.gohtml"))
|
||||||
signin_tpl := views.Must(views.FromFS(templates.FS, "signin.gohtml", "tailwind.gohtml"))
|
signin_tpl := views.Must(views.FromFS(templates.FS, "signin.gohtml", "tailwind.gohtml"))
|
||||||
pwReset_tpl := views.Must(views.FromFS(templates.FS, "pwReset.gohtml", "tailwind.gohtml"))
|
|
||||||
pwResetSent_tpl := views.Must(views.FromFS(templates.FS, "pwResetSent.gohtml", "tailwind.gohtml"))
|
|
||||||
resetPass_tpl := views.Must(views.FromFS(templates.FS, "pwChange.gohtml", "tailwind.gohtml"))
|
|
||||||
|
|
||||||
err := signup_tpl.TestTemplate(nil)
|
err := signup_tpl.TestTemplate(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -246,14 +99,6 @@ func Default(user_service *models.UserService, session_service *models.SessionSe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = pwReset_tpl.TestTemplate(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = pwResetSent_tpl.TestTemplate(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return WithTemplates(user_service, session_service, email_service, signup_tpl, signin_tpl, pwReset_tpl, pwResetSent_tpl, resetPass_tpl)
|
return WithTemplates(user_service, signup_tpl, signin_tpl)
|
||||||
}
|
}
|
||||||
|
|||||||
10
go.mod
10
go.mod
@ -4,14 +4,12 @@ go 1.22.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/gorilla/csrf v1.7.2
|
|
||||||
github.com/jackc/pgx/v4 v4.18.3
|
github.com/jackc/pgx/v4 v4.18.3
|
||||||
github.com/pressly/goose/v3 v3.21.1
|
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-mail/mail/v2 v2.3.0 // indirect
|
github.com/gorilla/csrf v1.7.2
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgconn v1.14.3 // indirect
|
github.com/jackc/pgconn v1.14.3 // indirect
|
||||||
@ -20,11 +18,5 @@ require (
|
|||||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgtype v1.14.0 // indirect
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
|
||||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
54
go.sum
54
go.sum
@ -9,28 +9,20 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
github.com/go-mail/mail/v2 v2.3.0 h1:wha99yf2v3cpUzD1V9ujP404Jbw2uEvs+rBJybkdYcw=
|
|
||||||
github.com/go-mail/mail/v2 v2.3.0/go.mod h1:oE2UK8qebZAjjV1ZYUpY7FPnbi/kIU53l1dmqPRb4go=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
|
||||||
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
@ -78,8 +70,6 @@ github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx
|
|||||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@ -97,32 +87,18 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
|
|||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
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/pressly/goose/v3 v3.21.1 h1:5SSAKKWej8LVVzNLuT6KIvP1eFDuPvxa+B6H0w78buQ=
|
|
||||||
github.com/pressly/goose/v3 v3.21.1/go.mod h1:sqthmzV8PitchEkjecFJII//l43dLOCzfWh8pHEe+vE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
|
||||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
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=
|
||||||
@ -133,8 +109,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
@ -143,8 +119,6 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
|||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
@ -169,8 +143,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -183,8 +155,6 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
|
||||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -207,8 +177,6 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
@ -218,17 +186,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
|
||||||
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
|
||||||
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
|
||||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
|
||||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
|
||||||
modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
|
|
||||||
modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
|
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
||||||
|
|||||||
111
main.go
111
main.go
@ -3,19 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
userctx "git.kealoha.me/lks/lenslocked/context"
|
|
||||||
ctrlrs "git.kealoha.me/lks/lenslocked/controllers"
|
ctrlrs "git.kealoha.me/lks/lenslocked/controllers"
|
||||||
"git.kealoha.me/lks/lenslocked/migrations"
|
|
||||||
"git.kealoha.me/lks/lenslocked/models"
|
"git.kealoha.me/lks/lenslocked/models"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
_ "github.com/jackc/pgx/v4/stdlib"
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
@ -23,68 +17,14 @@ import (
|
|||||||
|
|
||||||
const DEBUG bool = true
|
const DEBUG bool = true
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Postgres string
|
|
||||||
Email struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
Username, Pass, Sender string
|
|
||||||
}
|
|
||||||
Csrf struct {
|
|
||||||
Key []byte
|
|
||||||
Secure bool
|
|
||||||
}
|
|
||||||
Server struct {
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig() (config, error) {
|
|
||||||
var cfg config
|
|
||||||
cfg.Csrf.Secure = !DEBUG
|
|
||||||
|
|
||||||
err := godotenv.Load()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Warning: Could not load a .env file")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Csrf.Key = []byte(os.Getenv("LENSLOCKED_CSRF_KEY"))
|
|
||||||
if len(cfg.Csrf.Key) < 32 {
|
|
||||||
return cfg, fmt.Errorf("Error: no or bad csrf protection key\nPlease set the LENSLOCKED_CSRF_KEY env var to a key at least 32 characters long.")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Postgres = os.Getenv("LENSLOCKED_DB_STRING")
|
|
||||||
|
|
||||||
cfg.Email.Host = os.Getenv("LENSLOCKED_EMAIL_HOST")
|
|
||||||
cfg.Email.Username = os.Getenv("LENSLOCKED_EMAIL_USERNAME")
|
|
||||||
cfg.Email.Pass = os.Getenv("LENSLOCKED_EMAIL_PASSWORD")
|
|
||||||
cfg.Email.Sender = os.Getenv("LENSLOCKED_EMAIL_FROM")
|
|
||||||
cfg.Email.Port, err = strconv.Atoi(os.Getenv("LENSLOCKED_EMAIL_PORT"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Warning: Invalid STMP port set in LENSLOCKED_EMAIL_PORT. Using port 587")
|
|
||||||
cfg.Email.Port = 587
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Server.Address = os.Getenv("LENSLOCKED_ADDRESS")
|
|
||||||
if cfg.Server.Address == "" {
|
|
||||||
if DEBUG {
|
|
||||||
cfg.Server.Address = ":3000"
|
|
||||||
} else {
|
|
||||||
return cfg, fmt.Errorf("No server address set\nPlease set the LENSLOCKED_ADDRESS env var to the servers address")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf8")
|
w.Header().Set("Content-Type", "text/html; charset=utf8")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
fmt.Fprint(w, "404 page not found")
|
fmt.Fprint(w, "404 page not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectDB(dbstr string) *sql.DB {
|
func ConnectDB() *sql.DB {
|
||||||
db, err := sql.Open("pgx", dbstr)
|
db, err := sql.Open("pgx", os.Getenv("LENSLOCKED_DB_STRING"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprint("Error connecting to database: %w", err))
|
panic(fmt.Sprint("Error connecting to database: %w", err))
|
||||||
}
|
}
|
||||||
@ -94,44 +34,22 @@ func ConnectDB(dbstr string) *sql.DB {
|
|||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
func MigrateDB(db *sql.DB, subfs fs.FS) error {
|
|
||||||
goose.SetBaseFS(subfs)
|
|
||||||
defer func() { goose.SetBaseFS(nil) }()
|
|
||||||
err := goose.SetDialect("postgres")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Migrate: %w", err)
|
|
||||||
}
|
|
||||||
err = goose.Up(db, ".")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Migrate: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg, err := loadConfig()
|
csrfKey := []byte(os.Getenv("LENSLOCKED_CSRF_KEY"))
|
||||||
if err != nil {
|
if len(csrfKey) < 32 {
|
||||||
panic(err)
|
panic("Error: bad csrf protection key")
|
||||||
}
|
}
|
||||||
db := ConnectDB(cfg.Postgres)
|
|
||||||
|
db := ConnectDB()
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
err = MigrateDB(db, migrations.FS)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userService := models.UserService{DB: db}
|
userService := models.UserService{DB: db}
|
||||||
sessionService := models.SessionService{DB: db}
|
var usersCtrlr ctrlrs.Users = ctrlrs.Default(&userService)
|
||||||
emailService := models.NewEmailService(cfg.Email.Host, cfg.Email.Port, cfg.Email.Username, cfg.Email.Pass, cfg.Email.Sender)
|
|
||||||
var usersCtrlr ctrlrs.Users = ctrlrs.Default(&userService, &sessionService, emailService)
|
|
||||||
|
|
||||||
umw := userctx.UserMiddleware{SS: &sessionService}
|
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(csrf.Protect(cfg.Csrf.Key, csrf.Secure(cfg.Csrf.Secure)))
|
|
||||||
r.Use(umw.SetUser)
|
|
||||||
|
|
||||||
r.Get("/", ctrlrs.StaticController("home.gohtml", "tailwind.gohtml"))
|
r.Get("/", ctrlrs.StaticController("home.gohtml", "tailwind.gohtml"))
|
||||||
r.Get("/contact", ctrlrs.StaticController("contact.gohtml", "tailwind.gohtml"))
|
r.Get("/contact", ctrlrs.StaticController("contact.gohtml", "tailwind.gohtml"))
|
||||||
@ -141,15 +59,10 @@ func main() {
|
|||||||
r.Post("/signup", usersCtrlr.PostSignup)
|
r.Post("/signup", usersCtrlr.PostSignup)
|
||||||
r.Get("/signin", usersCtrlr.GetSignin)
|
r.Get("/signin", usersCtrlr.GetSignin)
|
||||||
r.Post("/signin", usersCtrlr.PostSignin)
|
r.Post("/signin", usersCtrlr.PostSignin)
|
||||||
r.Post("/signout", usersCtrlr.GetSignout)
|
|
||||||
r.Get("/forgot-pw", usersCtrlr.GetForgotPassword)
|
|
||||||
r.Post("/forgot-pw", usersCtrlr.PostForgotPassword)
|
|
||||||
r.Get("/reset-pw", usersCtrlr.GetResetPass)
|
|
||||||
r.Post("/reset-pw", usersCtrlr.PostResetPass)
|
|
||||||
|
|
||||||
r.Get("/user", umw.RequireUserfn(usersCtrlr.CurrentUser))
|
r.Get("/user", usersCtrlr.CurrentUser)
|
||||||
|
|
||||||
r.NotFound(notFoundHandler)
|
r.NotFound(notFoundHandler)
|
||||||
|
fmt.Println("Starting the server on :3000...")
|
||||||
fmt.Printf("Starting the server on %s...\n", cfg.Server.Address)
|
http.ListenAndServe(":3000", csrf.Protect(csrfKey, csrf.Secure(!DEBUG))(r))
|
||||||
http.ListenAndServe(cfg.Server.Address, r)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
CREATE TABLE users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
email TEXT UNIQUE NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL
|
|
||||||
);
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE users;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
CREATE TABLE sessions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INT UNIQUE REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT UNIQUE NOT NULL
|
|
||||||
);
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE sessions;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
CREATE TABLE password_resets (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INT UNIQUE REFERENCES users (id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT UNIQUE NOT NULL,
|
|
||||||
expires_at TIMESTAMPTZ NOT NULL
|
|
||||||
);
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
DROP TABLE password_resets;
|
|
||||||
-- +goose StatementEnd
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package migrations
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
//go:embed *.sql
|
|
||||||
var FS embed.FS
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-mail/mail/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Email struct {
|
|
||||||
To string
|
|
||||||
Subject string
|
|
||||||
Text string
|
|
||||||
Html string
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailService struct {
|
|
||||||
DefaultSender string
|
|
||||||
|
|
||||||
dialer *mail.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEmailService(host string, port int, username, pass, sender string) *EmailService {
|
|
||||||
es := EmailService{
|
|
||||||
dialer: mail.NewDialer(host, port, username, pass),
|
|
||||||
DefaultSender: sender,
|
|
||||||
}
|
|
||||||
return &es
|
|
||||||
}
|
|
||||||
|
|
||||||
func (es *EmailService) Send(email Email) error {
|
|
||||||
msg := mail.NewMessage()
|
|
||||||
|
|
||||||
msg.SetHeader("To", email.To)
|
|
||||||
msg.SetHeader("From", es.DefaultSender)
|
|
||||||
msg.SetHeader("Subject", email.Subject)
|
|
||||||
|
|
||||||
if email.Html != "" && email.Text != "" {
|
|
||||||
msg.SetBody("text/plain", email.Text)
|
|
||||||
msg.AddAlternative("text/html", email.Html)
|
|
||||||
} else if email.Text != "" {
|
|
||||||
msg.SetBody("text/plain", email.Text)
|
|
||||||
} else if email.Html != "" {
|
|
||||||
msg.SetBody("text/html", email.Html)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := es.dialer.DialAndSend(msg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("send: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (es *EmailService) SendPasswordReset(to, resetURL string) error {
|
|
||||||
email := Email{
|
|
||||||
Subject: "Reset your password",
|
|
||||||
To: to,
|
|
||||||
Text: "To reset your password, please visit the following link: " + resetURL,
|
|
||||||
Html: `<p>To reset your password, please visit the following link: <a href="` + resetURL + `">` + resetURL + `</a></p>`,
|
|
||||||
}
|
|
||||||
err := es.Send(email)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("forgot password email: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
resetTokenBytes int = 32
|
|
||||||
)
|
|
||||||
|
|
||||||
func resetToken() (string, error) {
|
|
||||||
return RandString(resetTokenBytes)
|
|
||||||
}
|
|
||||||
func hashToken(token string) string {
|
|
||||||
tokenHash := sha256.Sum256([]byte(token))
|
|
||||||
return base64.URLEncoding.EncodeToString(tokenHash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
type PasswordReset struct {
|
|
||||||
ID int
|
|
||||||
UserID int
|
|
||||||
// Token is only set when a PasswordReset is being created.
|
|
||||||
Token string
|
|
||||||
TokenHash string
|
|
||||||
ExpiresAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type PasswordResetService struct {
|
|
||||||
DB *sql.DB
|
|
||||||
BytesPerToken int
|
|
||||||
Duration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *PasswordResetService) delete(id int) error {
|
|
||||||
_, err := service.DB.Exec(`
|
|
||||||
DELETE FROM password_resets
|
|
||||||
WHERE id = $1;`, id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("delete: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *PasswordResetService) Create(email string) (*PasswordReset, error) {
|
|
||||||
// Verify we have a valid email address for user
|
|
||||||
email = strings.ToLower(email)
|
|
||||||
var UserID int
|
|
||||||
row := service.DB.QueryRow(`
|
|
||||||
SELECT id FROM users WHERE email = $1;
|
|
||||||
`, email)
|
|
||||||
err := row.Scan(&UserID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := resetToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := service.Duration
|
|
||||||
if duration == 0 {
|
|
||||||
duration = time.Hour
|
|
||||||
}
|
|
||||||
|
|
||||||
pwReset := PasswordReset{
|
|
||||||
UserID: UserID,
|
|
||||||
Token: token,
|
|
||||||
TokenHash: hashToken(token),
|
|
||||||
ExpiresAt: time.Now().Add(duration),
|
|
||||||
}
|
|
||||||
|
|
||||||
row = service.DB.QueryRow(`
|
|
||||||
INSERT INTO password_resets (user_id, token_hash, expires_at)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE
|
|
||||||
SET token_hash = $2, expires_at = $3
|
|
||||||
RETURNING id;
|
|
||||||
`, pwReset.UserID, pwReset.TokenHash, pwReset.ExpiresAt)
|
|
||||||
err = row.Scan(&pwReset.ID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create: %w", err)
|
|
||||||
}
|
|
||||||
return &pwReset, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are going to consume a token and return the user associated with it, or return an error if the token wasn't valid for any reason.
|
|
||||||
func (service *PasswordResetService) Consume(token string) (*User, error) {
|
|
||||||
var pwReset PasswordReset
|
|
||||||
var user User
|
|
||||||
pwReset.TokenHash = hashToken(token)
|
|
||||||
|
|
||||||
row := service.DB.QueryRow(`
|
|
||||||
SELECT password_resets.id, password_resets.expires_at, users.id, users.email, users.password_hash
|
|
||||||
FROM password_resets JOIN users ON users.id = password_resets.user_id
|
|
||||||
WHERE password_resets.token_hash = $1;
|
|
||||||
`, pwReset.TokenHash)
|
|
||||||
err := row.Scan(&pwReset.ID, &pwReset.ExpiresAt, &user.ID, &user.Email, &user.PasswordHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Consume: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(pwReset.ExpiresAt) {
|
|
||||||
return nil, fmt.Errorf("Invalid token")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = service.delete(pwReset.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("consume: %w", err)
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RandBytes(n int) ([]byte, error) {
|
|
||||||
b := make([]byte, n)
|
|
||||||
nRead, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("bytes: %w", err)
|
|
||||||
}
|
|
||||||
if nRead < n {
|
|
||||||
return nil, fmt.Errorf("bytes: didn't read enough random bytes")
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
func RandString(n int) (string, error) {
|
|
||||||
b, err := RandBytes(n)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("string: %w", err)
|
|
||||||
}
|
|
||||||
return base64.URLEncoding.EncodeToString(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const SessionTokenBytes = 32
|
|
||||||
|
|
||||||
func SessionToken() (string, error) {
|
|
||||||
return RandString(SessionTokenBytes)
|
|
||||||
}
|
|
||||||
func hash(token string) string {
|
|
||||||
tokenHash := sha256.Sum256([]byte(token))
|
|
||||||
return base64.StdEncoding.EncodeToString(tokenHash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
ID int
|
|
||||||
UserID int
|
|
||||||
TokenHash string
|
|
||||||
// Token is only set when creating a new session. When looking up a session
|
|
||||||
// this will be left empty, as we only store the hash of a session token
|
|
||||||
// in our database and we cannot reverse it into a raw token.
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionService struct {
|
|
||||||
DB *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *SessionService) Create(userID int) (*Session, error) {
|
|
||||||
token, err := SessionToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create: %w", err)
|
|
||||||
}
|
|
||||||
session := Session{
|
|
||||||
UserID: userID,
|
|
||||||
Token: token,
|
|
||||||
TokenHash: hash(token),
|
|
||||||
}
|
|
||||||
row := ss.DB.QueryRow(`
|
|
||||||
INSERT INTO sessions (user_id, token_hash)
|
|
||||||
VALUES ($1, $2) ON CONFLICT (user_id) DO
|
|
||||||
UPDATE
|
|
||||||
SET token_hash = $2
|
|
||||||
RETURNING id;
|
|
||||||
`, session.UserID, session.TokenHash)
|
|
||||||
err = row.Scan(&session.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create: %w", err)
|
|
||||||
}
|
|
||||||
return &session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *SessionService) Delete(token string) error {
|
|
||||||
tokenHash := hash(token)
|
|
||||||
_, err := ss.DB.Exec(`DELETE FROM sessions WHERE token_hash = $1;`, tokenHash)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("delete: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ss *SessionService) User(token string) (*User, error) {
|
|
||||||
token_hash := hash(token)
|
|
||||||
var user User
|
|
||||||
|
|
||||||
row := ss.DB.QueryRow(`
|
|
||||||
SELECT users.id,
|
|
||||||
users.email,
|
|
||||||
users.password_hash
|
|
||||||
FROM sessions
|
|
||||||
JOIN users ON users.id = sessions.user_id
|
|
||||||
WHERE sessions.token_hash = $1;
|
|
||||||
`, token_hash)
|
|
||||||
err := row.Scan(&user.ID, &user.Email, &user.PasswordHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user, err
|
|
||||||
}
|
|
||||||
5
models/sql/users.sql
Normal file
5
models/sql/users.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
);
|
||||||
@ -42,22 +42,6 @@ func (us *UserService) Create(email, password string) (*User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserService) UpdatePassword(userID int, password string) error {
|
|
||||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update password: %w", err)
|
|
||||||
}
|
|
||||||
passwordHash := string(hashedBytes)
|
|
||||||
_, err = us.DB.Exec(`
|
|
||||||
UPDATE users
|
|
||||||
SET password_hash = $2
|
|
||||||
WHERE id = $1;`, userID, passwordHash)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update password: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (us UserService) Authenticate(email, password string) (*User, error) {
|
func (us UserService) Authenticate(email, password string) (*User, error) {
|
||||||
user := User{
|
user := User{
|
||||||
Email: strings.ToLower(email),
|
Email: strings.ToLower(email),
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
{{template "head" .}}
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
|
||||||
{{template "header".}}
|
|
||||||
<main class="px-6">
|
|
||||||
<div class="py-12 flex justify-center">
|
|
||||||
<div class="px-8 py-8 bg-white rounded shadow">
|
|
||||||
<h1 class="pt-4 pb-8 text-center text-3xl font-bold text-gray-900">
|
|
||||||
Reset your password
|
|
||||||
</h1>
|
|
||||||
<form action="/reset-pw" method="post">
|
|
||||||
<div class="hidden">
|
|
||||||
{{csrfField}}
|
|
||||||
</div>
|
|
||||||
<div class="py-2">
|
|
||||||
<label for="password" class="text-sm font-semibold text-gray-800"
|
|
||||||
>New password</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
px-3
|
|
||||||
py-2
|
|
||||||
border border-gray-300
|
|
||||||
placeholder-gray-500
|
|
||||||
text-gray-800
|
|
||||||
rounded
|
|
||||||
"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{if .Token}}
|
|
||||||
<div class="hidden">
|
|
||||||
<input type="hidden" id="token" name="token" value="{{.Token}}" />
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="py-2">
|
|
||||||
<label for="token" class="text-sm font-semibold text-gray-800"
|
|
||||||
>Password Reset Token</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
name="token"
|
|
||||||
id="token"
|
|
||||||
type="text"
|
|
||||||
placeholder="Check your email"
|
|
||||||
required
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
px-3
|
|
||||||
py-2
|
|
||||||
border border-gray-300
|
|
||||||
placeholder-gray-500
|
|
||||||
text-gray-800
|
|
||||||
rounded
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div class="py-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
py-4
|
|
||||||
px-2
|
|
||||||
bg-indigo-600
|
|
||||||
hover:bg-indigo-700
|
|
||||||
text-white
|
|
||||||
rounded
|
|
||||||
font-bold
|
|
||||||
text-lg
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Update password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="py-2 w-full flex justify-between">
|
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
<a href="/signup" class="underline">Sign up</a>
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
<a href="/signin" class="underline">Sign in</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{template "footer" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
{{template "head" .}}
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
|
||||||
{{template "header".}}
|
|
||||||
<main class="px-6">
|
|
||||||
<div class="py-12 flex justify-center">
|
|
||||||
<div class="px-8 py-8 bg-white rounded shadow">
|
|
||||||
<h1 class="pt-4 pb-8 text-center text-3xl font-bold text-gray-900">
|
|
||||||
Forgot your password?
|
|
||||||
</h1>
|
|
||||||
<p class="text-sm text-gray-600 pb-4">No problem. Enter your email address and we'll send you a link to reset your password.</p>
|
|
||||||
<form action="/forgot-pw" method="post">
|
|
||||||
<div class="hidden">
|
|
||||||
{{csrfField}}
|
|
||||||
</div>
|
|
||||||
<div class="py-2">
|
|
||||||
<label for="email" class="text-sm font-semibold text-gray-800"
|
|
||||||
>Email Address</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email address"
|
|
||||||
required
|
|
||||||
autocomplete="email"
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
px-3
|
|
||||||
py-2
|
|
||||||
border border-gray-300
|
|
||||||
placeholder-gray-500
|
|
||||||
text-gray-800
|
|
||||||
rounded
|
|
||||||
"
|
|
||||||
value="{{.Email}}"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="py-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
py-4
|
|
||||||
px-2
|
|
||||||
bg-indigo-600
|
|
||||||
hover:bg-indigo-700
|
|
||||||
text-white
|
|
||||||
rounded
|
|
||||||
font-bold
|
|
||||||
text-lg
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Reset password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="py-2 w-full flex justify-between">
|
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
Need an account?
|
|
||||||
<a href="/signup" class="underline">Sign up</a>
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
<a href="/signin" class="underline">Remember your password?</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{template "footer" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
{{template "head" .}}
|
|
||||||
<body class="min-h-screen bg-gray-100">
|
|
||||||
{{template "header".}}
|
|
||||||
<main class="px-6">
|
|
||||||
<div class="py-12 flex justify-center">
|
|
||||||
<div class="px-8 py-8 bg-white rounded shadow">
|
|
||||||
<h1 class="pt-4 pb-8 text-center text-3xl font-bold text-gray-900">
|
|
||||||
Check your email
|
|
||||||
</h1>
|
|
||||||
<p class="text-sm text-gray-600 pb-4">An email has been sent to the email address {{.Email}} with instructions to reset your password.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{template "footer" .}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -54,7 +54,7 @@
|
|||||||
<a href="/signup" class="underline">Sign up</a>
|
<a href="/signup" class="underline">Sign up</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
<a href="/forgot-pw" class="underline">Forgot your password?</a>
|
<a href="/reset-pw" class="underline">Forgot your password?</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
<a href="/signin" class="underline">Sign in</a>
|
<a href="/signin" class="underline">Sign in</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-500">
|
<p class="text-xs text-gray-500">
|
||||||
<a href="/forgot-pw" class="underline">Forgot your password?</a>
|
<a href="/reset-pw" class="underline">Forgot your password?</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -16,15 +16,8 @@
|
|||||||
<a class="text-base font-semibold hover:text-blue-100 pr-8" href="/faq">FAQ</a>
|
<a class="text-base font-semibold hover:text-blue-100 pr-8" href="/faq">FAQ</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-4">
|
<div class="space-x-4">
|
||||||
{{if currentUser }}
|
|
||||||
<form action="/signout" method="post" class="inline pr-4">
|
|
||||||
{{csrfField}}
|
|
||||||
<button type="submit">Sign out</button>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<a href="/signin">Sign in</a>
|
<a href="/signin">Sign in</a>
|
||||||
<a href="/signup" clss="px-4 py-2 bg-blue-700 hover:bg-blue-600 rounded">Sign up</a>
|
<a href="/signup" clss="px-4 py-2 bg-blue-700 hover:bg-blue-600 rounded">Sign up</a>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
userctx "git.kealoha.me/lks/lenslocked/context"
|
|
||||||
"git.kealoha.me/lks/lenslocked/models"
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,35 +25,23 @@ func (t Template) Execute(w http.ResponseWriter, r *http.Request, data interface
|
|||||||
}
|
}
|
||||||
|
|
||||||
tpl = tpl.Funcs(template.FuncMap{
|
tpl = tpl.Funcs(template.FuncMap{
|
||||||
"csrfField": func() template.HTML { return csrf.TemplateField(r) },
|
"csrfField": func() template.HTML { return csrf.TemplateField(r) },
|
||||||
"currentUser": func() *models.User { return userctx.User(r.Context()) },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf8")
|
w.Header().Set("Content-Type", "text/html; charset=utf8")
|
||||||
var buf bytes.Buffer
|
err = tpl.Execute(w, data)
|
||||||
err = tpl.Execute(&buf, data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error executing template: %v", err)
|
log.Printf("Error executing template: %v", err)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
io.Copy(w, &buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Template) TestTemplate(data interface{}) error {
|
func (t Template) TestTemplate(data interface{}) error {
|
||||||
var testWriter strings.Builder
|
var testWriter strings.Builder
|
||||||
tpl, err := t.htmlTpl.Clone()
|
tpl, err := t.htmlTpl.Clone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tpl = tpl.Funcs(template.FuncMap{
|
|
||||||
"csrfField": func() template.HTML {
|
|
||||||
return `<div class="hidden">STUB: PLACEHOLDER</div>`
|
|
||||||
},
|
|
||||||
"currentUser": func() *models.User {
|
|
||||||
return &models.User{ID: 0, Email: "a@a"}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return tpl.Execute(&testWriter, data)
|
return tpl.Execute(&testWriter, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,11 +52,8 @@ func FromFile(pattern ...string) (Template, error) {
|
|||||||
func FromFS(fs fs.FS, pattern ...string) (Template, error) {
|
func FromFS(fs fs.FS, pattern ...string) (Template, error) {
|
||||||
tpl := template.New(pattern[0])
|
tpl := template.New(pattern[0])
|
||||||
tpl = tpl.Funcs(template.FuncMap{
|
tpl = tpl.Funcs(template.FuncMap{
|
||||||
"csrfField": func() (template.HTML, error) {
|
"csrfField": func() template.HTML {
|
||||||
return "", fmt.Errorf("csrfField Not Implimented")
|
return `<div class="hidden">STUB: PLACEHOLDER</div>`
|
||||||
},
|
|
||||||
"currentUser": func() (*models.User, error) {
|
|
||||||
return nil, fmt.Errorf("currentUser Not Implimented")
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
tpl, err := tpl.ParseFS(fs, pattern...)
|
tpl, err := tpl.ParseFS(fs, pattern...)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user