Add login view
This commit is contained in:
parent
b7221cb364
commit
63b1cf1c3f
9 changed files with 687 additions and 2 deletions
4
assets/migrations/1_user.down.sql
Normal file
4
assets/migrations/1_user.down.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
DROP TABLE "reset";
|
||||||
|
DROP TABLE "confirmation";
|
||||||
|
DROP TABLE "email";
|
||||||
|
DROP TABLE "user";
|
37
assets/migrations/1_user.up.sql
Normal file
37
assets/migrations/1_user.up.sql
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" bigserial NOT NULL,
|
||||||
|
"is_admin" boolean NOT NULL DEFAULT false,
|
||||||
|
"password" bytea NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "email" (
|
||||||
|
"address" text NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY ("address")
|
||||||
|
);
|
||||||
|
CREATE INDEX ON "email" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "confirmation" (
|
||||||
|
"email_address" text NOT NULL,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"selector" text NOT NULL,
|
||||||
|
"verifier" bytea NOT NULL, -- hashed
|
||||||
|
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY ("selector")
|
||||||
|
);
|
||||||
|
CREATE INDEX ON "confirmation" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE "reset" (
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"selector" text NOT NULL,
|
||||||
|
"verifier" bytea NOT NULL, -- hashed
|
||||||
|
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY ("selector")
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX ON "reset" ("user_id");
|
24
assets/templates/layouts/auth_login.tmpl
Normal file
24
assets/templates/layouts/auth_login.tmpl
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Landing Page</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Login</h1>
|
||||||
|
{{- range .Errors }}
|
||||||
|
<p class="is-error">{{ . }}</p>
|
||||||
|
{{- end }}
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<input type="email" name="login" required>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
{{ .csrfField }}
|
||||||
|
<input type="submit" value="Log In">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
231
internal/database/email.xo.go
Normal file
231
internal/database/email.xo.go
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
// Package database contains the types for schema 'public'.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// Code generated by xo. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Email represents a row from '"public"."email"'.
|
||||||
|
type Email struct {
|
||||||
|
Address string `db:"address"` // address
|
||||||
|
UserID int64 `db:"user_id"` // user_id
|
||||||
|
CreatedAt time.Time `db:"created_at"` // created_at
|
||||||
|
|
||||||
|
// xo fields
|
||||||
|
_exists, _deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists determines if the Email exists in the database.
|
||||||
|
func (e *Email) Exists() bool {
|
||||||
|
return e._exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted provides information if the Email has been deleted from the database.
|
||||||
|
func (e *Email) Deleted() bool {
|
||||||
|
return e._deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts the Email to the database.
|
||||||
|
func (e *Email) Insert(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if already exist, bail
|
||||||
|
if e._exists {
|
||||||
|
return errors.New("insert failed: already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql insert query, primary key must be provided
|
||||||
|
const sqlstr = `INSERT INTO "public"."email" (` +
|
||||||
|
`"address", "user_id", "created_at"` +
|
||||||
|
`) VALUES (` +
|
||||||
|
`$1, $2, $3` +
|
||||||
|
`)`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, e.Address, e.UserID, e.CreatedAt)
|
||||||
|
_, err = db.Exec(sqlstr, e.Address, e.UserID, e.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set existence
|
||||||
|
e._exists = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the Email in the database.
|
||||||
|
func (e *Email) Update(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if doesn't exist, bail
|
||||||
|
if !e._exists {
|
||||||
|
return errors.New("update failed: does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if deleted, bail
|
||||||
|
if e._deleted {
|
||||||
|
return errors.New("update failed: marked for deletion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `UPDATE "public"."email" SET (` +
|
||||||
|
`"user_id", "created_at"` +
|
||||||
|
`) = ( ` +
|
||||||
|
`$1, $2` +
|
||||||
|
`) WHERE "address" = $3`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, e.UserID, e.CreatedAt, e.Address)
|
||||||
|
_, err = db.Exec(sqlstr, e.UserID, e.CreatedAt, e.Address)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the Email to the database.
|
||||||
|
func (e *Email) Save(db XODB) error {
|
||||||
|
if e.Exists() {
|
||||||
|
return e.Update(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Insert(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert performs an upsert for Email.
|
||||||
|
//
|
||||||
|
// NOTE: PostgreSQL 9.5+ only
|
||||||
|
func (e *Email) Upsert(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if already exist, bail
|
||||||
|
if e._exists {
|
||||||
|
return errors.New("insert failed: already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `INSERT INTO "public"."email" (` +
|
||||||
|
`"address", "user_id", "created_at"` +
|
||||||
|
`) VALUES (` +
|
||||||
|
`$1, $2, $3` +
|
||||||
|
`) ON CONFLICT ("address") DO UPDATE SET (` +
|
||||||
|
`"address", "user_id", "created_at"` +
|
||||||
|
`) = (` +
|
||||||
|
`EXCLUDED."address", EXCLUDED."user_id", EXCLUDED."created_at"` +
|
||||||
|
`)`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, e.Address, e.UserID, e.CreatedAt)
|
||||||
|
_, err = db.Exec(sqlstr, e.Address, e.UserID, e.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set existence
|
||||||
|
e._exists = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the Email from the database.
|
||||||
|
func (e *Email) Delete(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if doesn't exist, bail
|
||||||
|
if !e._exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if deleted, bail
|
||||||
|
if e._deleted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `DELETE FROM "public"."email" WHERE "address" = $1`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, e.Address)
|
||||||
|
_, err = db.Exec(sqlstr, e.Address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set deleted
|
||||||
|
e._deleted = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns the User associated with the Email's UserID (user_id).
|
||||||
|
//
|
||||||
|
// Generated from foreign key 'email_user_id_fkey'.
|
||||||
|
func (e *Email) User(db XODB) (*User, error) {
|
||||||
|
return UserByID(db, e.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmailByAddress retrieves a row from '"public"."email"' as a Email.
|
||||||
|
//
|
||||||
|
// Generated from index 'email_pkey'.
|
||||||
|
func EmailByAddress(db XODB, address string) (*Email, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `SELECT ` +
|
||||||
|
`"address", "user_id", "created_at" ` +
|
||||||
|
`FROM "public"."email" ` +
|
||||||
|
`WHERE "address" = $1`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, address)
|
||||||
|
e := Email{
|
||||||
|
_exists: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.QueryRow(sqlstr, address).Scan(&e.Address, &e.UserID, &e.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmailsByUserID retrieves a row from '"public"."email"' as a Email.
|
||||||
|
//
|
||||||
|
// Generated from index 'email_user_id_idx'.
|
||||||
|
func EmailsByUserID(db XODB, userID int64) ([]*Email, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `SELECT ` +
|
||||||
|
`"address", "user_id", "created_at" ` +
|
||||||
|
`FROM "public"."email" ` +
|
||||||
|
`WHERE "user_id" = $1`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, userID)
|
||||||
|
q, err := db.Query(sqlstr, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer q.Close()
|
||||||
|
|
||||||
|
// load results
|
||||||
|
res := []*Email{}
|
||||||
|
for q.Next() {
|
||||||
|
e := Email{
|
||||||
|
_exists: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan
|
||||||
|
err = q.Scan(&e.Address, &e.UserID, &e.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, &e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
186
internal/database/user.xo.go
Normal file
186
internal/database/user.xo.go
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// Package database contains the types for schema 'public'.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// Code generated by xo. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a row from '"public"."user"'.
|
||||||
|
type User struct {
|
||||||
|
ID int64 `db:"id"` // id
|
||||||
|
IsAdmin bool `db:"is_admin"` // is_admin
|
||||||
|
Password []byte `db:"password"` // password
|
||||||
|
CreatedAt time.Time `db:"created_at"` // created_at
|
||||||
|
|
||||||
|
// xo fields
|
||||||
|
_exists, _deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists determines if the User exists in the database.
|
||||||
|
func (u *User) Exists() bool {
|
||||||
|
return u._exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted provides information if the User has been deleted from the database.
|
||||||
|
func (u *User) Deleted() bool {
|
||||||
|
return u._deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts the User to the database.
|
||||||
|
func (u *User) Insert(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if already exist, bail
|
||||||
|
if u._exists {
|
||||||
|
return errors.New("insert failed: already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql insert query, primary key provided by sequence
|
||||||
|
const sqlstr = `INSERT INTO "public"."user" (` +
|
||||||
|
`"is_admin", "password", "created_at"` +
|
||||||
|
`) VALUES (` +
|
||||||
|
`$1, $2, $3` +
|
||||||
|
`) RETURNING "id"`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, u.IsAdmin, u.Password, u.CreatedAt)
|
||||||
|
err = db.QueryRow(sqlstr, u.IsAdmin, u.Password, u.CreatedAt).Scan(&u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set existence
|
||||||
|
u._exists = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the User in the database.
|
||||||
|
func (u *User) Update(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if doesn't exist, bail
|
||||||
|
if !u._exists {
|
||||||
|
return errors.New("update failed: does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if deleted, bail
|
||||||
|
if u._deleted {
|
||||||
|
return errors.New("update failed: marked for deletion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `UPDATE "public"."user" SET (` +
|
||||||
|
`"is_admin", "password", "created_at"` +
|
||||||
|
`) = ( ` +
|
||||||
|
`$1, $2, $3` +
|
||||||
|
`) WHERE "id" = $4`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, u.IsAdmin, u.Password, u.CreatedAt, u.ID)
|
||||||
|
_, err = db.Exec(sqlstr, u.IsAdmin, u.Password, u.CreatedAt, u.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the User to the database.
|
||||||
|
func (u *User) Save(db XODB) error {
|
||||||
|
if u.Exists() {
|
||||||
|
return u.Update(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Insert(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert performs an upsert for User.
|
||||||
|
//
|
||||||
|
// NOTE: PostgreSQL 9.5+ only
|
||||||
|
func (u *User) Upsert(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if already exist, bail
|
||||||
|
if u._exists {
|
||||||
|
return errors.New("insert failed: already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `INSERT INTO "public"."user" (` +
|
||||||
|
`"id", "is_admin", "password", "created_at"` +
|
||||||
|
`) VALUES (` +
|
||||||
|
`$1, $2, $3, $4` +
|
||||||
|
`) ON CONFLICT ("id") DO UPDATE SET (` +
|
||||||
|
`"id", "is_admin", "password", "created_at"` +
|
||||||
|
`) = (` +
|
||||||
|
`EXCLUDED."id", EXCLUDED."is_admin", EXCLUDED."password", EXCLUDED."created_at"` +
|
||||||
|
`)`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, u.ID, u.IsAdmin, u.Password, u.CreatedAt)
|
||||||
|
_, err = db.Exec(sqlstr, u.ID, u.IsAdmin, u.Password, u.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set existence
|
||||||
|
u._exists = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the User from the database.
|
||||||
|
func (u *User) Delete(db XODB) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// if doesn't exist, bail
|
||||||
|
if !u._exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if deleted, bail
|
||||||
|
if u._deleted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `DELETE FROM "public"."user" WHERE "id" = $1`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, u.ID)
|
||||||
|
_, err = db.Exec(sqlstr, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set deleted
|
||||||
|
u._deleted = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserByID retrieves a row from '"public"."user"' as a User.
|
||||||
|
//
|
||||||
|
// Generated from index 'user_pkey'.
|
||||||
|
func UserByID(db XODB, id int64) (*User, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// sql query
|
||||||
|
const sqlstr = `SELECT ` +
|
||||||
|
`"id", "is_admin", "password", "created_at" ` +
|
||||||
|
`FROM "public"."user" ` +
|
||||||
|
`WHERE "id" = $1`
|
||||||
|
|
||||||
|
// run query
|
||||||
|
XOLog(sqlstr, id)
|
||||||
|
u := User{
|
||||||
|
_exists: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.QueryRow(sqlstr, id).Scan(&u.ID, &u.IsAdmin, &u.Password, &u.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
85
internal/database/xo_db.xo.go
Normal file
85
internal/database/xo_db.xo.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Package database contains the types for schema 'public'.
|
||||||
|
package database
|
||||||
|
|
||||||
|
// Code generated by xo. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XODB is the common interface for database operations that can be used with
|
||||||
|
// types from schema 'public'.
|
||||||
|
//
|
||||||
|
// This should work with database/sql.DB and database/sql.Tx.
|
||||||
|
type XODB interface {
|
||||||
|
Exec(string, ...interface{}) (sql.Result, error)
|
||||||
|
Query(string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRow(string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
// XOLog provides the log func used by generated queries.
|
||||||
|
var XOLog = func(string, ...interface{}) {}
|
||||||
|
|
||||||
|
// ScannerValuer is the common interface for types that implement both the
|
||||||
|
// database/sql.Scanner and sql/driver.Valuer interfaces.
|
||||||
|
type ScannerValuer interface {
|
||||||
|
sql.Scanner
|
||||||
|
driver.Valuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice is a slice of strings.
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
// quoteEscapeRegex is the regex to match escaped characters in a string.
|
||||||
|
var quoteEscapeRegex = regexp.MustCompile(`([^\\]([\\]{2})*)\\"`)
|
||||||
|
|
||||||
|
// Scan satisfies the sql.Scanner interface for StringSlice.
|
||||||
|
func (ss *StringSlice) Scan(src interface{}) error {
|
||||||
|
buf, ok := src.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid StringSlice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// change quote escapes for csv parser
|
||||||
|
str := quoteEscapeRegex.ReplaceAllString(string(buf), `$1""`)
|
||||||
|
str = strings.Replace(str, `\\`, `\`, -1)
|
||||||
|
|
||||||
|
// remove braces
|
||||||
|
str = str[1 : len(str)-1]
|
||||||
|
|
||||||
|
// bail if only one
|
||||||
|
if len(str) == 0 {
|
||||||
|
*ss = StringSlice([]string{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse with csv reader
|
||||||
|
cr := csv.NewReader(strings.NewReader(str))
|
||||||
|
slice, err := cr.Read()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("exiting!: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*ss = StringSlice(slice)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value satisfies the driver.Valuer interface for StringSlice.
|
||||||
|
func (ss StringSlice) Value() (driver.Value, error) {
|
||||||
|
v := make([]string, len(ss))
|
||||||
|
for i, s := range ss {
|
||||||
|
v[i] = `"` + strings.Replace(strings.Replace(s, `\`, `\\\`, -1), `"`, `\"`, -1) + `"`
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(v, ",") + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice is a slice of ScannerValuers.
|
||||||
|
type Slice []ScannerValuer
|
|
@ -3,8 +3,9 @@ package web
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alexedwards/scs"
|
|
||||||
"bitmask.me/skeleton/internal/app"
|
"bitmask.me/skeleton/internal/app"
|
||||||
|
"github.com/alexedwards/scs"
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
|
@ -24,6 +25,22 @@ func (h *Handlers) Session() *scs.Session {
|
||||||
return h.session
|
return h.session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) commonRenderContext(r *http.Request) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
|
"Username": h.Session().GetString(r.Context(), SessKeyUserName),
|
||||||
|
"UserID": h.Session().GetString(r.Context(), SessKeyUserID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) CSRF() func(http.Handler) http.Handler {
|
||||||
|
return csrf.Protect(
|
||||||
|
[]byte("12345678901234567890123456789012"),
|
||||||
|
csrf.FieldName("authenticity_token"),
|
||||||
|
csrf.Secure(h.session.Cookie.Secure),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handlers) LandingPageHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) LandingPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
h.Templates().Get("landing.tmpl").Execute(w, nil)
|
h.Templates().Get("landing.tmpl").Execute(w, nil)
|
||||||
}
|
}
|
||||||
|
|
98
internal/web/handlers_auth.go
Normal file
98
internal/web/handlers_auth.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"bitmask.me/skeleton/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNotImplemented is returned whenever a feature is not implemented yet.
|
||||||
|
var ErrNotImplemented = errors.New("Not implemented")
|
||||||
|
|
||||||
|
// User interface is provided by all data types that are returned from a
|
||||||
|
// User store.
|
||||||
|
type User interface {
|
||||||
|
GetID() string
|
||||||
|
GetDisplayName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRow wraps a user row from the database.
|
||||||
|
type UserRow struct {
|
||||||
|
*database.User
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplayName implements User interface by returning the display name.
|
||||||
|
func (u UserRow) GetDisplayName() string {
|
||||||
|
return u.GetID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID implements the User interface by returning the user ID.
|
||||||
|
func (u UserRow) GetID() string {
|
||||||
|
return strconv.FormatInt(u.ID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthenticator returns a authable function from a Database.
|
||||||
|
func NewAuthenticator(db *sqlx.DB) func(user, pass string) (User, error) {
|
||||||
|
return func(user, pass string) (User, error) {
|
||||||
|
// Fetch email used for login
|
||||||
|
email, err := database.EmailByAddress(db, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err := email.User(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//u.Password
|
||||||
|
err = bcrypt.CompareHashAndPassword(row.Password, []byte(pass))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u := UserRow{row}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginPageHandler renders the login page, and sets session cookies
|
||||||
|
// on successful authentication.
|
||||||
|
func (h *Handlers) LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
type LoginForm struct {
|
||||||
|
Login string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
loginForm := LoginForm{
|
||||||
|
Login: r.PostFormValue("login"),
|
||||||
|
Password: r.PostFormValue("password"),
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate := NewAuthenticator(h.App.Database())
|
||||||
|
|
||||||
|
user, err := authenticate(loginForm.Login, loginForm.Password)
|
||||||
|
if err != nil {
|
||||||
|
context := h.commonRenderContext(r)
|
||||||
|
context["Errors"] = []string{"Wrong username or password"}
|
||||||
|
h.Templates().Get("auth_login.tmpl").Execute(w, context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := h.Session()
|
||||||
|
sess.Put(r.Context(), SessKeyUserID, user.GetID())
|
||||||
|
sess.Put(r.Context(), SessKeyUserName, user.GetDisplayName())
|
||||||
|
http.Redirect(w, r, "/app", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Templates().Get("auth_login.tmpl").Execute(w, h.commonRenderContext(r))
|
||||||
|
}
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"bitmask.me/skeleton/internal/app"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
"bitmask.me/skeleton/internal/app"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerRoutes(ac *app.App, r chi.Router) {
|
func registerRoutes(ac *app.App, r chi.Router) {
|
||||||
|
@ -20,6 +20,9 @@ func registerRoutes(ac *app.App, r chi.Router) {
|
||||||
h.Session().LoadAndSave,
|
h.Session().LoadAndSave,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
r.Get("/login", h.LoginPageHandler)
|
||||||
|
r.Post("/login", h.LoginPageHandler)
|
||||||
|
|
||||||
r.Get("/", h.LandingPageHandler)
|
r.Get("/", h.LandingPageHandler)
|
||||||
|
|
||||||
r.Route("/app", func(r chi.Router) {
|
r.Route("/app", func(r chi.Router) {
|
||||||
|
|
Loading…
Reference in a new issue