72 Commits

Author SHA1 Message Date
Adhityaa Chandrasekar
504d1bf866 release: v1.7.0 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
a4387b62ec common-main.scss: style all elements with Source Sans 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
1cce90dcf2 commento-login.scss: remove faux shadow from login box 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
8820dcd59e commento.js: remove debug console.log 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
638f7ba197 comment.go: omit state if empty 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
e1effd2a45 comment_list.go: check for moderator status conditionally 2019-05-01 20:39:49 -04:00
Adhityaa Chandrasekar
a9c48a8394 domain_new.go: reject domains with / 2019-05-01 20:39:48 -04:00
Adhityaa Chandrasekar
feeda79923 comment_list.go: refactor SQL statements 2019-05-01 18:45:14 -04:00
Adhityaa Chandrasekar
9d4ed4ca9f sigint.go: close DB connection before exit
Closes https://gitlab.com/commento/commento/issues/88
2019-05-01 18:39:40 -04:00
Adhityaa Chandrasekar
0b37b33530 smtp_configure.go: allow empty username/password
Closes https://gitlab.com/commento/commento/issues/126
2019-05-01 18:34:37 -04:00
Adhityaa Chandrasekar
b67d2ba58c commento.js: run nameWidthFix after elements are visible 2019-05-01 18:32:39 -04:00
Adhityaa Chandrasekar
5b6d31ce31 frontend: add option to not use Source Sans Pro
Closes https://gitlab.com/commento/commento/issues/136
2019-05-01 18:32:07 -04:00
Adhityaa Chandrasekar
409af7f205 autoserve: add option to serve prod 2019-05-01 18:17:57 -04:00
Adhityaa Chandrasekar
060520bd7f commento-common.scss: make buttons width unspecified 2019-05-01 18:17:49 -04:00
Adhityaa Chandrasekar
e396e043c6 source-sans.scss: use font-swapping 2019-05-01 17:52:02 -04:00
Adhityaa Chandrasekar
4a189fc698 frontend: add light font, remove external fonts
Partially closes https://gitlab.com/commento/commento/issues/136
2019-05-01 17:51:24 -04:00
Adhityaa Chandrasekar
cac1cfa84a main.go: add cron to auto cleanup SSO tokens 2019-04-20 23:39:09 -04:00
Adhityaa Chandrasekar
6317b384d9 sso: expire tokens after usage 2019-04-20 23:39:08 -04:00
Adhityaa Chandrasekar
fa2ccfe42e frontend: add markdown help 2019-04-20 22:04:56 -04:00
Adhityaa Chandrasekar
30772ec720 commento.js: do not set href if link is undefined
Fixes https://gitlab.com/commento/commento/issues/156
2019-04-20 21:10:08 -04:00
Adhityaa Chandrasekar
eec10491d6 dashboard-main.scss: remove theme css 2019-04-20 21:10:08 -04:00
Adhityaa Chandrasekar
e46f9cf9e7 version.go, footer.html: display version
Closes https://gitlab.com/commento/commento/issues/122
2019-04-20 21:10:08 -04:00
Adhityaa Chandrasekar
1d1cd46c2b frontend, api, db: add single sign-on
Closes https://gitlab.com/commento/commento/issues/90
2019-04-20 21:10:07 -04:00
Adhityaa Chandrasekar
536ec14b93 dashboard-domain.js: perform callback only if update successful 2019-04-20 20:33:50 -04:00
Adhityaa Chandrasekar
45c6361805 dashboard.html: move email schedule into general settings 2019-04-19 19:12:44 -04:00
Adhityaa Chandrasekar
a455ff54bc frontend, api: allow disabling login methods individually 2019-04-19 19:03:34 -04:00
Adhityaa Chandrasekar
0e54739980 commento.js: use a more stable sort in comments 2019-04-13 21:45:04 -04:00
Adhityaa Chandrasekar
7e9b3e5b26 commento.js: hide error div if no error reported 2019-04-13 21:34:27 -04:00
Adhityaa Chandrasekar
86393ad9ab commento.js: fix disappearing logout button
Fixes https://gitlab.com/commento/commento/issues/135
2019-04-13 21:29:06 -04:00
Adhityaa Chandrasekar
672863b48f commento.js: use question mark for anonymous comments 2019-04-13 21:18:28 -04:00
Adhityaa Chandrasekar
97f17d32ee commento.js: use black avatar for anonymous comments 2019-04-13 21:14:49 -04:00
Adhityaa Chandrasekar
be10baf971 commento.js: remove event listeners before adding new ones 2019-04-13 21:11:00 -04:00
Adhityaa Chandrasekar
9607c15c2b frontend, api: add ability to clear comments 2019-04-13 21:02:40 -04:00
Adhityaa Chandrasekar
65ea597c08 dashboard: use solid backgrounds and boxes 2019-04-13 20:50:53 -04:00
Adhityaa Chandrasekar
850dfc9712 dashboard-main.scss: remove subscription-nag CSS 2019-04-12 19:39:16 -04:00
MacOSO
3c9ba43ad1 commento-oauth.scss: fix alignment of social login buttons
In Microsoft Edge social login buttons wasn't be aligned.

Closes https://gitlab.com/commento/commento/issues/150
2019-04-10 05:15:45 +00:00
Johannes Zellner
b4790397c9 frontend: Make signup/login/passwordreset/forgot forms submittable
Fixes #117
2019-03-24 11:52:55 +01:00
Adhityaa Chandrasekar
9d6955b81e commento.scss: set 100% width for commento-root 2019-03-20 14:07:46 -04:00
Adhityaa Chandrasekar
5ffdf9988a commento.js: use commento namespace in wrapping 2019-03-19 01:23:40 -04:00
Adhityaa Chandrasekar
5f1d46c7b2 comment_count_test.go: use array of strings 2019-03-10 09:23:34 -04:00
Adhityaa Chandrasekar
a2c8a73d3e utils_html.go: return empty string if title is empty 2019-03-10 09:23:24 -04:00
Adhityaa Chandrasekar
4945e53553 commento.js: move name width fix and load hash to post-render 2019-03-02 15:24:16 -05:00
Adhityaa Chandrasekar
88d4f8afcf commento.js: use scrollIntoView for #commento 2019-03-02 15:17:00 -05:00
Adhityaa Chandrasekar
15b1640f89 count.js: add comment count display JS 2019-03-02 15:14:42 -05:00
Adhityaa Chandrasekar
216016a4be commento-card.scss: add line-height to name badges 2019-03-02 14:35:55 -05:00
Adhityaa Chandrasekar
a7cd8066f8 commento.js: undo changes on failed vote 2019-03-02 14:30:23 -05:00
Adhityaa Chandrasekar
295318e6a6 commento.js: use full timestamp in timeago title 2019-03-02 14:30:12 -05:00
Adhityaa Chandrasekar
d26b6f6e9f commento.js: load CSS async 2019-03-02 14:09:29 -05:00
Adhityaa Chandrasekar
c8a2ece0d6 commento.js: remove logo SVG from footer 2019-03-02 14:09:11 -05:00
Adhityaa Chandrasekar
e9ba79974b commento.js: remove vote button onclick listeners before reset 2019-03-02 13:28:08 -05:00
Adhityaa Chandrasekar
beb54035cf .gitlab-ci.yml: use tar.gz in release binaries 2019-02-23 16:18:36 -05:00
Adhityaa Chandrasekar
1ccc95fae4 commento-input.scss: use pre-wrap for textarea 2019-02-23 12:08:38 -05:00
Adhityaa Chandrasekar
fa3fa39696 source-sans.scss: fix cyrillic fonts typo 2019-02-23 11:26:14 -05:00
Adhityaa Chandrasekar
b9bf9e360a oauth.go: re-arrange oauth providers 2019-02-23 11:23:41 -05:00
Adhityaa Chandrasekar
ecbb505c97 commento.scss: remove all unset 2019-02-23 11:23:41 -05:00
Adhityaa Chandrasekar
789a58bd7a api, frontend: add moderator tag to mods in comments 2019-02-22 22:57:35 -05:00
Adhityaa Chandrasekar
c30da607cb oauth_twitter_callback.go: add better error handling 2019-02-22 22:27:53 -05:00
Adhityaa Chandrasekar
be197f2b69 frontend: use pointer for names in card 2019-02-22 22:24:46 -05:00
Adhityaa Chandrasekar
d4b466b04f api: mirror user photos for better privacy 2019-02-22 22:23:27 -05:00
Adhityaa Chandrasekar
95093326e0 oauth_github_callback.go: add better error handling 2019-02-22 22:23:27 -05:00
Adhityaa Chandrasekar
3e5c1c2656 oauth: add gitlab 2019-02-22 22:23:27 -05:00
Adhityaa Chandrasekar
c07f3e8b9f oauth: add twitter 2019-02-22 22:23:26 -05:00
Adhityaa Chandrasekar
d367ac8391 email-notification.txt: remove background on buttons 2019-02-22 18:31:28 -05:00
Adhityaa Chandrasekar
0609ef0e27 commento-mod-toolss.scss: inline display mod buttons 2019-02-22 18:24:39 -05:00
Adhityaa Chandrasekar
adb87d7029 commento.js: use commento namespace for allShow 2019-02-22 18:22:49 -05:00
Adhityaa Chandrasekar
23bec48ebb commento.js: use commenterToken from args
Fixes https://gitlab.com/commento/commento/issues/114
2019-02-22 18:20:14 -05:00
Adhityaa Chandrasekar
685f3a3a58 router_static.go: add charset for html content 2019-02-20 11:07:29 -05:00
Adhityaa Chandrasekar
f4489c9921 commento.js: compute mobileView only once 2019-02-20 10:55:51 -05:00
Adhityaa Chandrasekar
352c93bf88 commento-oauth.scss: explicitly define button width 2019-02-20 10:50:52 -05:00
Adhityaa Chandrasekar
27caa60e0c commento.scss: unset previously defined CSS 2019-02-20 10:44:53 -05:00
Adhityaa Chandrasekar
e0f188909f release: v1.6.2 2019-02-19 00:12:28 -05:00
Adhityaa Chandrasekar
0b78e9e70c owner_reset_password.go: use ownerHex in SELECT 2019-02-19 00:11:26 -05:00
87 changed files with 2178 additions and 521 deletions

View File

@@ -50,8 +50,8 @@ aws-upload-tags:
- export PATH=$PATH:/go/bin
- cd /go/src/$CI_PROJECT_NAME
- make prod
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tgz .
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tgz s3://commento-release/
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tar.gz .
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tar.gz s3://commento-release/
build-docker:
stage: build-docker

13
api/Gopkg.lock generated
View File

@@ -25,6 +25,14 @@
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:d03d0fae6a7a80e89c540787a69ab6e0d3b773fdb3303c0b3d96a15490c6ef32"
name = "github.com/gomodule/oauth1"
packages = ["oauth"]
pruneopts = "UT"
revision = "9a59ed3b0a84f454c260f2f8f82918223fc5630f"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
@@ -116,11 +124,12 @@
[[projects]]
branch = "master"
digest = "1:82e6e4dc5ab71680d89684e4649be630fdeeaf81feb8e88e4a56273a0cd4d966"
digest = "1:341ceeee37101c62dae441691406bf4ecc71bbeb7b424417879fe88d9f88f487"
name = "golang.org/x/oauth2"
packages = [
".",
"github",
"gitlab",
"google",
"internal",
"jws",
@@ -153,6 +162,7 @@
analyzer-version = 1
input-imports = [
"github.com/adtac/go-akismet/akismet",
"github.com/gomodule/oauth1/oauth",
"github.com/gorilla/handlers",
"github.com/gorilla/mux",
"github.com/lib/pq",
@@ -164,6 +174,7 @@
"golang.org/x/net/html",
"golang.org/x/oauth2",
"golang.org/x/oauth2/github",
"golang.org/x/oauth2/gitlab",
"golang.org/x/oauth2/google",
]
solver-name = "gps-cdcl"

View File

@@ -13,7 +13,7 @@ type comment struct {
Html string `json:"html"`
ParentHex string `json:"parentHex"`
Score int `json:"score"`
State string `json:"state"`
State string `json:"state,omitempty"`
CreationDate time.Time `json:"creationDate"`
Direction int `json:"direction"`
}

View File

@@ -1,27 +1,51 @@
package main
import (
"github.com/lib/pq"
"net/http"
)
func commentCount(domain string, path string) (int, error) {
// path can be empty
func commentCount(domain string, paths []string) (map[string]int, error) {
commentCounts := map[string]int{}
if domain == "" {
return 0, errorMissingField
return nil, errorMissingField
}
p, err := pageGet(domain, path)
if len(paths) == 0 {
return nil, errorEmptyPaths
}
statement := `
SELECT path, commentCount
FROM pages
WHERE domain = $1 AND path = ANY($2);
`
rows, err := db.Query(statement, domain, pq.Array(paths))
if err != nil {
return 0, errorInternal
logger.Errorf("cannot get comments: %v", err)
return nil, errorInternal
}
defer rows.Close()
for rows.Next() {
var path string
var commentCount int
if err = rows.Scan(&path, &commentCount); err != nil {
logger.Errorf("cannot scan path and commentCount: %v", err)
return nil, errorInternal
}
commentCounts[path] = commentCount
}
return p.CommentCount, nil
return commentCounts, nil
}
func commentCountHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
Domain *string `json:"domain"`
Path *string `json:"path"`
Domain *string `json:"domain"`
Paths *[]string `json:"paths"`
}
var x request
@@ -31,13 +55,12 @@ func commentCountHandler(w http.ResponseWriter, r *http.Request) {
}
domain := domainStrip(*x.Domain)
path := *x.Path
count, err := commentCount(domain, path)
commentCounts, err := commentCount(domain, *x.Paths)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true, "count": count})
bodyMarshal(w, response{"success": true, "commentCounts": commentCounts})
}

