Implement login

This commit is contained in:
Sung Won Cho 2021-01-04 22:07:37 +11:00
commit 6cb0e4e223
7 changed files with 164 additions and 59 deletions

View file

@ -20,6 +20,7 @@ package app
import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/token"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
@ -99,3 +100,42 @@ func (a *App) CreateUser(email, password string) (database.User, error) {
return user, nil
}
// Authenticate authenticates a user
func (a *App) Authenticate(email, password string) (*database.User, error) {
var account database.Account
conn := a.DB.Debug().Where("email = ?", email).First(&account)
if conn.RecordNotFound() {
return nil, errors.New("not found")
} else if conn.Error != nil {
return nil, conn.Error
}
err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), []byte(password))
if err != nil {
return nil, err
}
var user database.User
err = a.DB.Where("id = ?", account.UserID).First(&user).Error
if err != nil {
return nil, errors.Wrap(err, "finding user")
}
return &user, nil
}
// SignIn signs in a user
func (a *App) SignIn(user *database.User) (*database.Session, error) {
err := a.TouchLastLoginAt(*user, a.DB)
if err != nil {
log.ErrorWrap(err, "touching login timestamp")
}
session, err := a.CreateSession(user.ID)
if err != nil {
return nil, errors.Wrap(err, "creating session")
}
return &session, nil
}

View file

