diff --git a/api/comment_approve_test.go b/api/comment_approve_test.go index 26833ae..e7b04cd 100644 --- a/api/comment_approve_test.go +++ b/api/comment_approve_test.go @@ -8,7 +8,7 @@ import ( func TestCommentApproveBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "") commentHex, _ := commentNew(commenterHex, "example.com", "/path.html", "root", "**foo**", "unapproved", time.Now().UTC()) diff --git a/api/comment_list.go b/api/comment_list.go index 484fcad..3042dba 100644 --- a/api/comment_list.go +++ b/api/comment_list.go @@ -151,5 +151,6 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) { "requireIdentification": d.RequireIdentification, "isFrozen": d.State == "frozen", "isModerator": isModerator, + "configuredOauths": configuredOauths, }) } diff --git a/api/comment_list_test.go b/api/comment_list_test.go index 2ab83c0..437abdc 100644 --- a/api/comment_list_test.go +++ b/api/comment_list_test.go @@ -9,7 +9,7 @@ import ( func TestCommentListBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "http://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "http://example.com/photo.jpg", "google", "") commentNew(commenterHex, "example.com", "/path.html", "root", "**foo**", "approved", time.Now().UTC()) commentNew(commenterHex, "example.com", "/path.html", "root", "**bar**", "approved", time.Now().UTC()) @@ -65,7 +65,7 @@ func TestCommentListEmpty(t *testing.T) { func TestCommentListSelfUnapproved(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "http://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "http://example.com/photo.jpg", "google", "") commentNew(commenterHex, "example.com", "/path.html", "root", "**foo**", "unapproved", time.Now().UTC()) diff --git a/api/comment_new.go b/api/comment_new.go index 88c8edf..647d827 100644 --- a/api/comment_new.go +++ b/api/comment_new.go @@ -116,5 +116,5 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) { return } - writeBody(w, response{"success": true, "commentHex": commentHex}) + writeBody(w, response{"success": true, "commentHex": commentHex, "approved": state == "approved"}) } diff --git a/api/comment_vote_test.go b/api/comment_vote_test.go index 74bd22d..bbb2c66 100644 --- a/api/comment_vote_test.go +++ b/api/comment_vote_test.go @@ -8,9 +8,9 @@ import ( func TestCommentVoteBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - cr0, _ := commenterNew("test1@example.com", "Test1", "undefined", "http://example.com/photo.jpg", "google") - cr1, _ := commenterNew("test2@example.com", "Test2", "undefined", "http://example.com/photo.jpg", "google") - cr2, _ := commenterNew("test3@example.com", "Test3", "undefined", "http://example.com/photo.jpg", "google") + cr0, _ := commenterNew("test1@example.com", "Test1", "undefined", "http://example.com/photo.jpg", "google", "") + cr1, _ := commenterNew("test2@example.com", "Test2", "undefined", "http://example.com/photo.jpg", "google", "") + cr2, _ := commenterNew("test3@example.com", "Test3", "undefined", "http://example.com/photo.jpg", "google", "") c0, _ := commentNew(cr0, "example.com", "/path.html", "root", "**foo**", "approved", time.Now().UTC()) diff --git a/api/commenter_get_test.go b/api/commenter_get_test.go index 3f11951..af0b776 100644 --- a/api/commenter_get_test.go +++ b/api/commenter_get_test.go @@ -7,7 +7,7 @@ import ( func TestCommenterGetByHexBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "") c, err := commenterGetByHex(commenterHex) if err != nil { @@ -33,7 +33,7 @@ func TestCommenterGetByHexEmpty(t *testing.T) { func TestCommenterGetBySession(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "") session, _ := commenterSessionNew() @@ -63,7 +63,7 @@ func TestCommenterGetBySessionEmpty(t *testing.T) { func TestCommenterGetByName(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "") session, _ := commenterSessionNew() diff --git a/api/commenter_login.go b/api/commenter_login.go new file mode 100644 index 0000000..f42229a --- /dev/null +++ b/api/commenter_login.go @@ -0,0 +1,71 @@ +package main + +import ( + "golang.org/x/crypto/bcrypt" + "net/http" + "time" +) + +func commenterLogin(email string, password string) (string, error) { + if email == "" || password == "" { + return "", errorMissingField + } + + statement := ` + SELECT commenterHex, passwordHash + FROM commenters + WHERE email = $1 AND provider = 'commento'; + ` + row := db.QueryRow(statement, email) + + var commenterHex string + var passwordHash string + if err := row.Scan(&commenterHex, &passwordHash); err != nil { + return "", errorInvalidEmailPassword + } + + if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil { + // TODO: is this the only possible error? + return "", errorInvalidEmailPassword + } + + session, err := randomHex(32) + if err != nil { + logger.Errorf("cannot create session hex: %v", err) + return "", errorInternal + } + + statement = ` + INSERT INTO + commenterSessions (session, commenterHex, creationDate) + VALUES ($1, $2, $3 ); + ` + _, err = db.Exec(statement, session, commenterHex, time.Now().UTC()) + if err != nil { + logger.Errorf("cannot insert session token: %v\n", err) + return "", errorInternal + } + + return session, nil +} + +func commenterLoginHandler(w http.ResponseWriter, r *http.Request) { + type request struct { + Email *string `json:"email"` + Password *string `json:"password"` + } + + var x request + if err := unmarshalBody(r, &x); err != nil { + writeBody(w, response{"success": false, "message": err.Error()}) + return + } + + session, err := commenterLogin(*x.Email, *x.Password) + if err != nil { + writeBody(w, response{"success": false, "message": err.Error()}) + return + } + + writeBody(w, response{"success": true, "session": session}) +} diff --git a/api/commenter_login_test.go b/api/commenter_login_test.go new file mode 100644 index 0000000..8d43d03 --- /dev/null +++ b/api/commenter_login_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "testing" +) + +func TestCommenterLoginBasics(t *testing.T) { + failTestOnError(t, setupTestEnv()) + + if _, err := commenterLogin("test@example.com", "hunter2"); err == nil { + t.Errorf("expected error not found when logging in without creating an account") + return + } + + commenterNew("test@example.com", "Test", "undefined", "undefined", "commento", "hunter2") + + if _, err := commenterLogin("test@example.com", "hunter2"); err != nil { + t.Errorf("unexpected error when logging in: %v", err) + return + } + + if _, err := commenterLogin("test@example.com", "h******"); err == nil { + t.Errorf("expected error not found when given wrong password") + return + } + + if session, err := commenterLogin("test@example.com", "hunter2"); session == "" { + t.Errorf("empty session on successful login: %v", err) + return + } +} + +func TestCommenterLoginEmpty(t *testing.T) { + failTestOnError(t, setupTestEnv()) + + if _, err := commenterLogin("test@example.com", ""); err == nil { + t.Errorf("expected error not found when passing empty password") + return + } + + commenterNew("test@example.com", "Test", "undefined", "", "commenter", "hunter2") + + if _, err := commenterLogin("test@example.com", ""); err == nil { + t.Errorf("expected error not found when passing empty password") + return + } +} + +func TestCommenterLoginNonCommento(t *testing.T) { + failTestOnError(t, setupTestEnv()) + + commenterNew("test@example.com", "Test", "undefined", "undefined", "google", "") + + if _, err := commenterLogin("test@example.com", "hunter2"); err == nil { + t.Errorf("expected error not found logging into a non-Commento account") + return + } +} diff --git a/api/commenter_new.go b/api/commenter_new.go index 76e3d14..9e56d48 100644 --- a/api/commenter_new.go +++ b/api/commenter_new.go @@ -1,28 +1,74 @@ package main import ( + "golang.org/x/crypto/bcrypt" + "net/http" "time" ) -func commenterNew(email string, name string, link string, photo string, provider string) (string, error) { +func commenterNew(email string, name string, link string, photo string, provider string, password string) (string, error) { if email == "" || name == "" || link == "" || photo == "" || provider == "" { return "", errorMissingField } + if provider == "commento" && password == "" { + return "", errorMissingField + } + + if _, err := commenterGetByEmail(provider, email); err == nil { + return "", errorEmailAlreadyExists + } + commenterHex, err := randomHex(32) if err != nil { return "", errorInternal } + passwordHash := []byte{} + if (password != "") { + passwordHash, err = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + logger.Errorf("cannot generate hash from password: %v\n", err) + return "", errorInternal + } + } + statement := ` INSERT INTO - commenters (commenterHex, email, name, link, photo, provider, joinDate) - VALUES ($1, $2, $3, $4, $5, $6, $7 ); + commenters (commenterHex, email, name, link, photo, provider, passwordHash, joinDate) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8 ); ` - _, err = db.Exec(statement, commenterHex, email, name, link, photo, provider, time.Now().UTC()) + _, err = db.Exec(statement, commenterHex, email, name, link, photo, provider, string(passwordHash), time.Now().UTC()) if err != nil { + logger.Errorf("cannot insert commenter: %v", err) return "", errorInternal } return commenterHex, nil } + + +func commenterNewHandler(w http.ResponseWriter, r *http.Request) { + type request struct { + Email *string `json:"email"` + Name *string `json:"name"` + Website *string `json:"website"` + Password *string `json:"password"` + } + + var x request + if err := unmarshalBody(r, &x); err != nil { + writeBody(w, response{"success": false, "message": err.Error()}) + return + } + + // TODO: add gravatar? + // TODO: email confirmation if provider = commento? + // TODO: email confirmation if provider = commento? + if _, err := commenterNew(*x.Email, *x.Name, *x.Website, "undefined", "commento", *x.Password); err != nil { + writeBody(w, response{"success": false, "message": err.Error()}) + return + } + + writeBody(w, response{"success": true, "confirmEmail": smtpConfigured}) +} diff --git a/api/commenter_new_test.go b/api/commenter_new_test.go index e40b947..e7fdda8 100644 --- a/api/commenter_new_test.go +++ b/api/commenter_new_test.go @@ -7,7 +7,7 @@ import ( func TestCommenterNewBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - if _, err := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google"); err != nil { + if _, err := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", ""); err != nil { t.Errorf("unexpected error creating new commenter: %v", err) return } @@ -16,13 +16,22 @@ func TestCommenterNewBasics(t *testing.T) { func TestCommenterNewEmpty(t *testing.T) { failTestOnError(t, setupTestEnv()) - if _, err := commenterNew("", "Test", "undefined", "https://example.com/photo.jpg", "google"); err == nil { + if _, err := commenterNew("", "Test", "undefined", "https://example.com/photo.jpg", "google", ""); err == nil { t.Errorf("expected error not found creating new commenter with empty email") return } - if _, err := commenterNew("", "", "", "", ""); err == nil { + if _, err := commenterNew("", "", "", "", "", ""); err == nil { t.Errorf("expected error not found creating new commenter with empty everything") return } } + +func TestCommenterNewCommento(t *testing.T) { + failTestOnError(t, setupTestEnv()) + + if _, err := commenterNew("test@example.com", "Test", "undefined", "", "commento", ""); err == nil { + t.Errorf("expected error not found creating new commento account with empty password") + return + } +} diff --git a/api/commenter_session_get_test.go b/api/commenter_session_get_test.go index 9643593..7523901 100644 --- a/api/commenter_session_get_test.go +++ b/api/commenter_session_get_test.go @@ -7,7 +7,7 @@ import ( func TestCommenterSessionGetBasics(t *testing.T) { failTestOnError(t, setupTestEnv()) - commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google") + commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "") session, _ := commenterSessionNew() diff --git a/api/config.go b/api/config.go index 62c6762..58c0ea5 100644 --- a/api/config.go +++ b/api/config.go @@ -29,8 +29,8 @@ func parseConfig() error { "SMTP_PORT": "", "SMTP_FROM_ADDRESS": "", - "OAUTH_GOOGLE_KEY": "", - "OAUTH_GOOGLE_SECRET": "", + "GOOGLE_KEY": "", + "GOOGLE_SECRET": "", } for key, value := range defaults { diff --git a/api/oauth.go b/api/oauth.go index c9c7fc3..bb20e14 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -2,7 +2,11 @@ package main import () +var configuredOauths []string + func oauthConfigure() error { + configuredOauths = []string{} + if err := googleOauthConfigure(); err != nil { return err } diff --git a/api/oauth_google.go b/api/oauth_google.go index 026f848..f519614 100644 --- a/api/oauth_google.go +++ b/api/oauth_google.go @@ -37,5 +37,7 @@ func googleOauthConfigure() error { Endpoint: google.Endpoint, } + configuredOauths = append(configuredOauths, "google"); + return nil } diff --git a/api/oauth_google_callback.go b/api/oauth_google_callback.go index 2f4acba..e2b9397 100644 --- a/api/oauth_google_callback.go +++ b/api/oauth_google_callback.go @@ -64,7 +64,7 @@ func googleCallbackHandler(w http.ResponseWriter, r *http.Request) { link = "undefined" } - commenterHex, err = commenterNew(email, user["name"].(string), link, user["picture"].(string), "google") + commenterHex, err = commenterNew(email, user["name"].(string), link, user["picture"].(string), "google", "") if err != nil { fmt.Fprintf(w, "Error: %s", err.Error()) return diff --git a/api/router_api.go b/api/router_api.go index 459bf40..57c12af 100644 --- a/api/router_api.go +++ b/api/router_api.go @@ -21,6 +21,8 @@ func initAPIRouter(router *mux.Router) error { router.HandleFunc("/api/domain/statistics", domainStatisticsHandler).Methods("POST") router.HandleFunc("/api/commenter/session/new", commenterSessionNewHandler).Methods("GET") + router.HandleFunc("/api/commenter/new", commenterNewHandler).Methods("POST") + router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST") router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST") router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET") diff --git a/db/20180610215858-commenter-password.sql b/db/20180610215858-commenter-password.sql new file mode 100644 index 0000000..5e9b5da --- /dev/null +++ b/db/20180610215858-commenter-password.sql @@ -0,0 +1,2 @@ +ALTER TABLE commenters + ADD passwordHash TEXT NOT NULL DEFAULT ''; diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 51f7f69..bf245ba 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -153,10 +153,10 @@
Moderators have the power to approve and delete comments. To make someone a moderator, add their email address down below. Once added, shiny new moderation buttons will appear on each comment for that person on each page on this domain.
-
-
- - +
+
+ +
@@ -244,10 +244,10 @@

-