View File

@@ -14,14 +14,14 @@ func TestCommentCountBasics(t *testing.T) {
commentNew(commenterHex, "example.com", "/path.html", "root", "**bar**", "approved", time.Now().UTC())
commentNew(commenterHex, "example.com", "/path.html", "root", "**baz**", "unapproved", time.Now().UTC())
count, err := commentCount("example.com", "/path.html")
counts, err := commentCount("example.com", []string{"/path.html"})
if err != nil {
t.Errorf("unexpected error counting comments: %v", err)
return
}
if count != 2 {
t.Errorf("expected count=2 got count=%d", count)
if counts["/path.html"] != 2 {
t.Errorf("expected count=2 got count=%d", counts["/path.html"])
return
}
}
@@ -29,25 +29,25 @@ func TestCommentCountBasics(t *testing.T) {
func TestCommentCountNewPage(t *testing.T) {
failTestOnError(t, setupTestEnv())
count, err := commentCount("example.com", "/path.html")
counts, err := commentCount("example.com", []string{"/path.html"})
if err != nil {
t.Errorf("unexpected error counting comments: %v", err)
return
}
if count != 0 {
t.Errorf("expected count=0 got count=%d", count)
if counts["/path.html"] != 0 {
t.Errorf("expected count=0 got count=%d", counts["/path.html"])
return
}
}
func TestCommentCountEmpty(t *testing.T) {
if _, err := commentCount("example.com", ""); err != nil {
if _, err := commentCount("example.com", []string{""}); err != nil {
t.Errorf("unexpected error counting comments on empty path: %v", err)
return
}
if _, err := commentCount("", ""); err == nil {
if _, err := commentCount("", []string{""}); err == nil {
t.Errorf("expected error not found counting comments with empty everything")
return
}

View File

@@ -12,22 +12,26 @@ func commentList(commenterHex string, domain string, path string, includeUnappro
}
statement := `
SELECT commentHex, commenterHex, markdown, html, parentHex, score, state, creationDate
SELECT
commentHex,
commenterHex,
markdown,
html,
parentHex,
score,
state,
creationDate
FROM comments
WHERE
comments.domain = $1 AND
comments.path = $2
`
comments.domain = $1 AND
comments.path = $2
`
if !includeUnapproved {
if commenterHex == "anonymous" {
statement += `
AND state = 'approved'
`
statement += `AND state = 'approved'`
} else {
statement += `
AND (state = 'approved' OR commenterHex = $3)
`
statement += `AND (state = 'approved' OR commenterHex = $3)`
}
}
@@ -54,16 +58,24 @@ func commentList(commenterHex string, domain string, path string, includeUnappro
comments := []comment{}
for rows.Next() {
c := comment{}
if err = rows.Scan(&c.CommentHex, &c.CommenterHex, &c.Markdown, &c.Html, &c.ParentHex, &c.Score, &c.State, &c.CreationDate); err != nil {
if err = rows.Scan(
&c.CommentHex,
&c.CommenterHex,
&c.Markdown,
&c.Html,
&c.ParentHex,
&c.Score,
&c.State,
&c.CreationDate); err != nil {
return nil, nil, errorInternal
}
if commenterHex != "anonymous" {
statement = `
SELECT direction
FROM votes
WHERE commentHex=$1 AND commenterHex=$2;
`
SELECT direction
FROM votes
WHERE commentHex=$1 AND commenterHex=$2;
`
row := db.QueryRow(statement, c.CommentHex, commenterHex)
if err = row.Scan(&c.Direction); err != nil {
@@ -120,6 +132,8 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
commenterHex := "anonymous"
isModerator := false
modList := map[string]bool{}
if *x.CommenterToken != "anonymous" {
c, err := commenterGetByCommenterToken(*x.CommenterToken)
if err != nil {
@@ -134,11 +148,15 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
}
for _, mod := range d.Moderators {
modList[mod.Email] = true
if mod.Email == c.Email {
isModerator = true
break
}
}
} else {
for _, mod := range d.Moderators {
modList[mod.Email] = true
}
}
domainViewRecord(domain, commenterHex)
@@ -149,16 +167,32 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
return
}
_commenters := map[string]commenter{}
for commenterHex, cr := range commenters {
if _, ok := modList[cr.Email]; ok {
cr.IsModerator = true
}
cr.Email = ""
_commenters[commenterHex] = cr
}
bodyMarshal(w, response{
"success": true,
"domain": domain,
"comments": comments,
"commenters": commenters,
"commenters": _commenters,
"requireModeration": d.RequireModeration,
"requireIdentification": d.RequireIdentification,
"isFrozen": d.State == "frozen",
"isModerator": isModerator,
"attributes": p,
"configuredOauths": configuredOauths,
"configuredOauths": map[string]bool{
"commento": d.CommentoProvider,
"google": googleConfigured && d.GoogleProvider,
"twitter": twitterConfigured && d.TwitterProvider,
"github": githubConfigured && d.GithubProvider,
"gitlab": gitlabConfigured && d.GitlabProvider,
"sso": d.SsoProvider,
},
})
}

View File

@@ -12,4 +12,5 @@ type commenter struct {
Photo string `json:"photo"`
Provider string `json:"provider,omitempty"`
JoinDate time.Time `json:"joinDate,omitempty"`
IsModerator bool `json:"isModerator"`
}

34
api/commenter_photo.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"io"
"net/http"
)
func commenterPhotoHandler(w http.ResponseWriter, r *http.Request) {
c, err := commenterGetByHex(r.FormValue("commenterHex"))
if err != nil {
http.NotFound(w, r)
return
}
url := c.Photo
if c.Provider == "google" {
url += "?sz=50"
} else if c.Provider == "github" {
url += "&s=50"
} else if c.Provider == "twitter" {
url += "?size=normal"
} else if c.Provider == "gitlab" {
url += "?width=50"
}
resp, err := http.Get(url)
if err != nil {
http.NotFound(w, r)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}

View File

@@ -50,6 +50,12 @@ func configParse() error {
"GITHUB_KEY": "",
"GITHUB_SECRET": "",
"TWITTER_KEY": "",
"TWITTER_SECRET": "",
"GITLAB_KEY": "",
"GITLAB_SECRET": "",
}
for key, value := range defaults {

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.6.1"
var version = "v1.7.0"

25
api/cron_sso_token.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"time"
)
func ssoTokenCleanupBegin() error {
go func() {
for {
statement := `
DELETE FROM ssoTokens
WHERE creationDate < $1;
`
_, err := db.Exec(statement, time.Now().UTC().Add(time.Duration(-10)*time.Minute))
if err != nil {
logger.Errorf("error cleaning up export rows: %v", err)
return
}
time.Sleep(10 * time.Minute)
}
}()
return nil
}

View File

@@ -17,4 +17,12 @@ type domain struct {
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
Moderators []moderator `json:"moderators"`
EmailNotificationPolicy string `json:"emailNotificationPolicy"`
CommentoProvider bool `json:"commentoProvider"`
GoogleProvider bool `json:"googleProvider"`
TwitterProvider bool `json:"twitterProvider"`
GithubProvider bool `json:"githubProvider"`
GitlabProvider bool `json:"gitlabProvider"`
SsoProvider bool `json:"ssoProvider"`
SsoSecret string `json:"ssoSecret"`
SsoUrl string `json:"ssoUrl"`
}

82
api/domain_clear.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"net/http"
)
func domainClear(domain string) error {
if domain == "" {
return errorMissingField
}
statement := `
DELETE FROM votes
USING comments
WHERE comments.commentHex = votes.commentHex AND comments.domain = $1;
`
_, err := db.Exec(statement, domain)
if err != nil {
logger.Errorf("cannot delete votes: %v", err)
return errorInternal
}
statement = `
DELETE FROM comments
WHERE comments.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf(statement, domain)
return errorInternal
}
statement = `
DELETE FROM pages
WHERE pages.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf(statement, domain)
return errorInternal
}
return nil
}
func domainClearHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
OwnerToken *string `json:"ownerToken"`
Domain *string `json:"domain"`
}
var x request
if err := bodyUnmarshal(r, &x); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
o, err := ownerGetByOwnerToken(*x.OwnerToken)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
domain := domainStrip(*x.Domain)
isOwner, err := domainOwnershipVerify(o.OwnerHex, domain)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
if !isOwner {
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
return
}
if err = domainClear(*x.Domain); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true})
}

View File

@@ -19,17 +19,6 @@ func domainDelete(domain string) error {
return errorNoSuchDomain
}
statement = `
DELETE FROM votes
USING comments
WHERE comments.commentHex = votes.commentHex AND comments.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf("cannot delete votes: %v", err)
return errorInternal
}
statement = `
DELETE FROM views
WHERE views.domain = $1;
@@ -50,23 +39,9 @@ func domainDelete(domain string) error {
return errorInternal
}
statement = `
DELETE FROM comments
WHERE comments.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf(statement, domain)
return errorInternal
}
statement = `
DELETE FROM pages
WHERE pages.domain = $1;
`
_, err = db.Exec(statement, domain)
if err != nil {
logger.Errorf(statement, domain)
// comments, votes, and pages are handled by domainClear
if err = domainClear(domain); err != nil {
logger.Errorf("cannot clear domain: %v", err)
return errorInternal
}

View File

@@ -8,7 +8,26 @@ func domainGet(dmn string) (domain, error) {
}
statement := `
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
SELECT
domain,
ownerHex,
name,
creationDate,
state,
importedComments,
autoSpamFilter,
requireModeration,
requireIdentification,
moderateAllAnonymous,
emailNotificationPolicy,
commentoProvider,
googleProvider,
twitterProvider,
githubProvider,
gitlabProvider,
ssoProvider,
ssoSecret,
ssoUrl
FROM domains
WHERE domain = $1;
`
@@ -16,7 +35,26 @@ func domainGet(dmn string) (domain, error) {
var err error
d := domain{}
if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
if err = row.Scan(
&d.Domain,
&d.OwnerHex,
&d.Name,
&d.CreationDate,
&d.State,
&d.ImportedComments,
&d.AutoSpamFilter,
&d.RequireModeration,
&d.RequireIdentification,
&d.ModerateAllAnonymous,
&d.EmailNotificationPolicy,
&d.CommentoProvider,
&d.GoogleProvider,
&d.TwitterProvider,
&d.GithubProvider,
&d.GitlabProvider,
&d.SsoProvider,
&d.SsoSecret,
&d.SsoUrl); err != nil {
return d, errorNoSuchDomain
}

View File

@@ -10,7 +10,26 @@ func domainList(ownerHex string) ([]domain, error) {
}
statement := `
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous, emailNotificationPolicy
SELECT
domain,
ownerHex,
name,
creationDate,
state,
importedComments,
autoSpamFilter,
requireModeration,
requireIdentification,
moderateAllAnonymous,
emailNotificationPolicy,
commentoProvider,
googleProvider,
twitterProvider,
githubProvider,
gitlabProvider,
ssoProvider,
ssoSecret,
ssoUrl
FROM domains
WHERE ownerHex=$1;
`
@@ -24,7 +43,26 @@ func domainList(ownerHex string) ([]domain, error) {
domains := []domain{}
for rows.Next() {
d := domain{}
if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification, &d.ModerateAllAnonymous, &d.EmailNotificationPolicy); err != nil {
if err = rows.Scan(
&d.Domain,
&d.OwnerHex,
&d.Name,
&d.CreationDate,
&d.State,
&d.ImportedComments,
&d.AutoSpamFilter,
&d.RequireModeration,
&d.RequireIdentification,
&d.ModerateAllAnonymous,
&d.EmailNotificationPolicy,
&d.CommentoProvider,
&d.GoogleProvider,
&d.TwitterProvider,
&d.GithubProvider,
&d.GitlabProvider,
&d.SsoProvider,
&d.SsoSecret,
&d.SsoUrl); err != nil {
logger.Errorf("cannot Scan domain: %v", err)
return nil, errorInternal
}
@@ -63,5 +101,14 @@ func domainListHandler(w http.ResponseWriter, r *http.Request) {
return
}
bodyMarshal(w, response{"success": true, "domains": domains})
bodyMarshal(w, response{
"success": true,
"domains": domains,
"configuredOauths": map[string]bool{
"google": googleConfigured,
"twitter": twitterConfigured,
"github": githubConfigured,
"gitlab": gitlabConfigured,
},
})
}

View File

@@ -2,6 +2,7 @@ package main
import (
"net/http"
"strings"
"time"
)
@@ -10,6 +11,10 @@ func domainNew(ownerHex string, name string, domain string) error {
return errorMissingField
}
if strings.Contains(domain, "/") {
return errorInvalidDomain
}
statement := `
INSERT INTO
domains (ownerHex, name, domain, creationDate)