@ -15,7 +15,7 @@ type Controllers struct {
func New(cfg config.Config, app *app.App) *Controllers {
c := Controllers{}
c.Users = NewUsers(cfg, app.DB)
c.Users = NewUsers(cfg, app)
c.Static = NewStatic(cfg)
return &c

View file

@ -1,27 +1,31 @@
package controllers
import (
"encoding/json"
"net/http"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/config"
"github.com/dnote/dnote/pkg/server/log"
"github.com/dnote/dnote/pkg/server/views"
"github.com/jinzhu/gorm"
)
// NewUsers creates a new Users controller.
// It panics if the necessary templates are not parsed.
func NewUsers(cfg config.Config, db *gorm.DB) *Users {
func NewUsers(cfg config.Config, app *app.App) *Users {
return &Users{
NewView: views.NewView(cfg.PageTemplateDir, views.Config{Title: "Join", Layout: "base"}, "users/new"),
LoginView: views.NewView(cfg.PageTemplateDir, views.Config{Title: "Login", Layout: "base"}, "users/login"),
onPremise: cfg.OnPremise,
db: db,
app: app,
}
}
// Users is a user controller.
type Users struct {
NewView *views.View
db *gorm.DB
LoginView *views.View
app *app.App
onPremise bool
}
@ -43,3 +47,41 @@ type LoginForm struct {
Email string `schema:"email" json:"email"`
Password string `schema:"password" json:"password"`
}
func (u *Users) Login(w http.ResponseWriter, r *http.Request) {
var form LoginForm
if err := parseRequestData(r, &form); err != nil {
log.Error(err.Error())
w.WriteHeader(500)
return
}
user, err := u.app.Authenticate(form.Email, form.Password)
if err != nil {
log.Error(err.Error())
w.WriteHeader(500)
return
}
session, err := u.app.SignIn(user)
if err != nil {
log.Error(err.Error())
w.WriteHeader(500)
return
}
setSessionCookie(w, session.Key, session.ExpiresAt)
response := SessionResponse{
Key: session.Key,
ExpiresAt: session.ExpiresAt.Unix(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Error(err.Error())
return
}
}

View file

@ -0,0 +1,12 @@
package database
import (
"github.com/pkg/errors"
)
type modelError string
var (
// ErrNotFound an error that indicates that the given resource is not found
ErrNotFound error = errors.New("not found")
)

View file

@ -25,7 +25,6 @@ import (
"net/http"
"github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/api"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/config"
"github.com/dnote/dnote/pkg/server/controllers"
@ -33,67 +32,17 @@ import (
"github.com/dnote/dnote/pkg/server/job"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/routes"
"github.com/dnote/dnote/pkg/server/web"
"github.com/jinzhu/gorm"
"github.com/gobuffalo/packr/v2"
"github.com/pkg/errors"
)
var versionTag = "master"
var port = flag.String("port", "3000", "port to connect to")
var rootBox *packr.Box
var pageDir = flag.String("pageDir", "views", "the path to a directory containing page templates")
var staticDir = flag.String("staticDir", "./static/", "the path to the static directory ")
func init() {
rootBox = packr.New("root", "../../web/public")
}
func mustFind(box *packr.Box, path string) []byte {
b, err := rootBox.Find(path)
if err != nil {
panic(errors.Wrapf(err, "getting file content for %s", path))
}
return b
}
func initWebContext(db *gorm.DB) web.Context {
staticBox := packr.New("static", "../../web/public/static")
return web.Context{
DB: db,
IndexHTML: mustFind(rootBox, "index.html"),
RobotsTxt: mustFind(rootBox, "robots.txt"),
ServiceWorkerJs: mustFind(rootBox, "service-worker.js"),
StaticFileSystem: staticBox,
}
}
func initServer(a app.App) (*http.ServeMux, error) {
apiRouter, err := api.NewRouter(&api.API{App: &a})
if err != nil {
return nil, errors.Wrap(err, "initializing router")
}
webCtx := initWebContext(a.DB)
webHandlers, err := web.Init(webCtx)
if err != nil {
return nil, errors.Wrap(err, "initializing web handlers")
}
mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", apiRouter))
mux.Handle("/static/", webHandlers.GetStatic)
mux.HandleFunc("/service-worker.js", webHandlers.GetServiceWorker)
mux.HandleFunc("/robots.txt", webHandlers.GetRobots)
mux.HandleFunc("/", webHandlers.GetRoot)
return mux, nil
}
func initDB(c config.Config) *gorm.DB {
db, err := gorm.Open("postgres", c.DB.GetConnectionStr())
if err != nil {
@ -152,7 +101,7 @@ func startCmd() {
r := routes.New(&app, rc)
log.Printf("Dnote version %s is running on port %s", versionTag, *port)
log.Printf("dnote version %s is running on port %s", versionTag, *port)
log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%s", cfg.Port), r))
}
@ -161,7 +110,7 @@ func versionCmd() {
}
func rootCmd() {
fmt.Printf(`Dnote Server - A simple personal knowledge base
fmt.Printf(`dnote server - a simple personal knowledge base
Usage:
dnote-server [command]

View file

@ -31,7 +31,8 @@ func NewWebRoutes(app *app.App, c *controllers.Controllers) []Route {
return []Route{
{"GET", "/", Auth(app, http.HandlerFunc(c.Users.New), &AuthParams{RedirectGuestsToLogin: true}), true},
{"GET", "/new", http.HandlerFunc(c.Users.New), true},
{"GET", "/login", http.HandlerFunc(c.Users.New), true},
{"GET", "/login", c.Users.LoginView, true},
{"POST", "/login", http.HandlerFunc(c.Users.Login), true},
}
}

View file

@ -0,0 +1,61 @@
{{define "yield"}}
<div class="auth-page">
<div class="container">
<h1 class="heading">Sign in to Dnote</h1>
<div class="body">
{{template "alert" .}}
<div class="panel">
{{template "loginForm"}}
</div>
</div>
<div class="footer">
<div class="callout">Don&#39;t have an account?</div>
<a href="/register" class="cta">
Create account
</a>
</div>
</div>
</div>
{{end}}
{{define "loginForm"}}
<form action="/login" method="POST">
{{csrfField}}
<div class="input-row">
<label for="email-input" class="label">
Email
<input
tabindex="1"
id="email-input"
name="email"
type="email"
placeholder="you@example.com"
class="form-control"
/>
</label>
</div>
<div class="input-row">
<label for="password-input" class="label">
Password
<a href="/forgot" class="forgot">
Forgot?
</a>
<input
tabindex="2"
id="password-input"
name="password"
type="password"
placeholder="&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;"
class="form-control"
/>
</label>
</div>
<button tabindex="3" type="submit" class="auth-button button button-normal button-stretch button-first">Log In</button>
</form>
{{end}}