// Package web provides the login-gated HTTP surface for the application.
package web
import (
"errors"
"fmt"
"html/template"
"net/http"
"time"
"webfilebrowser/internal/auth"
)
const (
loginPath = "/login"
logoutPath = "/logout"
changePasswordPath = "/change-password"
sessionCookieName = "webfilebrowser_session"
)
var (
loginTemplate = template.Must(template.New("login").Parse(`
Sign in
Sign in
{{if .Error}}{{.Error}}
{{end}}
`))
changePasswordTemplate = template.Must(template.New("change-password").Parse(`
Change password
Change your password
You must choose a new password before continuing.
{{if .Error}}{{.Error}}
{{end}}
`))
)
// New returns an HTTP handler that requires a valid server-side session for
// every route other than the login endpoint.
func New(store *auth.Store) http.Handler {
return &application{store: store}
}
type application struct {
store *auth.Store
}
func (app *application) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path == loginPath {
app.handleLogin(writer, request)
return
}
user, token, ok := app.currentUser(writer, request)
if !ok {
return
}
if request.URL.Path == logoutPath {
app.handleLogout(writer, request, token)
return
}
if user.MustChangePassword {
if request.URL.Path != changePasswordPath {
http.Redirect(writer, request, changePasswordPath, http.StatusSeeOther)
return
}
app.handleChangePassword(writer, request, user)
return
}
if request.URL.Path == changePasswordPath {
http.Redirect(writer, request, "/", http.StatusSeeOther)
return
}
if request.URL.Path == "/" {
app.handleHome(writer, request, user)
return
}
http.NotFound(writer, request)
}
func (app *application) handleLogin(writer http.ResponseWriter, request *http.Request) {
switch request.Method {
case http.MethodGet:
renderLogin(writer, http.StatusOK, "")
case http.MethodPost:
if err := request.ParseForm(); err != nil {
renderLogin(writer, http.StatusBadRequest, "Unable to read sign-in details.")
return
}
user, err := app.store.Authenticate(request.FormValue("email"), request.FormValue("password"))
if err != nil {
renderLogin(writer, http.StatusUnauthorized, "Invalid email or password.")
return
}
token, err := app.store.CreateSession(user.ID)
if err != nil {
http.Error(writer, "Unable to create session.", http.StatusInternalServerError)
return
}
app.setSessionCookie(writer, token)
destination := "/"
if user.MustChangePassword {
destination = changePasswordPath
}
http.Redirect(writer, request, destination, http.StatusSeeOther)
default:
writer.Header().Set("Allow", http.MethodGet+", "+http.MethodPost)
http.Error(writer, "Method not allowed.", http.StatusMethodNotAllowed)
}
}
func (app *application) handleChangePassword(writer http.ResponseWriter, request *http.Request, user auth.User) {
switch request.Method {
case http.MethodGet:
renderChangePassword(writer, http.StatusOK, "")
case http.MethodPost:
if err := request.ParseForm(); err != nil {
renderChangePassword(writer, http.StatusBadRequest, "Unable to read password details.")
return
}
err := app.store.ChangePassword(user.ID, request.FormValue("current_password"), request.FormValue("new_password"))
if errors.Is(err, auth.ErrPasswordUnchanged) {
renderChangePassword(writer, http.StatusBadRequest, "Your new password must differ from the current password.")
return
}
if errors.Is(err, auth.ErrInvalidCredentials) {
renderChangePassword(writer, http.StatusBadRequest, "Your current password is incorrect.")
return
}
if err != nil {
http.Error(writer, "Unable to change password.", http.StatusInternalServerError)
return
}
http.Redirect(writer, request, "/", http.StatusSeeOther)
default:
writer.Header().Set("Allow", http.MethodGet+", "+http.MethodPost)
http.Error(writer, "Method not allowed.", http.StatusMethodNotAllowed)
}
}
func (app *application) handleLogout(writer http.ResponseWriter, request *http.Request, token string) {
if request.Method != http.MethodPost {
writer.Header().Set("Allow", http.MethodPost)
http.Error(writer, "Method not allowed.", http.StatusMethodNotAllowed)
return
}
if err := app.store.DeleteSession(token); err != nil {
http.Error(writer, "Unable to end session.", http.StatusInternalServerError)
return
}
http.SetCookie(writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(writer, request, loginPath, http.StatusSeeOther)
}
func (app *application) handleHome(writer http.ResponseWriter, _ *http.Request, user auth.User) {
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = fmt.Fprintf(writer, "webFileBrowserwebFileBrowser
File browser is being prepared.
Signed in as %s.
", template.HTMLEscapeString(user.Email))
}
func (app *application) currentUser(writer http.ResponseWriter, request *http.Request) (auth.User, string, bool) {
cookie, err := request.Cookie(sessionCookieName)
if err != nil {
http.Redirect(writer, request, loginPath, http.StatusSeeOther)
return auth.User{}, "", false
}
user, err := app.store.LookupSession(cookie.Value)
if err != nil {
app.clearSessionCookie(writer)
http.Redirect(writer, request, loginPath, http.StatusSeeOther)
return auth.User{}, "", false
}
app.setSessionCookie(writer, cookie.Value)
return user, cookie.Value, true
}
func (app *application) setSessionCookie(writer http.ResponseWriter, token string) {
http.SetCookie(writer, &http.Cookie{
Name: sessionCookieName,
Value: token,
Path: "/",
Expires: time.Now().Add(app.store.SessionLifetime()),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
}
func (app *application) clearSessionCookie(writer http.ResponseWriter) {
http.SetCookie(writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
}
func renderLogin(writer http.ResponseWriter, status int, message string) {
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(status)
_ = loginTemplate.Execute(writer, struct{ Error string }{Error: message})
}
func renderChangePassword(writer http.ResponseWriter, status int, message string) {
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(status)
_ = changePasswordTemplate.Execute(writer, struct{ Error string }{Error: message})
}