69
api/domain_sso.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"net/http"
)
func domainSsoSecretNew(domain string) (string, error) {
if domain == "" {
return "", errorMissingField
}
ssoSecret, err := randomHex(32)
if err != nil {
logger.Errorf("error generating SSO secret hex: %v", err)
return "", errorInternal
}
statement := `
UPDATE domains
SET ssoSecret = $2
WHERE domain = $1;
`
_, err = db.Exec(statement, domain, ssoSecret)
if err != nil {
logger.Errorf("cannot update ssoSecret: %v", err)
return "", errorInternal
}
return ssoSecret, nil
}
func domainSsoSecretNewHandler(w http.ResponseWriter, r *http.Request) {
type request struct {
OwnerToken *string `json:"ownerToken"`
Domain *string `json:"domain"`
}
var x request
if err := bodyUnmarshal(r, &x); err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
o, err := ownerGetByOwnerToken(*x.OwnerToken)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
domain := domainStrip(*x.Domain)
isOwner, err := domainOwnershipVerify(o.OwnerHex, domain)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
if !isOwner {
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
return
}
ssoSecret, err := domainSsoSecretNew(domain)
if err != nil {
bodyMarshal(w, response{"success": false, "message": err.Error()})
return
}
bodyMarshal(w, response{"success": true, "ssoSecret": ssoSecret})
}

View File

@@ -5,13 +5,46 @@ import (
)
func domainUpdate(d domain) error {
if d.SsoProvider && d.SsoUrl == "" {
return errorMissingField
}
statement := `
UPDATE domains
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6, moderateAllAnonymous=$7, emailNotificationPolicy=$8
SET
name=$2,
state=$3,
autoSpamFilter=$4,
requireModeration=$5,
requireIdentification=$6,
moderateAllAnonymous=$7,
emailNotificationPolicy=$8,
commentoProvider=$9,
googleProvider=$10,
twitterProvider=$11,
githubProvider=$12,
gitlabProvider=$13,
ssoProvider=$14,
ssoUrl=$15
WHERE domain=$1;
`
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification, d.ModerateAllAnonymous, d.EmailNotificationPolicy)
_, err := db.Exec(statement,
d.Domain,
d.Name,
d.State,
d.AutoSpamFilter,
d.RequireModeration,
d.RequireIdentification,
d.ModerateAllAnonymous,
d.EmailNotificationPolicy,
d.CommentoProvider,
d.GoogleProvider,
d.TwitterProvider,
d.GithubProvider,
d.GitlabProvider,
d.SsoProvider,
d.SsoUrl)
if err != nil {
logger.Errorf("cannot update non-moderators: %v", err)
return errorInternal

View File

@@ -44,3 +44,5 @@ var errorNewOwnerForbidden = errors.New("New user registrations are forbidden an
var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
var errorDatabaseMigration = errors.New("Encountered error applying database migration.")
var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.")
var errorEmptyPaths = errors.New("Empty paths field.")
var errorInvalidDomain = errors.New("Invalid domain name. Do not include the URL path after the forward slash.")

View File

@@ -2,6 +2,7 @@ package main
func main() {
exitIfError(loggerCreate())
exitIfError(versionPrint())
exitIfError(configParse())
exitIfError(dbConnect(5))
exitIfError(migrate())
@@ -15,6 +16,7 @@ func main() {
exitIfError(versionCheckStart())
exitIfError(domainExportCleanupBegin())
exitIfError(viewsCleanupBegin())
exitIfError(ssoTokenCleanupBegin())
exitIfError(routesServe())
}

View File

@@ -2,18 +2,27 @@ package main
import ()
var configuredOauths []string
var googleConfigured bool
var twitterConfigured bool
var githubConfigured bool
var gitlabConfigured bool
func oauthConfigure() error {
configuredOauths = []string{}
if err := googleOauthConfigure(); err != nil {
return err
}
if err := twitterOauthConfigure(); err != nil {
return err
}
if err := githubOauthConfigure(); err != nil {
return err
}
if err := gitlabOauthConfigure(); err != nil {
return err
}
return nil
}

View File

@@ -37,7 +37,7 @@ func githubOauthConfigure() error {
Endpoint: github.Endpoint,
}
configuredOauths = append(configuredOauths, "github")
githubConfigured = true
return nil
}

View File

@@ -57,6 +57,10 @@ func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
}
resp, err := http.Get("https://api.github.com/user?access_token=" + token.AccessToken)
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
@@ -80,6 +84,23 @@ func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
email = user["email"].(string)
}
if user["name"] == nil {
fmt.Fprintf(w, "Error: no name returned by Github")
return
}
name := user["name"].(string)
link := "undefined"
if user["html_url"] != nil {
link = user["html_url"].(string)
}
photo := "undefined"
if user["avatar_url"] != nil {
photo = user["avatar_url"].(string)
}
c, err := commenterGetByEmail("github", email)
if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s", err.Error())
@@ -90,14 +111,7 @@ func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
// TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter {
var link string
if val, ok := user["html_url"]; ok {
link = val.(string)
} else {
link = "undefined"
}
commenterHex, err = commenterNew(email, user["name"].(string), link, user["avatar_url"].(string), "github", "")
commenterHex, err = commenterNew(email, name, link, photo, "github", "")
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return

42
api/oauth_gitlab.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/gitlab"
"os"
)
var gitlabConfig *oauth2.Config
func gitlabOauthConfigure() error {
gitlabConfig = nil
if os.Getenv("GITLAB_KEY") == "" && os.Getenv("GITLAB_SECRET") == "" {
return nil
}
if os.Getenv("GITLAB_KEY") == "" {
logger.Errorf("COMMENTO_GITLAB_KEY not configured, but COMMENTO_GITLAB_SECRET is set")
return errorOauthMisconfigured
}
if os.Getenv("GITLAB_SECRET") == "" {
logger.Errorf("COMMENTO_GITLAB_SECRET not configured, but COMMENTO_GITLAB_KEY is set")
return errorOauthMisconfigured
}
logger.Infof("loading gitlab OAuth config")
gitlabConfig = &oauth2.Config{
RedirectURL: os.Getenv("ORIGIN") + "/api/oauth/gitlab/callback",
ClientID: os.Getenv("GITLAB_KEY"),
ClientSecret: os.Getenv("GITLAB_SECRET"),
Scopes: []string{
"read_user",
},
Endpoint: gitlab.Endpoint,
}
gitlabConfigured = true
return nil
}

View File

@@ -0,0 +1,96 @@
package main
import (
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
)
func gitlabCallbackHandler(w http.ResponseWriter, r *http.Request) {
commenterToken := r.FormValue("state")
code := r.FormValue("code")
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
token, err := gitlabConfig.Exchange(oauth2.NoContext, code)
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
resp, err := http.Get("https://gitlab.com/api/v4/user?access_token=" + token.AccessToken)
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
logger.Infof("%v", resp.StatusCode)
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(w, "Error: %s", errorCannotReadResponse.Error())
return
}
user := make(map[string]interface{})
if err := json.Unmarshal(contents, &user); err != nil {
fmt.Fprintf(w, "Error: %s", errorInternal.Error())
return
}
if user["email"] == nil {
fmt.Fprintf(w, "Error: no email address returned by Gitlab")
return
}
email := user["email"].(string)
if user["name"] == nil {
fmt.Fprintf(w, "Error: no name returned by Gitlab")
return
}
name := user["name"].(string)
link := "undefined"
if user["web_url"] != nil {
link = user["web_url"].(string)
}
photo := "undefined"
if user["avatar_url"] != nil {
photo = user["avatar_url"].(string)
}
c, err := commenterGetByEmail("gitlab", email)
if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
var commenterHex string
// TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter {
commenterHex, err = commenterNew(email, name, link, photo, "gitlab", "")
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
} else {
commenterHex = c.CommenterHex
}
if err := commenterSessionUpdate(commenterToken, commenterHex); err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
fmt.Fprintf(w, "<html><script>window.parent.close()</script></html>")
}

View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"net/http"
)
func gitlabRedirectHandler(w http.ResponseWriter, r *http.Request) {
if gitlabConfig == nil {
logger.Errorf("gitlab oauth access attempt without configuration")
fmt.Fprintf(w, "error: this website has not configured gitlab OAuth")
return
}
commenterToken := r.FormValue("commenterToken")
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "error: %s\n", err.Error())
return
}
url := gitlabConfig.AuthCodeURL(commenterToken)
http.Redirect(w, r, url, http.StatusFound)
}

View File

@@ -37,7 +37,7 @@ func googleOauthConfigure() error {
Endpoint: google.Endpoint,
}
configuredOauths = append(configuredOauths, "google")
googleConfigured = true
return nil
}

61
api/oauth_sso.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"time"
)
type ssoPayload struct {
Domain string `json:"domain"`
Token string `json:"token"`
Email string `json:"email"`
Name string `json:"name"`
Link string `json:"link"`
Photo string `json:"photo"`
}
func ssoTokenNew(domain string, commenterToken string) (string, error) {
token, err := randomHex(32)
if err != nil {
logger.Errorf("error generating SSO token hex: %v", err)
return "", errorInternal
}
statement := `
INSERT INTO
ssoTokens (token, domain, commenterToken, creationDate)
VALUES ($1, $2, $3, $4 );
`
_, err = db.Exec(statement, token, domain, commenterToken, time.Now().UTC())
if err != nil {
logger.Errorf("error inserting SSO token: %v", err)
return "", errorInternal
}
return token, nil
}
func ssoTokenExtract(token string) (string, string, error) {
statement := `
SELECT domain, commenterToken
FROM ssoTokens
WHERE token = $1;
`
row := db.QueryRow(statement, token)
var domain string
var commenterToken string
if err := row.Scan(&domain, &commenterToken); err != nil {
return "", "", errorNoSuchToken
}
statement = `
DELETE FROM ssoTokens
WHERE token = $1;
`
if _, err := db.Exec(statement, token); err != nil {
logger.Errorf("cannot delete SSO token after usage: %v", err)
return "", "", errorInternal
}
return domain, commenterToken, nil
}

