some-automoderation/routes/login_page.go

380 lines
12 KiB
Go

package routes
import (
"context"
"embed"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"slices"
"strconv"
"time"
"golang.org/x/crypto/bcrypt"
"sunshine.industries/some-automoderation/rooms"
"sunshine.industries/some-automoderation/sessions"
)
type LoginSectionData struct {
IsRoomExisting bool
RoomName string
}
type loginPageData struct {
LoginSection LoginSectionData
}
// function to register all http routes for servicing auth pages and logic
func registerLoginRoutes(
templateFs *embed.FS,
sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager,
) {
// login page
http.HandleFunc(loginPath, func(w http.ResponseWriter, r *http.Request) {
renderLoginPage(w, "", false)
})
http.HandleFunc("/login/join", joinRoomHandler(templateFs, sessionSM, roomsM))
http.HandleFunc("/login/create", createRoomHandler(templateFs, sessionSM, roomsM))
http.HandleFunc("/login/room-name-check", checkRoomName(templateFs, roomsM))
http.Handle("/logout", authedPageMiddleware(sessionSM,
http.HandlerFunc(logoutRoute(sessionSM, roomsM))))
}
const authCookieName = "auth"
const loginPath = "/login"
type contextKey string
func getContextSession(ctx context.Context) (sessions.SessionData, bool) {
val := ctx.Value(contextKey("session"))
if val == nil {
return sessions.SessionData{}, false
} else {
return ctx.Value(contextKey("session")).(sessions.SessionData), true
}
}
var ErrAuthCookieMissing = errors.New("auth cookie is missing")
var ErrAuthCookieValueInvalid = errors.New("auth cookie value is not decoeable")
var ErrAuthSessionNotFound = errors.New("session not found")
func getRequestSession(r *http.Request,
sessionsM sessions.SessionManagement) (sessions.SessionData, error) {
sessionCookie, err := r.Cookie(authCookieName)
if err != nil {
return sessions.SessionData{}, ErrAuthCookieMissing
}
sessionId, err := strconv.Atoi(sessionCookie.Value)
if err != nil {
return sessions.SessionData{}, ErrAuthCookieValueInvalid
}
session := sessionsM.Get(sessionId)
if session == (sessions.SessionData{}) {
return sessions.SessionData{}, ErrAuthSessionNotFound
}
return session, nil
}
// checks sessionId from cookie
// when non-zero session found - pass to next http.Hander
// when no session available - render same as login page and redirect to /
func authedPageMiddleware(
sessionsM sessions.SessionManagement, next http.Handler,
) http.Handler {
returnNoAccess := func(w http.ResponseWriter, r *http.Request) {
log.Printf("auth middle > restricting access to %s", r.URL.Path)
w.Header().Add("HX-Redirect", "/")
// TODO i suppose i could add error?
return
}
rerturnSuccess := func(w http.ResponseWriter, r *http.Request, session sessions.SessionData) {
ctx := context.WithValue(r.Context(), contextKey("session"), session)
log.Printf("auth middle > allowing access to %s for %+v", r.URL.Path, session)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := getRequestSession(r, sessionsM)
if err != nil {
returnNoAccess(w, r)
return
} else {
rerturnSuccess(w, r, session)
return
}
})
}
func renderLoginPage(w http.ResponseWriter, roomName string, isRoomExisting bool) {
baseFile := "templates/base.gohtml"
pageTempl := "templates/login.gohtml"
loginSecion := "templates/login-section.gohtml"
tmpl := template.Must(template.ParseFS(templateFs, pageTempl, baseFile, loginSecion))
title := "Some Automoderation: Join room or create one"
if roomName != "" {
title = fmt.Sprintf("Some Automoderation: join room '%s'", roomName)
}
data := pageData{
Base: baseData{
Title: title,
},
Content: loginPageData{
LoginSection: LoginSectionData{
IsRoomExisting: isRoomExisting,
RoomName: roomName,
},
},
}
err := tmpl.ExecuteTemplate(w, "full-page", data)
if err != nil {
log.Printf("my error in executing template, huh\n %s", err)
}
}
func createRoomHandler(templateFs *embed.FS,
sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
// TODO return error notice somehow
w.WriteHeader(http.StatusBadRequest)
}
roomName := r.PostFormValue("roomName")
_, exists, _ := roomsM.Get(roomName)
if exists {
// TODO return anouther error notice
log.Printf("error, room name occupied %s", roomName)
return
}
personPassHash, err := hashPassword(r.PostFormValue("personalPassword"))
if err != nil {
log.Printf("error, room name occupied %s", roomName)
return
}
person := rooms.Person{
Id: rooms.RandomPersonId(),
Name: r.PostFormValue("personalName"),
PasswordHash: personPassHash,
}
roomPassHash, err := hashPassword(r.PostFormValue("roomPassword"))
newRoom := rooms.Room{
Name: roomName,
PasswordHash: roomPassHash,
AdminIds: []rooms.PersonId{person.Id},
Paricipants: []rooms.PersonId{person.Id},
AllKnownPeople: map[rooms.PersonId]rooms.Person{
person.Id: person},
}
err = roomsM.Save(newRoom)
if err != nil {
log.Printf("what am i to do? error saving room %s", err)
// todo return error notice somehow
}
newSession, err := sessionSM.Save(r.Context(), newRoom.Name, person.Id)
if err != nil {
log.Printf("what am i to do? error saving session %s", err)
// todo return error notice somehow
}
http.SetCookie(w, &http.Cookie{
Name: authCookieName,
Value: fmt.Sprint(newSession.SessionId),
Secure: true,
HttpOnly: true,
Path: "/",
})
w.Header().Add("HX-Redirect", fmt.Sprintf("/room/%s", newRoom.Name))
}
}
// checking whether the room name already exists
// toggle button between Join or Create
func checkRoomName(templateFs *embed.FS,
roomsM rooms.RoomManager,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
}
roomName := r.PostFormValue("roomName")
_, isFound, err := roomsM.Get(roomName)
if err != nil {
log.Printf("/login/room-name-check error finding room %s\n", err)
}
templFile := "templates/login-section.gohtml"
tmpl := template.Must(template.ParseFS(templateFs, templFile))
err = tmpl.ExecuteTemplate(w, "formButton", isFound)
}
}
func joinRoomHandler(templateFs *embed.FS,
sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
}
roomName := r.PostFormValue("roomName")
roomPass := r.PostFormValue("roomPassword")
personName := r.PostFormValue("personalName")
personPass := r.PostFormValue("personalPassword")
// a) get room data
room, _, err := roomsM.Get(roomName)
if err != nil {
log.Printf("/login/join error getting room %s", roomName)
w.WriteHeader(http.StatusBadRequest)
// TODO render error to be put in error place
return // no such room
} else {
log.Printf("/login/submit found room %+v", room)
}
// b) check if room password OK
if !isPasswordCorrect(roomPass, room.PasswordHash) {
log.Printf("/login/join bad room pass for %+v", room)
w.WriteHeader(http.StatusForbidden)
// TODO render error to be put in error place
return // bad room password
}
var person rooms.Person
for _, participant := range room.AllKnownPeople {
if participant.Name == personName {
person = participant
}
}
// c) check if such person exists,
// knownPerson, found :=
// check the password
if (person != rooms.Person{}) && !isPasswordCorrect(personPass, person.PasswordHash) {
log.Printf("/login/join bad person pass for %+s", person.Name)
w.WriteHeader(http.StatusForbidden)
// TODO render error to be put in error place
return // bad person password
}
// person joining for thethe first time
if (person == rooms.Person{}) {
log.Printf("/login/join room pass correct, new person joins")
// creating a new person with provided password hash
personPassHash, err := hashPassword(personPass)
person = rooms.Person{
Name: personName,
PasswordHash: personPassHash,
Id: rooms.RandomPersonId(),
}
err = roomsM.Update(r.Context(), room.Name, func(fromRoom rooms.Room) (toRoom rooms.Room) {
log.Printf("/login/join about to modify room %+v", fromRoom)
toRoom = fromRoom
toRoom.AllKnownPeople[person.Id] = person
log.Printf("/login/join will save %+v", toRoom)
return toRoom
})
if err != nil {
log.Printf("/login/join problem adding person to room %+v", person.Name)
w.WriteHeader(http.StatusInternalServerError)
// TODO render error to be put in error place
// with message try again
return // error adding New person
}
}
// TODO handle context and cancells, with separate function that writeds new updated room
// now we have room and person, can create a session
// and we've checked password
err = roomsM.Update(r.Context(), room.Name, func(fromRoom rooms.Room) (toRoom rooms.Room) {
toRoom = fromRoom
if !slices.Contains(toRoom.Paricipants, person.Id) {
// consequtive login from additional devices
toRoom.Paricipants = append(toRoom.Paricipants, person.Id)
}
return toRoom
})
if err != nil {
log.Printf("/login/join problem sitting joining person at a table %+v", person.Name)
w.WriteHeader(http.StatusInternalServerError)
// TODO render error to be put in error place
// with message try again
return // error sitting a new person
}
newSession, err := sessionSM.Save(r.Context(), room.Name, person.Id)
if err != nil {
log.Printf("/login/submit > error saving session %s", err)
}
http.SetCookie(w, &http.Cookie{
Name: authCookieName,
Value: fmt.Sprint(newSession.SessionId),
Secure: true,
HttpOnly: true,
Path: "/",
})
log.Printf("is is %+v. room things %s & %s, personal things %s and %s. \n found room %+v",
newSession, roomName, roomPass, personName, personPass, room,
)
// TODO render what? index page with some data passed?
// or, what? i could just redirect to / for now
w.Header().Add("HX-Redirect", fmt.Sprintf("/room/%s", newSession.RoomId))
}
}
func logoutRoute(sessionSM sessions.SessionManagement,
roomsM rooms.RoomManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, found := getContextSession(r.Context())
if !found {
log.Printf("/logout session not found, it's ok.")
// though this is under middleware for now, should be impossible
// TODO return error i guess
w.Header().Add("HX-Redirect", "/")
return
}
http.SetCookie(w, &http.Cookie{
Name: authCookieName,
Expires: time.Now().Add(-time.Hour),
Value: "",
Secure: true,
HttpOnly: true,
Path: "/",
})
err := sessionSM.Remove(r.Context(), session.SessionId)
if err != nil {
log.Printf("/logout error deleting session: %s", err)
}
err = roomsM.Update(r.Context(), session.RoomId, func(fromRoom rooms.Room) (toRoom rooms.Room) {
toRoom = fromRoom
toRoom.PersonToStandUpFromTable(session.PersonId)
return toRoom
})
if err != nil {
log.Printf("/logout error removing person from table: %s", err)
}
log.Printf("/logout deleting session %+v", session)
w.Header().Add("HX-Redirect", "/")
}
}
func isPasswordCorrect(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func hashPassword(password string) (string, error) {
hashBytes, err := bcrypt.GenerateFromPassword([]byte(password), 0)
if err != nil {
return "", err
}
return string(hashBytes), nil
}