package charakterin import ( "database/sql" "errors" "io/ioutil" "log" "net/http" "net/url" "time" "regexp" _ "github.com/lib/pq" ) const ( NoSuchUser = "pq: no_such_user" InvalidPassword = "pq: invalid_password" UsernameTaken = "pq: username_taken" EmailAlreadyRegistered = "pq: email_already_registered" ) // Renderer wird verwendet, um die Routen (bspw. Login-Route) zu rendern. Damit bleibt Charakterin selbst ohne Template. type Renderer interface { // RenderLoginPage zeigt die Login-Seite an. RenderLoginPage(w http.ResponseWriter, data map[string]interface{}) // RenderRegistrationPage zeigt die Registrations-Seite an. RenderRegistrationPage(w http.ResponseWriter, data map[string]interface{}) // RenderUserSettingsPage zeigt die Seite für die Benutzereinstellungen an. RenderUserSettingsPage(w http.ResponseWriter, data map[string]interface{}) } // Charakterin ist das tolle Login- und Accountmanagementsystem. type Charakterin struct { renderer Renderer FallbackRoute string Database *sql.DB } var reEmail, _ = regexp.Compile(`(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,})`) // New erstellt eine neue Instanz von Charakterin. func New(db *sql.DB) *Charakterin { if err := db.Ping(); err != nil { log.Fatalln("no valid database connection supplied:", err) return nil } return &Charakterin{ nil, "/", db, } } // UseRenderer sagt charakterin, welchen Renderer es benutzen soll. func (c *Charakterin) UseRenderer(renderer Renderer) { c.renderer = renderer } // DisplayLoginWithData rendert die Loginseite mit Daten (vorheriger Benutzer, Fehlermeldung) func (c *Charakterin) DisplayLoginWithData(w http.ResponseWriter, r *http.Request, data map[string]interface{}) { if c.IsLoggedIn(r) { http.Redirect(w, r, c.FallbackRoute, 302) return } if c.renderer == nil { log.Println("charakterin: no renderer set") return } if _, ok := data["previous_user"]; !ok { data["previous_user"] = "" } if _, ok := data["error"]; !ok { data["error"] = "" } c.renderer.RenderLoginPage(w, data) } // DisplayLogin zeigt die Route für den Login an, wenn der User nicht bereits eingeloggt ist. func (c *Charakterin) DisplayLogin(w http.ResponseWriter, r *http.Request) { c.DisplayLoginWithData(w, r, make(map[string]interface{})) } // LoginRequest versucht, einen Benutzer mit den gegebenen Daten einzuloggen. func (c *Charakterin) LoginRequest(username, password string) (string, error) { var result string err := c.Database.QueryRow("SELECT * FROM login.new_session($1, $2)", username, password).Scan(&result) if err != nil { return "", err } return result, nil } // Login versucht einen User durch einen Request einzuloggen. func (c *Charakterin) Login(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { return } // POST-Data lesen values, err := readBody(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } username := values.Get("username") session, err := c.LoginRequest(username, values.Get("password")) if err != nil { errStr := err.Error() if errStr == NoSuchUser || errStr == InvalidPassword { log.Printf("invalid login attempt by '%s': %s\n", username, errStr[4:]) data := make(map[string]interface{}) data["previous_user"] = username data["error"] = "Ungültiger Benutzername oder Passwort. Oder Lukas hats mal wieder kaputt gemacht." c.DisplayLoginWithData(w, r, data) return } http.Error(w, errStr, http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "session", Value: session, Expires: time.Now().AddDate(1, 0, 0), }) http.Redirect(w, r, c.FallbackRoute, 302) } // Logout loggt einen Charakter aus. Wird direkt über den Request gehandlet. func (c *Charakterin) Logout(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { http.Redirect(w, r, c.FallbackRoute, 302) return } stmt, err := c.Database.Prepare("DELETE FROM login.sessions WHERE id=$1") if err != nil { http.Error(w, "500", http.StatusInternalServerError) return } result, err := stmt.Exec(cookie.Value) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } if val, err := result.RowsAffected(); err != nil || val == 0 { log.Println("could not remove session", cookie.Value, err) http.Redirect(w, r, c.FallbackRoute, 302) return } http.SetCookie(w, &http.Cookie{ Name: "session", Value: "benis", Expires: time.Now(), MaxAge: 0, }) http.Redirect(w, r, c.FallbackRoute, 302) } // IsLoggedIn überprüft anhand eines Request, ob der User eingeloggt ist. func (c *Charakterin) IsLoggedIn(r *http.Request) bool { cookie, err := r.Cookie("session") if err != nil { return false } var result string err = c.Database.QueryRow(`SELECT login.get_user_by_session($1)`, cookie.Value).Scan(&result) if err != nil { log.Println(err) return false } return true } // DisplayRegistrationWithData rendert die Registration mit Daten (vorheriger Benutzer, Fehlermeldung) func (c *Charakterin) DisplayRegistrationWithData(w http.ResponseWriter, r *http.Request, data map[string]interface{}) { if c.IsLoggedIn(r) { http.Redirect(w, r, c.FallbackRoute, 302) return } if c.renderer == nil { log.Println("charakterin: no renderer set") return } if _, ok := data["previous_user"]; !ok { data["previous_user"] = "" } if _, ok := data["previous_email"]; !ok { data["previous_email"] = "" } if _, ok := data["error"]; !ok { data["error"] = "" } c.renderer.RenderRegistrationPage(w, data) } // SaveUser handlet die neuen Benutzerdaten und speichert sie func (c *Charakterin) SaveUserRoute(w http.ResponseWriter, r *http.Request) { user, err := c.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } values, err := readBody(r) if err != nil { http.Error(w, "400", http.StatusBadRequest) return } displayName := values.Get("display_name") curPass := values.Get("current_password") newPass := values.Get("new_password") if len(curPass) > 0 && len(newPass) > 0 { var success bool err = c.Database.QueryRow("SELECT login.compare_passwords($1, $2)", user.Password, curPass).Scan(&success) if err != nil { http.Error(w, "500", http.StatusInternalServerError) log.Println(err) return } if !success { http.Error(w, "Ungueltiges Passwort.", 400) return } res, err := c.Database.Exec("UPDATE login.users SET password = login.hash_password($1) WHERE id = $2", newPass, user.ID) if err != nil { http.Error(w, "500", http.StatusInternalServerError) log.Println(err) return } if n, _ := res.RowsAffected(); n == 0 { log.Println("could not change password, no rows affected") } } curDsp, err := user.DisplayName.Value() if len(displayName) > 0 || err == nil { var res sql.Result if err == nil && displayName == curDsp { http.Error(w, "name unchanged", 400) return } if len(displayName) == 0 || displayName == user.Name { res, err = c.Database.Exec("UPDATE login.users SET display_name = NULL WHERE id = $1", user.ID) } else { res, err = c.Database.Exec("UPDATE login.users SET display_name = $1 WHERE id = $2", displayName, user.ID) } if err != nil { http.Error(w, "500", http.StatusInternalServerError) log.Println(err) return } if n, _ := res.RowsAffected(); n == 0 { log.Println("could not change display name, no rows affected") } } w.WriteHeader(200) w.Write([]byte("1")) } // DisplayRegistration zeigt die Route für die Registration an, wenn der User nicht bereits eingeloggt ist. func (c *Charakterin) DisplayRegistration(w http.ResponseWriter, r *http.Request) { c.DisplayRegistrationWithData(w, r, make(map[string]interface{})) } // Register versucht einen Benutzer zu registrieren. func (c *Charakterin) Register(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { return } // POST-Data lesen values, err := readBody(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } username := values.Get("username") password := values.Get("password") email := values.Get("email") if len(username) < 3 { data := make(map[string]interface{}) data["error"] = "Username zu kurz, du Hurensohn" data["previous_email"] = email c.DisplayRegistrationWithData(w, r, data) return } if len(password) < 3 { data := make(map[string]interface{}) data["error"] = "Passwort zu kurz, du Hurensohn" data["previous_user"] = username c.DisplayRegistrationWithData(w, r, data) return } if len(email) < 3 || !reEmail.MatchString(email) { data := make(map[string]interface{}) data["error"] = "Fick deine Email" data["previous_email"] = email data["previous_user"] = username c.DisplayRegistrationWithData(w, r, data) return } var result string err = c.Database.QueryRow("SELECT * FROM login.register_user($1, $2, $3)", username, password, email).Scan(&result) if err != nil { errStr := err.Error() if errStr == UsernameTaken { data := make(map[string]interface{}) data["error"] = "Der Benutzername wird bereits verwendet." data["previous_email"] = email c.DisplayRegistrationWithData(w, r, data) return } else if errStr == EmailAlreadyRegistered { data := make(map[string]interface{}) data["error"] = "Diese E-Mail wird bereits verwendet." data["previous_user"] = username c.DisplayRegistrationWithData(w, r, data) return } http.Error(w, errStr, http.StatusInternalServerError) return } log.Printf("user '%s' has been registered.\n", username) if err := c.ConfirmEmail(result); err != nil { log.Println("could not activate user", err) http.Error(w, "user created, could not activate", http.StatusInternalServerError) return } session, err := c.LoginRequest(username, password) if err != nil { log.Println("failed auto-login for", username, err) http.Redirect(w, r, c.FallbackRoute, 302) return } http.SetCookie(w, &http.Cookie{ Name: "session", Value: session, Expires: time.Now().AddDate(1, 0, 0), }) http.Redirect(w, r, c.FallbackRoute, 302) } func (c *Charakterin) GetUserFromRequest(r *http.Request) (*User, error) { cookie, err := r.Cookie("session") if err != nil { return nil, errors.New("no_session_cookie") } var email, name string var displayName sql.NullString var password []byte var id int var lastActivity *time.Time var isActive bool err = c.Database.QueryRow(`SELECT id, email, name, password, display_name, last_activity, is_active FROM login.get_user_by_session($1)`, cookie.Value).Scan(&id, &email, &name, &password, &displayName, &lastActivity, &isActive) if err != nil { return nil, err } user := &User{ id, name, email, password, displayName, lastActivity, isActive, r.Header.Get("User-Agent"), } return user, nil } // GetUserByID gibt den User mit der gegebenen ID zurück func (c *Charakterin) GetUserByID(id int) (*User, error) { user := &User{} err := c.Database.QueryRow(`SELECT id, email, name, password, display_name, last_activity, is_active FROM login.users WHERE id = $1`, id).Scan(&user.ID, &user.EMail, &user.Name, &user.Password, &user.DisplayName, &user.LastActivity, &user.IsActive) if err != nil { return nil, err } return user, nil } // ConfirmEmail konfirmiert die email addresse mit der gegebenen confirm id. func (c *Charakterin) ConfirmEmail(confirmId string) error { var result string err := c.Database.QueryRow(`SELECT * FROM login.confirm_user_email($1)`, confirmId).Scan(&result) if err != nil { return err } return nil } // DisplayUserSettingsWithData zeigt die Benutzereinstellungs-Seite mit den gegebenen Daten an. func (c *Charakterin) DisplayUserSettingsWithData(w http.ResponseWriter, r *http.Request, data map[string]interface{}) { // Überprüfen, ob der User überhaupt eingeloggt ist user, err := c.GetUserFromRequest(r) if err != nil { http.Redirect(w, r, c.FallbackRoute, 302) return } if _, ok := data["error"]; !ok { data["error"] = "" } if _, ok := data["user"]; !ok { data["user"] = user } c.renderer.RenderUserSettingsPage(w, data) } // DisplayUserSettingsWithData zeigt die Benutzereinstellungs-Seite an. func (c *Charakterin) DisplayUserSettings(w http.ResponseWriter, r *http.Request) { c.DisplayUserSettingsWithData(w, r, make(map[string]interface{})) } func readBody(r *http.Request) (url.Values, error) { defer r.Body.Close() data, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } values, err := url.ParseQuery(string(data)) if err != nil { return nil, err } return values, nil }