116
api/oauth_sso_callback.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
)
func ssoCallbackHandler(w http.ResponseWriter, r *http.Request) {
payloadHex := r.FormValue("payload")
signature := r.FormValue("hmac")
payloadBytes, err := hex.DecodeString(payloadHex)
if err != nil {
fmt.Fprintf(w, "Error: invalid JSON payload hex encoding: %s\n", err.Error())
return
}
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
fmt.Fprintf(w, "Error: invalid HMAC signature hex encoding: %s\n", err.Error())
return
}
payload := ssoPayload{}
err = json.Unmarshal(payloadBytes, &payload)
if err != nil {
fmt.Fprintf(w, "Error: cannot unmarshal JSON payload: %s\n", err.Error())
return
}
if payload.Token == "" || payload.Email == "" || payload.Name == "" {
fmt.Fprintf(w, "Error: %s\n", errorMissingField.Error())
return
}
if payload.Link == "" {
payload.Link = "undefined"
}
if payload.Photo == "" {
payload.Photo = "undefined"
}
domain, commenterToken, err := ssoTokenExtract(payload.Token)
if err != nil {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
d, err := domainGet(domain)
if err != nil {
if err == errorNoSuchDomain {
fmt.Fprintf(w, "Error: %s\n", err.Error())
} else {
logger.Errorf("cannot get domain for SSO: %v", err)
fmt.Fprintf(w, "Error: %s\n", errorInternal.Error())
}
return
}
if d.SsoSecret == "" || d.SsoUrl == "" {
fmt.Fprintf(w, "Error: %s\n", errorMissingConfig.Error())
return
}
key, err := hex.DecodeString(d.SsoSecret)
if err != nil {
logger.Errorf("cannot decode SSO secret as hex: %v", err)
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
h := hmac.New(sha256.New, key)
h.Write(payloadBytes)
expectedSignatureBytes := h.Sum(nil)
if !hmac.Equal(expectedSignatureBytes, signatureBytes) {
fmt.Fprintf(w, "Error: HMAC signature verification failed\n")
return
}
_, err = commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
c, err := commenterGetByEmail("sso:"+domain, payload.Email)
if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
var commenterHex string
// TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter {
commenterHex, err = commenterNew(payload.Email, payload.Name, payload.Link, payload.Photo, "sso:"+domain, "")
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
} else {
commenterHex = c.CommenterHex
}
if err = commenterSessionUpdate(commenterToken, commenterHex); err != nil {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
fmt.Fprintf(w, "<html><script>window.parent.close()</script></html>")
}

88
api/oauth_sso_redirect.go Normal file
View File

@@ -0,0 +1,88 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/url"
)
func ssoRedirectHandler(w http.ResponseWriter, r *http.Request) {
commenterToken := r.FormValue("commenterToken")
domain := r.Header.Get("Referer")
if commenterToken == "" {
fmt.Fprintf(w, "Error: %s\n", errorMissingField.Error())
return
}
domain = domainStrip(domain)
if domain == "" {
fmt.Fprintf(w, "Error: No Referer header found in request\n")
return
}
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
d, err := domainGet(domain)
if err != nil {
fmt.Fprintf(w, "Error: %s\n", errorNoSuchDomain.Error())
return
}
if !d.SsoProvider {
fmt.Fprintf(w, "Error: SSO not configured for %s\n", domain)
return
}
if d.SsoSecret == "" || d.SsoUrl == "" {
fmt.Fprintf(w, "Error: %s\n", errorMissingConfig.Error())
return
}
key, err := hex.DecodeString(d.SsoSecret)
if err != nil {
logger.Errorf("cannot decode SSO secret as hex: %v", err)
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
token, err := ssoTokenNew(domain, commenterToken)
if err != nil {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
tokenBytes, err := hex.DecodeString(token)
if err != nil {
logger.Errorf("cannot decode hex token: %v", err)
fmt.Fprintf(w, "Error: %s\n", errorInternal.Error())
return
}
h := hmac.New(sha256.New, key)
h.Write(tokenBytes)
signature := hex.EncodeToString(h.Sum(nil))
u, err := url.Parse(d.SsoUrl)
if err != nil {
// this should really not be happening; we're checking if the
// passed URL is valid at domain update
logger.Errorf("cannot parse URL: %v", err)
fmt.Fprintf(w, "Error: %s\n", errorInternal.Error())
return
}
q := u.Query()
q.Set("token", token)
q.Set("hmac", signature)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
}

51
api/oauth_twitter.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
"github.com/gomodule/oauth1/oauth"
"os"
"sync"
)
type twitterOauthState struct {
CommenterToken string
Cred *oauth.Credentials
}
var twitterClient *oauth.Client
var twitterCredMapLock sync.RWMutex
var twitterCredMap map[string]twitterOauthState
func twitterOauthConfigure() error {
twitterClient = nil
if os.Getenv("TWITTER_KEY") == "" && os.Getenv("TWITTER_SECRET") == "" {
return nil
}
if os.Getenv("TWITTER_KEY") == "" {
logger.Errorf("COMMENTO_TWITTER_KEY not configured, but COMMENTO_TWITTER_SECRET is set")
return errorOauthMisconfigured
}
if os.Getenv("TWITTER_SECRET") == "" {
logger.Errorf("COMMENTO_TWITTER_SECRET not configured, but COMMENTO_TWITTER_KEY is set")
return errorOauthMisconfigured
}
logger.Infof("loading twitter OAuth config")
twitterClient = &oauth.Client{
TemporaryCredentialRequestURI: "https://api.twitter.com/oauth/request_token",
ResourceOwnerAuthorizationURI: "https://api.twitter.com/oauth/authenticate",
TokenRequestURI: "https://api.twitter.com/oauth/access_token",
Credentials: oauth.Credentials{
Token: os.Getenv("TWITTER_KEY"),
Secret: os.Getenv("TWITTER_SECRET"),
},
}
twitterCredMap = make(map[string]twitterOauthState, 1e3)
twitterConfigured = true
return nil
}

View File

@@ -0,0 +1,108 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func twitterCallbackHandler(w http.ResponseWriter, r *http.Request) {
token := r.FormValue("oauth_token")
verifier := r.FormValue("oauth_verifier")
twitterCredMapLock.RLock()
s, ok := twitterCredMap[token]
twitterCredMapLock.RUnlock()
commenterToken := s.CommenterToken
if !ok {
fmt.Fprintf(w, "no such token/verifier combination found")
return
}
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
x, _, err := twitterClient.RequestToken(nil, s.Cred, verifier)
if err != nil {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
twitterCredMapLock.Lock()
delete(twitterCredMap, token)
twitterCredMapLock.Unlock()
resp, err := twitterClient.Get(nil, x, "https://api.twitter.com/1.1/account/verify_credentials.json", url.Values{"include_email": {"true"}})
if err != nil {
fmt.Fprintf(w, "Error getting email: %s\n", err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
msg, _ := ioutil.ReadAll(resp.Body)
fmt.Fprintf(w, "Error: status %d: %s\n", resp.StatusCode, msg)
return
}
var res map[string]interface{}
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
fmt.Fprintf(w, "Error: %s\n", err.Error())
return
}
if res["email"] == nil {
fmt.Fprintf(w, "Error: no email address returned by Twitter")
return
}
email := res["email"].(string)
if res["name"] == nil {
fmt.Fprintf(w, "Error: no name returned by Twitter")
return
}
name := res["name"].(string)
link := "undefined"
photo := "undefined"
if res["handle"] != nil {
handle := res["screen_name"].(string)
link = "https://twitter.com/" + handle
photo = "https://twitter.com/" + handle + "/profile_image"
}
c, err := commenterGetByEmail("twitter", email)
if err != nil && err != errorNoSuchCommenter {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
var commenterHex string
// TODO: in case of returning users, update the information we have on record?
if err == errorNoSuchCommenter {
commenterHex, err = commenterNew(email, name, link, photo, "twitter", "")
if err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
} else {
commenterHex = c.CommenterHex
}
if err := commenterSessionUpdate(commenterToken, commenterHex); err != nil {
fmt.Fprintf(w, "Error: %s", err.Error())
return
}
fmt.Fprintf(w, "<html><script>window.parent.close()</script></html>")
}

View File

@@ -0,0 +1,39 @@
package main
import (
"fmt"
"net/http"
"os"
)
func twitterRedirectHandler(w http.ResponseWriter, r *http.Request) {
if twitterClient == nil {
logger.Errorf("twitter oauth access attempt without configuration")
fmt.Fprintf(w, "error: this website has not configured twitter OAuth")
return
}
commenterToken := r.FormValue("commenterToken")
_, err := commenterGetByCommenterToken(commenterToken)
if err != nil && err != errorNoSuchToken {
fmt.Fprintf(w, "error: %s\n", err.Error())
return
}
cred, err := twitterClient.RequestTemporaryCredentials(nil, os.Getenv("ORIGIN")+"/api/oauth/twitter/callback", nil)
if err != nil {
logger.Errorf("cannot get temporary twitter credentials: %v", err)
fmt.Fprintf(w, "error: %v", errorInternal.Error())
return
}
twitterCredMapLock.Lock()
twitterCredMap[cred.Token] = twitterOauthState{
CommenterToken: commenterToken,
Cred: cred,
}
twitterCredMapLock.Unlock()
http.Redirect(w, r, twitterClient.AuthorizationURL(cred, nil), http.StatusFound)
}

View File

@@ -18,8 +18,9 @@ func ownerResetPassword(resetHex string, password string) error {
statement := `
UPDATE owners SET passwordHash=$1
WHERE email IN (
SELECT email FROM ownerResetHexes
WHERE ownerHex = (
SELECT ownerHex
FROM ownerResetHexes
WHERE resetHex=$2
);
`

View File

@@ -14,6 +14,8 @@ func apiRouterInit(router *mux.Router) error {
router.HandleFunc("/api/domain/new", domainNewHandler).Methods("POST")
router.HandleFunc("/api/domain/delete", domainDeleteHandler).Methods("POST")
router.HandleFunc("/api/domain/clear", domainClearHandler).Methods("POST")
router.HandleFunc("/api/domain/sso/new", domainSsoSecretNewHandler).Methods("POST")
router.HandleFunc("/api/domain/list", domainListHandler).Methods("POST")
router.HandleFunc("/api/domain/update", domainUpdateHandler).Methods("POST")
router.HandleFunc("/api/domain/moderator/new", domainModeratorNewHandler).Methods("POST")
@@ -27,6 +29,7 @@ func apiRouterInit(router *mux.Router) error {
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/commenter/photo", commenterPhotoHandler).Methods("GET")
router.HandleFunc("/api/email/get", emailGetHandler).Methods("POST")
router.HandleFunc("/api/email/update", emailUpdateHandler).Methods("POST")
@@ -38,6 +41,15 @@ func apiRouterInit(router *mux.Router) error {
router.HandleFunc("/api/oauth/github/redirect", githubRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).Methods("GET")
router.HandleFunc("/api/oauth/twitter/redirect", twitterRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/twitter/callback", twitterCallbackHandler).Methods("GET")
router.HandleFunc("/api/oauth/gitlab/redirect", gitlabRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/gitlab/callback", gitlabCallbackHandler).Methods("GET")
router.HandleFunc("/api/oauth/sso/redirect", ssoRedirectHandler).Methods("GET")
router.HandleFunc("/api/oauth/sso/callback", ssoCallbackHandler).Methods("GET")
router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST")
router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST")
router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST")

View File

@@ -36,6 +36,7 @@ func fileDetemplate(f string) ([]byte, error) {
x = strings.Replace(x, "[[[.Origin]]]", os.Getenv("ORIGIN"), -1)
x = strings.Replace(x, "[[[.CdnPrefix]]]", os.Getenv("CDN_PREFIX"), -1)
x = strings.Replace(x, "[[[.Footer]]]", footer, -1)
x = strings.Replace(x, "[[[.Version]]]", version, -1)
return []byte(x), nil
}
@@ -116,7 +117,7 @@ func staticRouterInit(router *mux.Router) error {
if path.Ext(p) != "" {
contentType[p] = mime.TypeByExtension(path.Ext(p))
} else {
contentType[p] = mime.TypeByExtension("html")
contentType[p] = "text/html; charset=utf-8"
}
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,7 +7,14 @@ import (
)
func sigintCleanup() int {
// TODO: close the database connection and do other cleanup jobs
if db != nil {
err := db.Close()
if err == nil {
logger.Errorf("cannot close database connection: %v", err)
return 1
}
}
return 0
}

View File

@@ -13,12 +13,16 @@ func smtpConfigure() error {
password := os.Getenv("SMTP_PASSWORD")
host := os.Getenv("SMTP_HOST")
port := os.Getenv("SMTP_PORT")
if username == "" || password == "" || host == "" || port == "" {
if host == "" || port == "" {
logger.Warningf("smtp not configured, no emails will be sent")
smtpConfigured = false
return nil
}
if username == "" || password == "" {
logger.Warningf("no SMTP username/password set, Commento will assume they aren't required")
}
if os.Getenv("SMTP_FROM_ADDRESS") == "" {
logger.Errorf("COMMENTO_SMTP_FROM_ADDRESS not set")
smtpConfigured = false

View File

@@ -6,6 +6,10 @@ import (
)
func htmlTitleRecurse(h *html.Node) string {
if h == nil || h.FirstChild == nil {
return ""
}
if h.Type == html.ElementNode && h.Data == "title" {
return h.FirstChild.Data
}

View File

@@ -9,6 +9,11 @@ import (
"time"
)
func versionPrint() error {
logger.Infof("starting Commento %s", version)
return nil
}
func versionCheckStart() error {
go func() {
printedError := false

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.6.0';

View File

@@ -0,0 +1,16 @@
-- Make all login providers optional (but enabled by default)
ALTER TABLE domains
ADD commentoProvider BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE domains
ADD googleProvider BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE domains
ADD twitterProvider BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE domains
ADD githubProvider BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE domains
ADD gitlabProvider BOOLEAN NOT NULL DEFAULT true;

10
db/20190420181913-sso.sql Normal file
View File

@@ -0,0 +1,10 @@
-- Single Sign-On (SSO)
ALTER TABLE domains
ADD ssoProvider BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE domains
ADD ssoSecret TEXT NOT NULL DEFAULT '';
ALTER TABLE domains
ADD ssoUrl TEXT NOT NULL DEFAULT '';

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS ssoTokens (
token TEXT NOT NULL UNIQUE PRIMARY KEY ,
domain TEXT NOT NULL ,
commenterToken TEXT NOT NULL ,
creationDate TIMESTAMP NOT NULL
);

View File

@@ -0,0 +1,2 @@
UPDATE config
SET version = 'v1.7.0';

View File

@@ -4,7 +4,6 @@
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Email Confirmation</title>
</head>

View File

@@ -8,7 +8,6 @@
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/dashboard.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Dashboard</title>
</head>
@@ -160,39 +159,48 @@
<ul class="tabs">
<li class="tab-link original current" data-tab="mod-tab-1">General</li>
<li class="tab-link" data-tab="mod-tab-2">Add/Remove Moderators</li>
<li class="tab-link" data-tab="mod-tab-3">Email Settings</li>
</ul>
<div id="mod-tab-1" class="content original current">
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
<label for="spam-filtering">Automatic spam filtering</label>
<div class="pitch">
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. This is strongly recommended. Requires backend configuration.
<div class="question">
<div class="title">
Comment Filtering
</div>
<div class="answer">
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
<label for="spam-filtering">Automatic spam filtering</label>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
<label for="require-moderation">Require all comments to be approved manually</label>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
</div>
</div>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
<label for="require-moderation">Require all comments to be approved manually</label>
<div class="pitch">
Enabling this would require a moderator to approve all comments. This is generally recommended if your site doesn't receive too much traffic.
<div class="question">
<div class="title">
Email Schedule
</div>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
<label for="allow-anonymous">Allow anonymous comments</label>
<div class="pitch">
Enabling this would allow your readers to comment anonymously. Disabling would require the to authenticate themselves (using their Google account, for example). Recommended.
</div>
</div>
<div class="row no-border commento-round-check indent" v-if="domains[cd].allowAnonymous">
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
<div class="pitch">
Enabling this would require a moderator to approve all anonymous comments. This is recommended if most of your spam comments are from anonymous users.
<div class="answer">
<div class="row no-border commento-round-check">
<input type="radio" id="email-all" value="all" v-model="domains[cd].emailNotificationPolicy">
<label for="email-all">Whenever a new comment is created</label>
</div>
<div class="row no-border commento-round-check">
<input type="radio" id="email-pending-moderation" value="pending-moderation" v-model="domains[cd].emailNotificationPolicy">
<label for="email-pending-moderation">Only for comments pending moderation</label>
</div>
<div class="row no-border commento-round-check">
<input type="radio" id="email-none" value="none" v-model="domains[cd].emailNotificationPolicy">
<label for="email-none">Do not email moderators</label>
</div>
</div>
</div>
@@ -223,31 +231,6 @@
</div>
</div>
</div>
<div id="mod-tab-3" class="content">
<div class="normal-text">
You can enable email notifications to notify your moderators when a new comment is posted or when a comment is pending moderation. Commento tries to be smart about how often an email is sent. Emails will be delayed and batched until you go 10 minutes without one. This requires valid SMTP settings in order to send emails.<br><br>
</div>
<div class="question">
When do you want emails sent to moderators?
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-all" value="all" v-model="domains[cd].emailNotificationPolicy">
<label for="email-all">Whenever a new comment is created</label>
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-pending-moderation" value="pending-moderation" v-model="domains[cd].emailNotificationPolicy">
<label for="email-pending-moderation">Only for comments pending moderation</label>
</div>
<div class="row no-border commento-round-check indent">
<input type="radio" id="email-none" value="none" v-model="domains[cd].emailNotificationPolicy">
<label for="email-none">Do not email moderators</label>
</div>
<br>
<div class="center">
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
</div>
</div>
</div>
</div>
</div>
@@ -272,6 +255,71 @@
<input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name">
</div>
</div>
<div class="question">
<div class="title">
Authentication Options
</div>
<div class="answer">
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
<label for="allow-anonymous">Anonymous comments</label>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].commentoProvider" id="commento-provider">
<label for="commento-provider">Email address login</label>
</div>
<div class="row no-border commento-round-check" v-if="configuredOauths.google">
<input type="checkbox" v-model="domains[cd].googleProvider" id="google-provider">
<label for="google-provider">Google login</label>
</div>
<div class="row no-border commento-round-check" v-if="configuredOauths.twitter">
<input type="checkbox" v-model="domains[cd].twitterProvider" id="twitter-provider">
<label for="twitter-provider">Twitter login</label>
</div>
<div class="row no-border commento-round-check" v-if="configuredOauths.github">
<input type="checkbox" v-model="domains[cd].githubProvider" id="github-provider">
<label for="github-provider">GitHub login</label>
</div>
<div class="row no-border commento-round-check" v-if="configuredOauths.gitlab">
<input type="checkbox" v-model="domains[cd].gitlabProvider" id="gitlab-provider">
<label for="gitlab-provider">GitLab login</label>
</div>
<div class="row no-border commento-round-check">
<input type="checkbox" v-model="domains[cd].ssoProvider" id="sso-provider" @change="window.commento.ssoProviderChangeHandler()">
<label for="sso-provider">Single sign-on</label>
</div>
<div class="indent" v-if="domains[cd].ssoProvider">
<div class="row">
<div class="label">HMAC shared secret key</div>
<input class="input gray-input monospace" id="sso-secret" readonly="true" type="text" placeholder="Loading..." v-model="domains[cd].ssoSecret">
</div>
<div class="row">
<div class="label">Redirect URL</div>
<input class="input gray-input" id="sso-url" type="text" :placeholder="domains[cd].ssoUrl" v-model="domains[cd].ssoUrl">
</div>
<div class="normal-text">
<div class="subtext-container">
<div class="subtext">
Read the Commento documentation <a href="https://docs.commento.io/configuration/frontend/sso.html">on single sign-on</a>.
</div>
</div>
</div>
</div>
<div class="warning" v-if="!domains[cd].allowAnonymous && !domains[cd].commentoProvider && !domains[cd].googleProvider && !domains[cd].twitterProvider && !domains[cd].githubProvider && !domains[cd].gitlabProvider">
You have disabled all authentication options. Your readers will not be able to login, create comments, or vote.
</div>
</div>
</div>
<div class="center">
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
</div>
@@ -355,39 +403,67 @@
<div id="danger-view" class="view hidden">
<div class="view-inside">
<div class="mid-view">
<div class="tabs-container">
<div class="tab">
<ul class="tabs">
<li class="tab-link original current" data-tab="danger-tab-1">Freeze Comments</li>
<li class="tab-link current" data-tab="danger-tab-2">Delete Domain</li>
</ul>
<div class="center center-title">
Danger Zone
</div>
<div id="danger-tab-1" class="content original current">
<div class="box" v-if="domains[cd].state == 'frozen'">
<div class="box-subtitle">
If you desire to re-allow comments again on your website, you can do so. You can, of course, freeze the site again in the future.
<div class="action-buttons-container">
<div class="action-buttons">
<div class="action-button" v-if="domains[cd].state != 'frozen'">
<div class="left">
<div class="title">
Freeze Domain
</div>
<div class="subtitle">
Freezing your domain will disable new comments and voting temporarily. You may unfreeze the domain later.
</div>
<button onclick="document.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button>
</div>
<div class="box" v-if="domains[cd].state != 'frozen'">
<div class="box-subtitle">
If you desire to temporarily freeze new comments (domain-wide), thereby making it read-only, you can do so. You can choose to unfreeze later; this is temporary.
</div>
<button id="orange-button" onclick="document.location.hash='#freeze-domain-modal'" class="button orange-button">Freeze Domain</button>
<div class="right">
<button onclick="document.location.hash='#freeze-domain-modal'"
class="button orange-button">Freeze</button>
</div>
</div>
</div>
<div id="danger-tab-2" class="content">
<div class="box">
<div class="box-subtitle">
Want to completely remove Commento from your website? This will permanently delete all comments and there is literally no way to retrieve your data once you do this.
<div class="action-button" v-if="domains[cd].state == 'frozen'">
<div class="left">
<div class="title">
Unfreeze Domain
</div>
<div class="subtitle">
Unfreezing your domain will allow readers to create new comments and vote on comments again. You may re-freeze the domain later.
</div>
</div>
<div class="right">
<button onclick="document.location.hash='#unfreeze-domain-modal'"
class="button green-button">Unfreeze</button>
</div>
</div>
<div class="action-button">
<div class="left">
<div class="title">
Clear All Comments
</div>
<div class="subtitle">
This will permanently delete all comments without affecting your settings. This may be useful if you want to clear all comments after testing Commento. Cannot be reversed.
</div>
</div>
<div class="right">
<button onclick="document.location.hash='#clear-comments-modal'"
class="button big-red-button">Clear</button>
</div>
</div>
<div class="action-button">
<div class="left">
<div class="title">
Delete Domain
</div>
<div class="subtitle">
This will permanently delete all comments and all data associated with your domain. There is literally no way to retrieve your data once you do this. Please be certain.
</div>
</div>
<div class="right">
<button onclick="document.location.hash='#delete-domain-modal'"
class="button big-red-button">Delete</button>
</div>
<button id="big-red-button" class="button big-red-button" onclick="document.location.hash='#delete-domain-modal'">Delete Domain</button>
</div>
</div>
</div>
@@ -404,8 +480,8 @@
<div class="modal-subtitle">
Are you absolutely sure you want to freeze your domain, thereby making it read-only? You can choose to unfreeze later; this is temporary.
</div>
<div class="modal-contents">
<button id="orange-button" class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
<div class="modal-contents center">
<button class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
</div>
</div>
</div>
@@ -417,8 +493,21 @@
<div class="modal-subtitle">
Are you absolutely sure you want to unfreeze your domain? This will re-allow new comments. You can choose to freeze again in the future.
</div>
<div class="modal-contents">
<button id="blue-button" class="button green-button" onclick="window.commento.domainUnfreezeHandler()">Unfreeze Domain</button>
<div class="modal-contents center">
<button class="button green-button" onclick="window.commento.domainUnfreezeHandler()">Unfreeze Domain</button>
</div>
</div>
</div>
<div id="clear-comments-modal" class="modal-window">
<div class="inside">
<a href="#modal-close" title="Close" class="modal-close"></a>
<div class="modal-title">Clear Comments</div>
<div class="modal-subtitle">
Are you absolutely sure you want to clear all comments data? This is not reversible, so please be certain.
</div>
<div class="modal-contents center">
<button class="button big-red-button" onclick="window.commento.domainClearHandler()">Clear</button>
</div>
</div>
</div>
@@ -430,8 +519,8 @@
<div class="modal-subtitle">
Are you absolutely sure? This will permanently delete all comments and there is literally no way to retrieve your data once you do this.
</div>
<div class="modal-contents">
<button id="big-red-button" class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
<div class="modal-contents center">
<button class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
</div>
</div>
</div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -19,4 +19,7 @@
</div>
</div>
</div>
<div class="copyright">
Commento [[[.Version]]]
</div>
</div>

View File

@@ -5,7 +5,6 @@
<script src="[[[.CdnPrefix]]]/js/forgot.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Reset your Password</title>
</head>
@@ -17,16 +16,22 @@
<div class="auth-form-container">
<div class="auth-form">
<div class="form-title">
Reset your Password
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<button id="reset-button" class="button" onclick="window.commento.sendResetHex()">Send Reset Password Link</button>
<form onsubmit="window.commento.sendResetHex(event)">
<div class="form-title">
Reset your Password
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<button id="reset-button" class="button" type="submit">Send Reset Password Link</button>
</form>
<a class="link" href="[[[.Origin]]]/login">Suddenly remembered your password? Login.</a>
</div>
</div>

View File

@@ -76,6 +76,7 @@ const jsCompileMap = {
"js/logout.js"
],
"js/commento.js": ["js/commento.js"],
"js/count.js": ["js/count.js"],
"js/unsubscribe.js": [
"js/constants.js",
"js/utils.js",

View File

@@ -1,14 +1,6 @@
(function(global, document) {
"use strict";
if (global.commento === undefined) {
console.log("[commento] error: window.commento namespace not defined; maybe there's a mismatch in version between the backend and the frontend?");
return;
} else {
global = global.commento;
}
// Do not use other files like utils.js and http.js in the gulpfile to build
// commento.js for the following reasons:
// - We don't use jQuery in the actual JavaScript payload because we need
@@ -63,6 +55,8 @@
var ID_CONTENTS = "commento-comment-contents-";
var ID_NAME = "commento-comment-name-";
var ID_SUBMIT_BUTTON = "commento-submit-button-";
var ID_MARKDOWN_BUTTON = "commento-markdown-button-";
var ID_MARKDOWN_HELP = "commento-markdown-help-";
var ID_FOOTER = "commento-footer";
@@ -70,6 +64,7 @@
var cdn = "[[[.CdnPrefix]]]";
var root = null;
var cssOverride;
var noFonts;
var autoInit;
var isAuthenticated = false;
var comments = [];
@@ -81,10 +76,11 @@
var isLocked = false;
var stickyCommentHex = "none";
var shownReply = {};
var configuredOauths = [];
var configuredOauths = {};
var popupBoxType = "login";
var oauthButtonsShown = false;
var selfHex = undefined;
var mobileView = null;
function $(id) {
@@ -147,6 +143,19 @@
}
function removeAllEventListeners(node) {
if (node !== null) {
var replacement = node.cloneNode(true);
if (node.parentNode !== null) {
node.parentNode.replaceChild(replacement, node);
return replacement;
}
}
return node;
}
function onclick(node, f, arg) {
node.addEventListener("click", function() {
f(arg);
@@ -231,7 +240,12 @@
var loggedContainer = create("div");
var loggedInAs = create("div");
var name = create("a");
var name;
if (commenter.link !== "undefined") {
name = create("a");
} else {
name = create("div");
}
var avatar;
var logout = create("div");
var color = colorGet(commenter.commenterHex + "-" + commenter.name);
@@ -249,7 +263,9 @@
onclick(logout, global.logout);
attrSet(loggedContainer, "style", "display: none");
attrSet(name, "href", commenter.link);
if (commenter.link !== "undefined") {
attrSet(name, "href", commenter.link);
}
if (commenter.photo === "undefined") {
avatar = create("div");
avatar.style["background"] = color;
@@ -257,13 +273,7 @@
classAdd(avatar, "avatar");
} else {
avatar = create("img");
if (commenter.provider === "google") {
attrSet(avatar, "src", commenter.photo + "?sz=50");
} else if (commenter.provider === "github") {
attrSet(avatar, "src", commenter.photo + "&s=50");
} else {
attrSet(avatar, "src", commenter.photo);
}
attrSet(avatar, "src", cdn + "/api/commenter/photo?commenterHex=" + commenter.commenterHex);
classAdd(avatar, "avatar-img");
}
@@ -297,6 +307,7 @@
}
selfLoad(resp.commenter);
global.allShow();
call(callback);
});
@@ -320,7 +331,6 @@
var footer = create("div");
var aContainer = create("div");
var a = create("a");
var img = create("img");
var text = create("span");
footer.id = ID_FOOTER;
@@ -328,21 +338,18 @@
classAdd(footer, "footer");
classAdd(aContainer, "logo-container");
classAdd(a, "logo");
classAdd(img, "logo-svg");
classAdd(text, "logo-text");
attrSet(footer, "style", "display: none");
attrSet(a, "href", "https://commento.io");
attrSet(a, "target", "_blank");
attrSet(img, "src", cdn + "/images/logo.svg");
text.innerText = "Powered by Commento";
text.innerText = "Commento";
append(a, img);
append(a, text);
append(aContainer, a);
append(footer, aContainer);
append(root, footer);
return footer;
}
@@ -357,6 +364,8 @@
if (!resp.success) {
errorShow(resp.message);
return;
} else {
errorHide();
}
requireIdentification = resp.requireIdentification;
@@ -370,8 +379,6 @@
commenters = Object.assign({}, commenters, resp.commenters)
configuredOauths = resp.configuredOauths;
cssLoad(cdn + "/css/commento.css", "window.commento.loadCssOverride()");
call(callback);
});
}
@@ -386,6 +393,13 @@
}
function errorHide() {
var el = $(ID_ERROR);
attrSet(el, "style", "display: none;");
}
function errorElementCreate() {
var el = create("div");
@@ -406,6 +420,82 @@
};
function markdownHelpShow(id) {
var textareaSuperContainer = $(ID_SUPER_CONTAINER + id);
var markdownButton = $(ID_MARKDOWN_BUTTON + id);
var markdownHelp = create("table");
var italicsContainer = create("tr");
var italicsLeft = create("td");
var italicsRight = create("td");
var boldContainer = create("tr");
var boldLeft = create("td");
var boldRight = create("td");
var codeContainer = create("tr");
var codeLeft = create("td");
var codeRight = create("td");
var strikethroughContainer = create("tr");
var strikethroughLeft = create("td");
var strikethroughRight = create("td");
var hyperlinkContainer = create("tr");
var hyperlinkLeft = create("td");
var hyperlinkRight = create("td");
var quoteContainer = create("tr");
var quoteLeft = create("td");
var quoteRight = create("td");
markdownHelp.id = ID_MARKDOWN_HELP + id;
classAdd(markdownHelp, "markdown-help");
boldLeft.innerHTML = "<b>bold</b>";
boldRight.innerHTML = "surround text with <pre>**two asterisks**</pre>";
italicsLeft.innerHTML = "<i>italics</i>";
italicsRight.innerHTML = "surround text with <pre>*asterisks*</pre>";
codeLeft.innerHTML = "<pre>code</pre>";
codeRight.innerHTML = "surround text with <pre>`backticks`</pre>";
strikethroughLeft.innerHTML = "<strike>strikethrough</strike>";
strikethroughRight.innerHTML = "surround text with <pre>~~two tilde characters~~</pre>";
hyperlinkLeft.innerHTML = "<a href=\"https://example.com\">hyperlink</a>";
hyperlinkRight.innerHTML = "<pre>[hyperlink](https://example.com)</pre> or just a bare URL";
quoteLeft.innerHTML = "<blockquote>quote</blockquote>";
quoteRight.innerHTML = "prefix with <pre>&gt;</pre>";
markdownButton = removeAllEventListeners(markdownButton);
onclick(markdownButton, markdownHelpHide, id);
append(italicsContainer, italicsLeft);
append(italicsContainer, italicsRight);
append(markdownHelp, italicsContainer);
append(boldContainer, boldLeft);
append(boldContainer, boldRight);
append(markdownHelp, boldContainer);
append(hyperlinkContainer, hyperlinkLeft);
append(hyperlinkContainer, hyperlinkRight);
append(markdownHelp, hyperlinkContainer);
append(codeContainer, codeLeft);
append(codeContainer, codeRight);
append(markdownHelp, codeContainer);
append(strikethroughContainer, strikethroughLeft);
append(strikethroughContainer, strikethroughRight);
append(markdownHelp, strikethroughContainer);
append(quoteContainer, quoteLeft);
append(quoteContainer, quoteRight);
append(markdownHelp, quoteContainer);
append(textareaSuperContainer, markdownHelp);
}
function markdownHelpHide(id) {
var markdownButton = $(ID_MARKDOWN_BUTTON + id);
var markdownHelp = $(ID_MARKDOWN_HELP + id);
markdownButton = removeAllEventListeners(markdownButton);
onclick(markdownButton, markdownHelpShow, id);
remove(markdownHelp);
}
function textareaCreate(id) {
var textareaSuperContainer = create("div");
var textareaContainer = create("div");
@@ -414,18 +504,21 @@
var anonymousCheckbox = create("input");
var anonymousCheckboxLabel = create("label");
var submitButton = create("button");
var markdownButton = create("a");
textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
textarea.id = ID_TEXTAREA + id;
anonymousCheckbox.id = ID_ANONYMOUS_CHECKBOX + id;
submitButton.id = ID_SUBMIT_BUTTON + id;
markdownButton.id = ID_MARKDOWN_BUTTON + id;
classAdd(textareaContainer, "textarea-container");
classAdd(anonymousCheckboxContainer, "round-check");
classAdd(anonymousCheckboxContainer, "anonymous-checkbox-container");
classAdd(submitButton, "button");
classAdd(submitButton, "submit-button");
classAdd(markdownButton, "markdown-button");
classAdd(textareaSuperContainer, "button-margin");
attrSet(textarea, "placeholder", "Add a comment");
@@ -434,9 +527,11 @@
anonymousCheckboxLabel.innerText = "Comment anonymously";
submitButton.innerText = "Add Comment";
markdownButton.innerHTML = "<b>M &#8595;</b> &nbsp; Markdown";
textarea.oninput = autoExpander(textarea);
onclick(submitButton, submitAccountDecide, id);
onclick(markdownButton, markdownHelpShow, id);
append(textareaContainer, textarea);
append(textareaSuperContainer, textareaContainer);
@@ -446,6 +541,7 @@
if (!requireIdentification) {
append(textareaSuperContainer, anonymousCheckboxContainer);
}
append(textareaSuperContainer, markdownButton);
return textareaSuperContainer;
}
@@ -521,7 +617,7 @@
}
var json = {
"commenterToken": commenterTokenGet(),
"commenterToken": commenterToken,
"domain": parent.location.host,
"path": parent.location.pathname,
"parentHex": id,
@@ -532,6 +628,8 @@
if (!resp.success) {
errorShow(resp.message);
return;
} else {
errorHide();
}
var message = "";
@@ -560,7 +658,7 @@
"score": 0,
"state": "approved",
"direction": 0,
"creationDate": (new Date()).toISOString(),
"creationDate": new Date(),
}],
}, "root")
@@ -591,9 +689,9 @@
"#da7c30",
"#3e9651",
"#cc2529",
"#535154",
"#6b4c9a",
"#922428",
"#6b4c9a",
"#535154",
];
var total = 0;
@@ -652,16 +750,24 @@
cur.sort(function(a, b) {
if (a.commentHex === stickyCommentHex) {
return -Infinity;
}
if (b.commentHex === stickyCommentHex) {
} else if (b.commentHex === stickyCommentHex) {
return Infinity;
}
return b.score - a.score;
if (a.score !== b.score) {
return b.score - a.score;
}
if (a.creationDate < b.creationDate) {
return -1;
} else {
return 1;
}
});
var curTime = (new Date()).getTime();
var cards = create("div");
cur.forEach(function(comment) {
var mobileView = root.getBoundingClientRect()["width"] < 450;
var commenter = commenters[comment.commenterHex];
var avatar;
var card = create("div");
@@ -727,27 +833,29 @@
} else {
sticky.title = "Sticky";
}
timeago.title = comment.creationDate.toString();
card.style["borderLeft"] = "2px solid " + color;
name.innerText = commenter.name;
text.innerHTML = comment.html;
timeago.innerHTML = timeDifference((new Date()).getTime(), Date.parse(comment.creationDate));
timeago.innerHTML = timeDifference(curTime, comment.creationDate);
score.innerText = scorify(comment.score);
if (commenter.photo === "undefined") {
avatar = create("div");
avatar.style["background"] = color;
avatar.innerHTML = commenter.name[0].toUpperCase();
if (comment.commenterHex === "anonymous") {
avatar.innerHTML = "?";
avatar.style["font-weight"] = "bold";
} else {
avatar.innerHTML = commenter.name[0].toUpperCase();
}
classAdd(avatar, "avatar");
} else {
avatar = create("img");
if (commenter.provider === "google") {
attrSet(avatar, "src", commenter.photo + "?sz=50");
} else if (commenter.provider === "github") {
attrSet(avatar, "src", commenter.photo + "&s=50");
} else {
attrSet(avatar, "src", commenter.photo);
}
attrSet(avatar, "src", cdn + "/api/commenter/photo?commenterHex=" + commenter.commenterHex);
classAdd(avatar, "avatar-img");
}
@@ -755,6 +863,9 @@
if (isModerator && comment.state !== "approved") {
classAdd(card, "dark-card");
}
if (commenter.isModerator) {
classAdd(name, "moderator");
}
if (comment.state === "flagged") {
classAdd(name, "flagged");
}
@@ -803,7 +914,9 @@
onclick(sticky, global.commentSticky, comment.commentHex);
if (isAuthenticated) {
upDownOnclickSet(upvote, downvote, comment.commentHex, comment.direction);
var upDown = upDownOnclickSet(upvote, downvote, comment.commentHex, comment.direction);
upvote = upDown[0];
downvote = upDown[1];
} else {
onclick(upvote, global.loginBoxShow, null);
onclick(downvote, global.loginBoxShow, null);
@@ -884,6 +997,8 @@
if (!resp.success) {
errorShow(resp.message);
return
} else {
errorHide();
}
var card = $(ID_CARD + commentHex);
@@ -907,6 +1022,8 @@
if (!resp.success) {
errorShow(resp.message);
return
} else {
errorHide();
}
var card = $(ID_CARD + commentHex);
@@ -925,6 +1042,9 @@
function upDownOnclickSet(upvote, downvote, commentHex, direction) {
upvote = removeAllEventListeners(upvote);
downvote = removeAllEventListeners(downvote);
if (direction > 0) {
onclick(upvote, global.vote, [commentHex, [1, 0]]);
onclick(downvote, global.vote, [commentHex, [1, -1]]);
@@ -935,6 +1055,8 @@
onclick(upvote, global.vote, [commentHex, [0, 1]]);
onclick(downvote, global.vote, [commentHex, [0, -1]]);
}
return [upvote, downvote];
}
@@ -953,7 +1075,9 @@
"direction": newDirection,
};
upDownOnclickSet(upvote, downvote, commentHex, newDirection);
var upDown = upDownOnclickSet(upvote, downvote, commentHex, newDirection);
upvote = upDown[0];
downvote = upDown[1];
classRemove(upvote, "upvoted");
classRemove(downvote, "downvoted");
@@ -968,7 +1092,13 @@
post(origin + "/api/comment/vote", json, function(resp) {
if (!resp.success) {
errorShow(resp.message);
classRemove(upvote, "upvoted");
classRemove(downvote, "downvoted");
score.innerText = scorify(parseInt(score.innerText.replace(/[^\d-.]/g, "")) - newDirection + oldDirection);
upDownOnclickSet(upvote, downvote, commentHex, oldDirection);
return;
} else {
errorHide();
}
});
}
@@ -990,6 +1120,7 @@
replyButton.title = "Cancel reply";
replyButton = removeAllEventListeners(replyButton);
onclick(replyButton, global.replyCollapse, id);
};
@@ -1006,6 +1137,7 @@
replyButton.title = "Reply to this comment";
replyButton = removeAllEventListeners(replyButton);
onclick(replyButton, global.replyShow, id)
}
@@ -1023,6 +1155,7 @@
button.title = "Expand children";
button = removeAllEventListeners(button);
onclick(button, global.commentUncollapse, id);
}
@@ -1040,6 +1173,7 @@
button.title = "Collapse children";
button = removeAllEventListeners(button);
onclick(button, global.commentCollapse, id);
}
@@ -1055,6 +1189,9 @@
if (!(parentHex in parentMap)) {
parentMap[parentHex] = [];
}
comment.creationDate = new Date(comment.creationDate);
parentMap[parentHex].push(comment);
});
@@ -1118,6 +1255,8 @@
if (!resp.success) {
errorShow(resp.message);
return;
} else {
errorHide();
}
cookieSet("commentoCommenterToken", resp.commenterToken);
@@ -1167,10 +1306,14 @@
global.popupRender = function(id) {
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
var loginBox = create("div");
var ssoSubtitle = create("div");
var ssoButtonContainer = create("div");
var ssoButton = create("div");
var hr1 = create("hr");
var oauthSubtitle = create("div");
var oauthButtonsContainer = create("div");
var oauthButtons = create("div");
var hr = create("hr");
var hr2 = create("hr");
var emailSubtitle = create("div");
var emailContainer = create("div");
var email = create("div");
@@ -1186,7 +1329,7 @@
emailButton.id = ID_LOGIN_BOX_EMAIL_BUTTON;
loginLink.id = ID_LOGIN_BOX_LOGIN_LINK;
loginLinkContainer.id = ID_LOGIN_BOX_LOGIN_LINK_CONTAINER;
hr.id = ID_LOGIN_BOX_HR;
hr2.id = ID_LOGIN_BOX_HR;
oauthSubtitle.id = ID_LOGIN_BOX_OAUTH_PRETEXT;
oauthButtonsContainer.id = ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER;
@@ -1199,6 +1342,9 @@
classAdd(emailButton, "email-button");
classAdd(loginLinkContainer, "login-link-container");
classAdd(loginLink, "login-link");
classAdd(ssoSubtitle, "login-box-subtitle");
classAdd(ssoButtonContainer, "oauth-buttons-container");
classAdd(ssoButton, "oauth-buttons");
classAdd(oauthSubtitle, "login-box-subtitle");
classAdd(oauthButtonsContainer, "oauth-buttons-container");
classAdd(oauthButtons, "oauth-buttons");
@@ -1209,6 +1355,7 @@
emailSubtitle.innerText = "Login with your email address";
emailButton.innerText = "Continue";
oauthSubtitle.innerText = "Proceed with social login";
ssoSubtitle.innerText = "Proceed with " + parent.location.host + " authentication";
onclick(emailButton, global.passwordAsk, id);
onclick(loginLink, global.popupSwitch);
@@ -1219,37 +1366,68 @@
attrSet(emailInput, "placeholder", "Email address");
attrSet(emailInput, "type", "text");
for (var i = 0; i < configuredOauths.length; i++) {
var numOauthConfigured = 0;
var oauthProviders = ["google", "twitter", "github", "gitlab"];
oauthProviders.forEach(function(provider) {
if (configuredOauths[provider]) {
var button = create("button");
classAdd(button, "button");
classAdd(button, provider + "-button");
button.innerText = provider;
onclick(button, global.commentoAuth, {"provider": provider, "id": id});
append(oauthButtons, button);
numOauthConfigured++;
}
});
if (configuredOauths["sso"]) {
var button = create("button");
classAdd(button, "button");
classAdd(button, configuredOauths[i] + "-button");
classAdd(button, "sso-button");
button.innerText = configuredOauths[i];
button.innerText = "Login with Single Sign-On";
onclick(button, global.commentoAuth, {"provider": configuredOauths[i], "id": id});
onclick(button, global.commentoAuth, {"provider": "sso", "id": id});
append(oauthButtons, button);
append(ssoButton, button);
append(ssoButtonContainer, ssoButton);
append(loginBox, ssoSubtitle);
append(loginBox, ssoButtonContainer);
if (numOauthConfigured > 0 || configuredOauths["commento"]) {
append(loginBox, hr1);
}
}
if (configuredOauths.length > 0) {
if (numOauthConfigured > 0) {
append(loginBox, oauthSubtitle);
append(oauthButtonsContainer, oauthButtons);
append(loginBox, oauthButtonsContainer);
append(loginBox, hr);
oauthButtonsShown = true;
} else {
oauthButtonsShown = false;
}
append(loginBox, emailSubtitle);
append(email, emailInput);
append(email, emailButton);
append(emailContainer, email);
append(loginBox, emailContainer);
append(loginLinkContainer, loginLink);
append(loginBox, loginLinkContainer);
if (numOauthConfigured > 0 && configuredOauths["commento"]) {
append(loginBox, hr2);
}
if (configuredOauths["commento"]) {
append(loginBox, emailSubtitle);
append(loginBox, emailContainer);
append(loginBox, loginLinkContainer);
}
append(loginBox, close);
@@ -1286,15 +1464,14 @@
global.loginBoxClose();
errorShow(resp.message);
return
} else {
errorHide();
}
cookieSet("commentoCommenterToken", resp.commenterToken);
selfLoad(resp.commenter);
var loggedContainer = $(ID_LOGGED_CONTAINER);
if (loggedContainer) {
attrSet(loggedContainer, "style", "");
}
global.allShow();
remove($(ID_LOGIN));
if (id !== null) {
@@ -1334,6 +1511,8 @@
global.loginBoxClose();
errorShow(resp.message);
return
} else {
errorHide();
}
loginUP(email.value, password.value, id);
@@ -1440,6 +1619,8 @@
if (!resp.success) {
errorShow(resp.message);
return
} else {
errorHide();
}
call(callback);
@@ -1527,7 +1708,7 @@
if (cssOverride === undefined) {
global.allShow();
} else {
cssLoad(cssOverride, "window.allShow()");
cssLoad(cssOverride, "window.commento.allShow()");
}
}
@@ -1536,7 +1717,6 @@
var mainArea = $(ID_MAIN_AREA);
var modTools = $(ID_MOD_TOOLS);
var loggedContainer = $(ID_LOGGED_CONTAINER);
var footer = $(ID_FOOTER);
attrSet(mainArea, "style", "");
@@ -1547,11 +1727,6 @@
if (loggedContainer) {
attrSet(loggedContainer, "style", "");
}
attrSet(footer, "style", "");
nameWidthFix();
loadHash();
}
@@ -1594,20 +1769,26 @@
if (ID_ROOT === undefined) {
ID_ROOT = "commento";
}
noFonts = attrGet(scripts[i], "data-no-fonts");
}
}
}
function loadHash() {
if (window.location.hash && window.location.hash.startsWith("#commento-")) {
var el = $(ID_CARD + window.location.hash.split("-")[1]);
if (el === null) {
return;
}
if (window.location.hash) {
if (window.location.hash.startsWith("#commento-")) {
var el = $(ID_CARD + window.location.hash.split("-")[1]);
if (el === null) {
return;
}
classAdd(el, "highlighted-card");
el.scrollIntoView(true);
classAdd(el, "highlighted-card");
el.scrollIntoView(true);
} else if (window.location.hash.startsWith("#commento")) {
root.scrollIntoView(true);
}
}
}
@@ -1619,6 +1800,10 @@
return;
}
if (mobileView === null) {
mobileView = root.getBoundingClientRect()["width"] < 450;
}
classAdd(root, "root");
loginBoxCreate();
@@ -1627,13 +1812,22 @@
mainAreaCreate();
var footer = footerLoad();
cssLoad(cdn + "/css/commento.css", "window.commento.loadCssOverride()");
if (noFonts !== "true") {
classAdd(root, "root-font");
}
selfGet(function() {
commentsGet(function() {
modToolsCreate();
rootCreate(function() {
commentsRender();
footerLoad();
attrSet(root, "style", "");
append(root, footer);
loadHash();
global.allShow();
nameWidthFix();
call(callback);
});
});
@@ -1684,4 +1878,4 @@
readyLoad();
}(window, document));
}(window.commento, document));

96
frontend/js/count.js Normal file
View File

@@ -0,0 +1,96 @@
(function(global, document) {
"use strict";
var origin = "[[[.Origin]]]";
function post(url, data, callback) {
var xmlDoc = new XMLHttpRequest();
xmlDoc.open("POST", url, true);
xmlDoc.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlDoc.onload = function() {
callback(JSON.parse(xmlDoc.response));
};
xmlDoc.send(JSON.stringify(data));
}
function main() {
var paths = [];
var doms = [];
var as = document.getElementsByTagName("a");
for (var i = 0; i < as.length; i++) {
var href = as[i].href;
if (href === undefined) {
return;
}
href = href.replace(/^.*\/\/[^\/]+/, "");
if (href.endsWith("#commento")) {
var path = href.substr(0, href.indexOf("#commento"));
if (path.startsWith(parent.location.host)) {
path = path.substr(parent.location.host.length);
}
paths.push(path);
doms.push(as[i]);
}
}
var json = {
"domain": parent.location.host,
"paths": paths,
};
post(origin + "/api/comment/count", json, function(resp) {
if (!resp.success) {
console.log("[commento] error: " + resp.message);
return;
}
for (var i = 0; i < paths.length; i++) {
var count = 0;
if (paths[i] in resp.commentCounts) {
count = resp.commentCounts[paths[i]];
}
doms[i].innerText = count + " " + (count === 1 ? "comment" : "comments");
}
});
}
var initted = false;
function init() {
if (initted) {
return;
}
initted = true;
main(undefined);
}
var readyLoad = function() {
var readyState = document.readyState;
if (readyState === "loading") {
// The document is still loading. The div we need to fill might not have
// been parsed yet, so let's wait and retry when the readyState changes.
// If there is more than one state change, we aren't affected because we
// have a double-call protection in init().
document.addEventListener("readystatechange", readyLoad);
} else if (readyState === "interactive") {
// The document has been parsed and DOM objects are now accessible. While
// JS, CSS, and images are still loading, we don't need to wait.
init();
} else if (readyState === "complete") {
// The page has fully loaded (including JS, CSS, and images). From our
// point of view, this is practically no different from interactive.
init();
}
};
readyLoad();
}(window, document));

View File

@@ -20,6 +20,18 @@
}
// Clears all comments in a domain.
global.domainClearHandler = function() {
var data = global.dashboard.$data;
global.domainClear(data.domains[data.cd].domain, function(success) {
if (success) {
document.location = global.origin + "/dashboard";
}
});
}
// Freezes a domain.
global.domainFreezeHandler = function() {
var data = global.dashboard.$data;

View File

@@ -102,6 +102,8 @@
global.vs("domains", resp.domains);
global.vs("configuredOauths", resp.configuredOauths);
if (callback !== undefined) {
callback();
}
@@ -118,14 +120,14 @@
};
global.post(global.origin + "/api/domain/update", json, function(resp) {
if (callback !== undefined) {
callback(resp.success);
}
if (!resp.success) {
global.globalErrorShow(resp.message);
return;
}
if (callback !== undefined) {
callback(resp.success);
}
});
}
@@ -149,4 +151,24 @@
});
}
// Clears the comments in a domain.
global.domainClear = function(domain, callback) {
var json = {
"ownerToken": global.cookieGet("commentoOwnerToken"),
"domain": domain,
};
global.post(global.origin + "/api/domain/clear", json, function(resp) {
if (!resp.success) {
global.globalErrorShow(resp.message);
return;
}
if (callback !== undefined) {
callback(resp.success);
}
});
}
} (window.commento, document));

View File

@@ -19,4 +19,27 @@
});
};
global.ssoProviderChangeHandler = function() {
var data = global.dashboard.$data;
if (data.domains[data.cd].ssoSecret === "") {
var json = {
"ownerToken": global.cookieGet("commentoOwnerToken"),
"domain": data.domains[data.cd].domain,
};
global.post(global.origin + "/api/domain/sso/new", json, function(resp) {
if (!resp.success) {
global.globalErrorShow(resp.message);
return;
}
data.domains[data.cd].ssoSecret = resp.ssoSecret;
$("#sso-secret").val(data.domains[data.cd].ssoSecret);
});
} else {
$("#sso-secret").val(data.domains[data.cd].ssoSecret);
}
};
} (window.commento, document));

View File

@@ -31,7 +31,7 @@
{
"id": "general",
"text": "General",
"meaning": "Email settings, data export",
"meaning": "Names, authentication, and export",
"selected": false,
"open": global.generalOpen,
},
@@ -59,7 +59,7 @@
{
"id": "danger",
"text": "Danger Zone",
"meaning": "Delete or freeze domain",
"meaning": "Here be dragons",
"selected": false,
"open": global.dangerOpen,
},
@@ -72,6 +72,9 @@
// list of domains dynamically loaded; obviously mutable
domains: [{show: false, viewsLast30Days: global.numberify(0), commentsLast30Days: global.numberify(0), moderators: []}],
// configured oauth providers that will be filled in after a backend request
configuredOauths: {},
// whether or not to show the settings column; mutable because we do not
// show the column until a domain has been selected
showSettings: false,

View File

@@ -4,7 +4,9 @@
(document);
// Talks to the API and sends an reset email.
global.sendResetHex = function() {
global.sendResetHex = function(event) {
event.preventDefault();
var allOk = global.unfilledMark(["#email"], function(el) {
el.css("border-bottom", "1px solid red");
});

View File

@@ -43,7 +43,9 @@
// Logs the user in and redirects to the dashboard.
global.login = function() {
global.login = function(event) {
event.preventDefault();
var allOk = global.unfilledMark(["#email", "#password"], function(el) {
el.css("border-bottom", "1px solid red");
});

View File

@@ -1,7 +1,9 @@
(function (global, document) {
"use strict";
global.resetPassword = function() {
global.resetPassword = function(event) {
event.preventDefault();
var allOk = global.unfilledMark(["#password", "#password2"], function(el) {
el.css("border-bottom", "1px solid red");
});

View File

@@ -4,7 +4,9 @@
// Signs up the user and redirects to either the login page or the email
// confirmation, depending on whether or not SMTP is configured in the
// backend.
global.signup = function() {
global.signup = function(event) {
event.preventDefault();
if ($("#password").val() !== $("#password2").val()) {
global.textSet("#err", "The two passwords don't match");
return;

View File

@@ -5,7 +5,6 @@
<script src="[[[.CdnPrefix]]]/js/login.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Login</title>
</head>
@@ -25,24 +24,26 @@
<div class="auth-form-container">
<div class="auth-form">
<div class="form-title">
Login to continue
</div>
<form onsubmit="window.commento.login(event)">
<div class="form-title">
Login to continue
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="row">
<div class="label">Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="row">
<div class="label">Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<button id="button" class="button" onclick="window.commento.login()">Login</button>
<button id="button" class="button" type="submit">Login</button>
</form>
<a class="link" href="[[[.Origin]]]/forgot">Trouble logging in? Reset your password.</a>
<a class="link" href="[[[.Origin]]]/signup">Don't have an account yet? Sign up.</a>

View File

@@ -5,7 +5,6 @@
<script src="[[[.CdnPrefix]]]/js/reset.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Reset your Password</title>
</head>
@@ -17,23 +16,25 @@
<div class="auth-form-container">
<div class="auth-form">
<div class="form-title">
Reset your Password
</div>
<form onsubmit="window.commento.resetPassword(event)">
<div class="form-title">
Reset your Password
</div>
<div class="row">
<div class="label">New Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="row">
<div class="label">New Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="row">
<div class="label">Confirm Password</div>
<input class="input" type="password" name="password2" id="password2" placeholder="">
</div>
<div class="row">
<div class="label">Confirm Password</div>
<input class="input" type="password" name="password2" id="password2" placeholder="">
</div>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<button id="reset-button" class="button" onclick="window.commento.resetPassword()">Reset Password</button>
<div class="err" id="err"></div>
<div class="msg" id="msg"></div>
<button id="reset-button" class="button" type="submit">Reset Password</button>
</form>
</div>
</div>

View File

@@ -21,13 +21,14 @@
font-size: 14px;
color: #555;
border: none;
display: block;
display: table;
z-index: 1;
margin-left: 48px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: fit-content;
cursor: pointer;
}
.commento-flagged::after {
@@ -39,6 +40,19 @@
margin-left: 8px;
padding: 2px 6px 2px 6px;
border-radius: 100px;
line-height: 17px;
}
.commento-moderator::after {
content: "Moderator";
text-transform: uppercase;
font-size: 10px;
background: $green-7;
color: white;
margin-left: 8px;
padding: 2px 6px 2px 6px;
border-radius: 100px;
line-height: 17px;
}
.commento-subtitle {

View File

@@ -1,23 +1,21 @@
@import "colors-main.scss";
code {
background: $red-1;
font-family: monospace;
line-height: 1.5;
color: $red-6;
padding: 2px;
margin: 2px;
font-size: 13px;
}
a {
color: $blue-6;
border-bottom: 1px solid $blue-6;
outline: none;
text-decoration: none;
}
a:focus {
box-shadow: 0 0 0 1px rgba(87, 85, 217, .2);
blockquote {
margin: 0 0 0 8px;
padding: 0 0 0 5px;
border-left: 2px solid $gray-5;
color: $gray-6;
}
.commento-button {
@@ -36,7 +34,6 @@ a:focus {
border: 1px solid transparent;
border-radius: 3px;
color: #fff;
width: 100px;
margin-left: 5px;
margin-right: 5px;
}

View File

@@ -2,7 +2,6 @@
.commento-footer {
margin: 36px 0px 12px 0px;
border-top: 1px solid $gray-1;
padding-right: 12px;
.commento-logo-container {
@@ -16,19 +15,16 @@
align-items: center;
padding: 5px;
border-radius: 3px;
}
.commento-logo-svg {
display: inline;
width: 18px;
height: 18px;
margin-right: 8px;
outline: none;
}
.commento-logo-text::before {
content: "Powered by ";
font-weight: 400;
}
.commento-logo-text {
font-size: 13px;
color: $gray-6;
color: $gray-7;
display: inline;
line-height: 24px;
position: relative;

View File

@@ -25,7 +25,7 @@ textarea::placeholder {
textarea {
display: inline-block;
font-family: 'Source Sans Pro', sans-serif;
white-space: pre-wrap;
padding: 8px;
outline: none;
overflow: auto;
@@ -67,7 +67,7 @@ textarea {
}
.commento-button-margin {
padding-bottom: 60px;
padding-top: 4px;
}
.commento-anonymous-checkbox-container {
@@ -90,3 +90,34 @@ textarea {
margin-top: -1px;
}
}
.commento-markdown-button {
color: $gray-6;
margin: 0px 16px;
font-size: 12px;
text-transform: uppercase;
border: none;
line-height: 58px;
font-weight: 400;
cursor: pointer;
b {
font-size: 12px;
}
}
.commento-markdown-help {
border: 1px solid $gray-3;
padding: 8px;
tr {
td {
padding: 0px 6px;
pre {
display: inline;
font-family: monospace;
font-size: 13px;
}
}
}
}

View File

@@ -39,6 +39,7 @@
position: absolute;
top: 6px;
left: 48px;
cursor: pointer;
}
}
}

View File

@@ -11,11 +11,8 @@
.commento-login-box {
width: 90%;
max-width: 500px;
min-height: 125px;
min-height: 100px;
background: $gray-1;
box-shadow: 0 4px 6px rgba(50,50,93,.11),0 1px 3px rgba(0,0,0,.08);
border: 1px solid transparent;
border-radius: 3px;
z-index: 100;
position: absolute;
top: 8px;

View File

@@ -12,6 +12,7 @@
margin-left: 12px;
background: none;
border: none;
display: inline;
}
}

View File

@@ -5,8 +5,6 @@
justify-content: center;
.commento-oauth-buttons {
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
z-index: 1;
@@ -16,12 +14,35 @@
background: #dd4b39;
text-transform: uppercase;
font-size: 13px;
width: 70px;
}
.commento-github-button {
background: #000000;
text-transform: uppercase;
font-size: 13px;
width: 70px;
}
.commento-twitter-button {
background: #00aced;
text-transform: uppercase;
font-size: 13px;
width: 70px;
}
.commento-gitlab-button {
background: #fc6d26;
text-transform: uppercase;
font-size: 13px;
width: 70px;
}
.commento-sso-button {
background: #000000;
text-transform: uppercase;
font-size: 13px;
width: 200px;
}
}
}

View File

@@ -1,15 +1,23 @@
@import "source-sans.scss";
.commento-root-min-height {
min-height: 350px;
min-height: 430px;
}
.commento-root-font {
* {
font-family: 'Source Sans Pro', sans-serif !important;
}
}
.commento-root {
overflow-x: hidden;
padding: 0px;
width: 100%;
font-family: inherit;
* {
font-family: "Source Sans Pro", "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
font-family: inherit;
font-size: 15px;
line-height: 1.5;
color: #50596c;
@@ -30,7 +38,7 @@
}
.commento-blurred {
filter: blur(4px);
opacity: 0.4;
}
.commento-main-area {

View File

@@ -1,6 +1,7 @@
@import "colors-main.scss";
@import "source-sans.scss";
html {
html, input, button, textarea {
font-family: 'Source Sans Pro', sans-serif;
font-size: 14px;
color: $gray-7;

View File

@@ -3,47 +3,6 @@
@import "checkbox.scss";
@import "button.scss";
.subscription-nag {
position: absolute;
top: 16px;
left: calc(50% - 200px);
height: 40px;
width: 400px;
border-radius: 3px;
-webkit-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
-moz-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
background: $gray-7;
color: $gray-0;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 10000;
animation: shake .5s linear;
-webkit-animation: shake .5s linear;
animation-delay: 1s;
-webkit-animation-delay: 1s;
}
@-webkit-keyframes shake {
8%, 41% {
-webkit-transform: translateX(-5px);
}
25%, 58% {
-webkit-transform: translateX(5px);
}
75% {
-webkit-transform: translateX(-2px);
}
92% {
-webkit-transform: translateX(2px);
}
0%, 100% {
-webkit-transform: translateX(0);
}
}
.global-error, .global-ok {
position: absolute;
bottom: 0px;
@@ -71,9 +30,12 @@ body {
margin: 0px;
padding: 0px;
list-style: none;
border-bottom: 1px solid $gray-4;
border-bottom: 1px solid $gray-1;
li {
font-weight: bold;
color: $gray-7;
font-size: 14px;
background: none;
display: inline-block;
padding: 10px 15px;
@@ -83,8 +45,14 @@ body {
}
li.current {
border-bottom: 1px solid $blue-6;
transition: all 0.1s;
background: $gray-1;
color: $blue-7;
}
li.current:hover {
background: $gray-1;
color: $blue-7;
}
}
@@ -107,6 +75,38 @@ body {
}
}
.action-buttons-container {
.action-buttons {
.action-button {
padding: 8px 16px 8px 16px;
display: flex;
border: 1px solid $gray-2;
margin: 8px;
.left {
padding: 8px;
.title {
font-weight: bold;
color: $gray-7;
font-size: 14px;
}
.subtitle {
color: $gray-6;
font-size: 14px;
}
}
.right {
float: right;
height: 100%;
margin: auto;
}
}
}
}
@import "email-main.scss";
.mod-emails-container {
@@ -281,7 +281,7 @@ body {
}
.setting-subtitle {
color: $gray-5;
color: $gray-6;
}
.super-setting {
@@ -306,23 +306,17 @@ body {
.pane-setting:hover {
color: $gray-6;
-webkit-box-shadow: inset -2px 0px 0 -1px $gray-4;
-moz-box-shadow: inset -2px 0px 0 -1px $gray-4;
box-shadow: inset -2px 0px 0 -1px $gray-4;
background: $gray-1;
}
.selected {
color: $blue-6;
-webkit-box-shadow: inset -2px 0px 0 -1px $blue-6;
-moz-box-shadow: inset -2px 0px 0 -1px $blue-6;
box-shadow: inset -2px 0px 0 -1px $blue-6;
color: $blue-7;
background: $gray-2;
}
.selected:hover {
color: $blue-7;
-webkit-box-shadow: inset -2px 0px 0 -1px $blue-6;
-moz-box-shadow: inset -2px 0px 0 -1px $blue-6;
box-shadow: inset -2px 0px 0 -1px $blue-6;
background: $gray-2;
}
}
@@ -373,14 +367,37 @@ body {
font-size: 13px;
line-height: 17px;
text-align: center;
a {
border: none;
}
}
}
}
.question {
font-size: 15px;
color: $gray-7;
margin-bottom: 10px;
padding: 8px 0px 8px 0px;
margin: 8px 0px 8px 0px;
display: flex;
width: 100%;
.title {
font-weight: bold;
color: $gray-7;
font-size: 14px;
width: 35%;
padding-top: 12px;
}
.answer {
font-size: 14px;
width: 100%;
}
}
.warning {
color: $orange-8;
font-weight: bold;
}
.float-right {
@@ -527,56 +544,11 @@ body {
.input::placeholder {
color: $gray-4;
}
}
.theme {
display: block;
width: calc(100% - 20px);
border: 1px solid white;
-webkit-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
-moz-box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
border-radius: 3px;
padding: 10px;
cursor: pointer;
margin-bottom: 20px;
background: white;
opacity: 0.5;
filter: alpha(opacity=50);
transition: all 0.3s;
.theme-title {
font-size: 24px;
text-align: center;
padding: 10px;
.monospace {
font-family: monospace;
font-size: 11px;
}
.theme-subtitle {
font-size: 15px;
text-align: center;
padding: 10px;
color: $gray-5;
}
.theme-image {
width: calc(100% - 40px);
padding: 20px;
}
}
.theme:hover {
opacity: 0.8;
filter: alpha(opacity=80);
}
.selectedtheme {
opacity: 1;
filter: alpha(opacity=100);
}
.selectedtheme:hover {
opacity: 1;
filter: alpha(opacity=100);
}
.no-border {
@@ -585,7 +557,7 @@ body {
.indent {
margin-top: 0px;
padding-left: 32px;
padding-left: 35px;
}
.stat {
@@ -667,12 +639,11 @@ foreignObject {
}
.red-button {
border: 1px solid $gray-3;
outline: none;
color: $red-8;
}
.red-button:hover {
border: 1px solid $red-6;
color: $red-7;
}
.green-button {
@@ -688,7 +659,7 @@ foreignObject {
}
.orange-button {
color: $orange-7;
color: $orange-8;
}
.orange-button:hover {
@@ -793,6 +764,6 @@ foreignObject {
}
code {
font-family: 'Source Code Pro', monospace;
font-family: monospace;
font-size: 13px;
}

View File

@@ -15,6 +15,7 @@
max-width: 400px;
.commento-input {
display: inline;
height: 40px;
background: $white;
border: none;

View File

@@ -2,22 +2,88 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic-ext.woff2) format('woff2');
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic.woff2) format('woff2');
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-greek.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url([[[.CdnPrefix]]]/fonts/source-sans-300-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
@@ -26,6 +92,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek.woff2) format('woff2');
unicode-range: U+0370-03FF;
@@ -34,6 +101,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
@@ -42,6 +110,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
@@ -50,6 +119,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
@@ -58,22 +128,25 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cryllic-ext.woff2) format('woff2');
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cyrillic-ext.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cryllic.woff2) format('woff2');
unicode-range: U+0700-045F, U+0490-0491, U+04B0-04B1, U+2116;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
@@ -82,6 +155,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek.woff2) format('woff2');
unicode-range: U+0370-03FF;
@@ -90,6 +164,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-vietnamese.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
@@ -98,6 +173,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
@@ -106,6 +182,7 @@
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-display: swap;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;

View File

@@ -5,7 +5,6 @@
<script src="[[[.CdnPrefix]]]/js/signup.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Signup</title>
</head>
@@ -24,38 +23,40 @@
<div class="auth-form-container">
<div class="auth-form">
<div class="form-title">
Create an account
</div>
<form onsubmit="window.commento.signup(event)">
<div class="form-title">
Create an account
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="row">
<div class="label">Email Address</div>
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
</div>
<div class="row">
<div class="label">Full Name</div>
<input class="input" type="text" name="name" id="name" placeholder="Full Name">
</div>
<div class="row">
<div class="label">Full Name</div>
<input class="input" type="text" name="name" id="name" placeholder="Full Name">
</div>
<div class="row">
<div class="label">Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="row">
<div class="label">Password</div>
<input class="input" type="password" name="password" id="password" placeholder="">
</div>
<div class="row">
<div class="label">Confirm Password</div>
<input class="input" type="password" name="password2" id="password2" placeholder="">
</div>
<div class="row">
<div class="label">Confirm Password</div>
<input class="input" type="password" name="password2" id="password2" placeholder="">
</div>
<input type="hidden" name="plan" id="plan" value="">
<input type="hidden" name="plan" id="plan" value="">
<div class="err" id="err"></div>
<div class="err" id="err"></div>
<p class="cent">
</p>
<p class="cent">
</p>
<button id="signup-button" class="button" onclick="window.commento.signup()">Sign up</button>
<button id="signup-button" class="button" type="submit">Sign up</button>
</form>
<a class="link" href="[[[.Origin]]]/login">Already have an account? Login instead.</a>
</div>

View File

@@ -5,7 +5,6 @@
<script src="[[[.CdnPrefix]]]/js/unsubscribe.js"></script>
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/unsubscribe.css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
<title>Commento: Unsubscribe</title>
</head>

View File

@@ -9,7 +9,11 @@ ctrl_c() {
exit
}
version=devel
if [[ "$1" == "" ]]; then
version=devel
else
version=$1
fi
binary_pid=
if make $version -j$(($(nproc) + 1)); then

View File

@@ -5,40 +5,6 @@
<title>You have {{ .Subject }}</title>
<style type="text/css">
@media only screen and (max-width:600px) {
.options {
float: none;
margin-bottom: 16px;
text-align: right;
}
.options::before {
content: "Options:";
float: left;
text-transform: uppercase;
color: #495057;
font-size: 12px;
font-weight: bold;
}
.option {
padding: 6px 8px;
margin: 10px 5px;
color: white;
border-radius: 2px;
}
.green {
background: #2f9e44;
}
.red {
background: #f03e3e;
}
.blue {
background: #1c7ed6;
}
.gray {
background: #495057;
}
.header {
padding-right: 0px;
}
.logo {
display: block;
float: none;