Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
504d1bf866 | ||
|
|
a4387b62ec | ||
|
|
1cce90dcf2 | ||
|
|
8820dcd59e | ||
|
|
638f7ba197 | ||
|
|
e1effd2a45 | ||
|
|
a9c48a8394 | ||
|
|
feeda79923 | ||
|
|
9d4ed4ca9f | ||
|
|
0b37b33530 | ||
|
|
b67d2ba58c | ||
|
|
5b6d31ce31 | ||
|
|
409af7f205 | ||
|
|
060520bd7f | ||
|
|
e396e043c6 | ||
|
|
4a189fc698 | ||
|
|
cac1cfa84a | ||
|
|
6317b384d9 | ||
|
|
fa2ccfe42e | ||
|
|
30772ec720 | ||
|
|
eec10491d6 | ||
|
|
e46f9cf9e7 | ||
|
|
1d1cd46c2b | ||
|
|
536ec14b93 | ||
|
|
45c6361805 | ||
|
|
a455ff54bc | ||
|
|
0e54739980 | ||
|
|
7e9b3e5b26 | ||
|
|
86393ad9ab | ||
|
|
672863b48f | ||
|
|
97f17d32ee | ||
|
|
be10baf971 | ||
|
|
9607c15c2b | ||
|
|
65ea597c08 | ||
|
|
850dfc9712 | ||
|
|
3c9ba43ad1 | ||
|
|
b4790397c9 | ||
|
|
9d6955b81e | ||
|
|
5ffdf9988a | ||
|
|
5f1d46c7b2 | ||
|
|
a2c8a73d3e | ||
|
|
4945e53553 | ||
|
|
88d4f8afcf | ||
|
|
15b1640f89 | ||
|
|
216016a4be | ||
|
|
a7cd8066f8 | ||
|
|
295318e6a6 | ||
|
|
d26b6f6e9f | ||
|
|
c8a2ece0d6 | ||
|
|
e9ba79974b | ||
|
|
beb54035cf | ||
|
|
1ccc95fae4 | ||
|
|
fa3fa39696 | ||
|
|
b9bf9e360a | ||
|
|
ecbb505c97 | ||
|
|
789a58bd7a | ||
|
|
c30da607cb | ||
|
|
be197f2b69 | ||
|
|
d4b466b04f | ||
|
|
95093326e0 | ||
|
|
3e5c1c2656 | ||
|
|
c07f3e8b9f | ||
|
|
d367ac8391 | ||
|
|
0609ef0e27 | ||
|
|
adb87d7029 | ||
|
|
23bec48ebb | ||
|
|
685f3a3a58 | ||
|
|
f4489c9921 | ||
|
|
352c93bf88 | ||
|
|
27caa60e0c | ||
|
|
e0f188909f | ||
|
|
0b78e9e70c |
@@ -50,8 +50,8 @@ aws-upload-tags:
|
|||||||
- export PATH=$PATH:/go/bin
|
- export PATH=$PATH:/go/bin
|
||||||
- cd /go/src/$CI_PROJECT_NAME
|
- cd /go/src/$CI_PROJECT_NAME
|
||||||
- make prod
|
- make prod
|
||||||
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tgz .
|
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tar.gz .
|
||||||
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tgz s3://commento-release/
|
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tar.gz s3://commento-release/
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
stage: build-docker
|
stage: build-docker
|
||||||
|
|||||||
13
api/Gopkg.lock
generated
13
api/Gopkg.lock
generated
@@ -25,6 +25,14 @@
|
|||||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||||
version = "v1.1.0"
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:d03d0fae6a7a80e89c540787a69ab6e0d3b773fdb3303c0b3d96a15490c6ef32"
|
||||||
|
name = "github.com/gomodule/oauth1"
|
||||||
|
packages = ["oauth"]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "9a59ed3b0a84f454c260f2f8f82918223fc5630f"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
|
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
|
||||||
name = "github.com/gorilla/context"
|
name = "github.com/gorilla/context"
|
||||||
@@ -116,11 +124,12 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:82e6e4dc5ab71680d89684e4649be630fdeeaf81feb8e88e4a56273a0cd4d966"
|
digest = "1:341ceeee37101c62dae441691406bf4ecc71bbeb7b424417879fe88d9f88f487"
|
||||||
name = "golang.org/x/oauth2"
|
name = "golang.org/x/oauth2"
|
||||||
packages = [
|
packages = [
|
||||||
".",
|
".",
|
||||||
"github",
|
"github",
|
||||||
|
"gitlab",
|
||||||
"google",
|
"google",
|
||||||
"internal",
|
"internal",
|
||||||
"jws",
|
"jws",
|
||||||
@@ -153,6 +162,7 @@
|
|||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
input-imports = [
|
input-imports = [
|
||||||
"github.com/adtac/go-akismet/akismet",
|
"github.com/adtac/go-akismet/akismet",
|
||||||
|
"github.com/gomodule/oauth1/oauth",
|
||||||
"github.com/gorilla/handlers",
|
"github.com/gorilla/handlers",
|
||||||
"github.com/gorilla/mux",
|
"github.com/gorilla/mux",
|
||||||
"github.com/lib/pq",
|
"github.com/lib/pq",
|
||||||
@@ -164,6 +174,7 @@
|
|||||||
"golang.org/x/net/html",
|
"golang.org/x/net/html",
|
||||||
"golang.org/x/oauth2",
|
"golang.org/x/oauth2",
|
||||||
"golang.org/x/oauth2/github",
|
"golang.org/x/oauth2/github",
|
||||||
|
"golang.org/x/oauth2/gitlab",
|
||||||
"golang.org/x/oauth2/google",
|
"golang.org/x/oauth2/google",
|
||||||
]
|
]
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type comment struct {
|
|||||||
Html string `json:"html"`
|
Html string `json:"html"`
|
||||||
ParentHex string `json:"parentHex"`
|
ParentHex string `json:"parentHex"`
|
||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
State string `json:"state"`
|
State string `json:"state,omitempty"`
|
||||||
CreationDate time.Time `json:"creationDate"`
|
CreationDate time.Time `json:"creationDate"`
|
||||||
Direction int `json:"direction"`
|
Direction int `json:"direction"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,51 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lib/pq"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commentCount(domain string, path string) (int, error) {
|
func commentCount(domain string, paths []string) (map[string]int, error) {
|
||||||
// path can be empty
|
commentCounts := map[string]int{}
|
||||||
|
|
||||||
if domain == "" {
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.CommentCount, nil
|
commentCounts[path] = commentCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentCounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func commentCountHandler(w http.ResponseWriter, r *http.Request) {
|
func commentCountHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
type request struct {
|
type request struct {
|
||||||
Domain *string `json:"domain"`
|
Domain *string `json:"domain"`
|
||||||
Path *string `json:"path"`
|
Paths *[]string `json:"paths"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var x request
|
var x request
|
||||||
@@ -31,13 +55,12 @@ func commentCountHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
domain := domainStrip(*x.Domain)
|
domain := domainStrip(*x.Domain)
|
||||||
path := *x.Path
|
|
||||||
|
|
||||||
count, err := commentCount(domain, path)
|
commentCounts, err := commentCount(domain, *x.Paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyMarshal(w, response{"success": true, "count": count})
|
bodyMarshal(w, response{"success": true, "commentCounts": commentCounts})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", "**bar**", "approved", time.Now().UTC())
|
||||||
commentNew(commenterHex, "example.com", "/path.html", "root", "**baz**", "unapproved", 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 {
|
if err != nil {
|
||||||
t.Errorf("unexpected error counting comments: %v", err)
|
t.Errorf("unexpected error counting comments: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if count != 2 {
|
if counts["/path.html"] != 2 {
|
||||||
t.Errorf("expected count=2 got count=%d", count)
|
t.Errorf("expected count=2 got count=%d", counts["/path.html"])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,25 +29,25 @@ func TestCommentCountBasics(t *testing.T) {
|
|||||||
func TestCommentCountNewPage(t *testing.T) {
|
func TestCommentCountNewPage(t *testing.T) {
|
||||||
failTestOnError(t, setupTestEnv())
|
failTestOnError(t, setupTestEnv())
|
||||||
|
|
||||||
count, err := commentCount("example.com", "/path.html")
|
counts, err := commentCount("example.com", []string{"/path.html"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error counting comments: %v", err)
|
t.Errorf("unexpected error counting comments: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if count != 0 {
|
if counts["/path.html"] != 0 {
|
||||||
t.Errorf("expected count=0 got count=%d", count)
|
t.Errorf("expected count=0 got count=%d", counts["/path.html"])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommentCountEmpty(t *testing.T) {
|
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)
|
t.Errorf("unexpected error counting comments on empty path: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := commentCount("", ""); err == nil {
|
if _, err := commentCount("", []string{""}); err == nil {
|
||||||
t.Errorf("expected error not found counting comments with empty everything")
|
t.Errorf("expected error not found counting comments with empty everything")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ func commentList(commenterHex string, domain string, path string, includeUnappro
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
SELECT commentHex, commenterHex, markdown, html, parentHex, score, state, creationDate
|
SELECT
|
||||||
|
commentHex,
|
||||||
|
commenterHex,
|
||||||
|
markdown,
|
||||||
|
html,
|
||||||
|
parentHex,
|
||||||
|
score,
|
||||||
|
state,
|
||||||
|
creationDate
|
||||||
FROM comments
|
FROM comments
|
||||||
WHERE
|
WHERE
|
||||||
comments.domain = $1 AND
|
comments.domain = $1 AND
|
||||||
@@ -21,13 +29,9 @@ func commentList(commenterHex string, domain string, path string, includeUnappro
|
|||||||
|
|
||||||
if !includeUnapproved {
|
if !includeUnapproved {
|
||||||
if commenterHex == "anonymous" {
|
if commenterHex == "anonymous" {
|
||||||
statement += `
|
statement += `AND state = 'approved'`
|
||||||
AND state = 'approved'
|
|
||||||
`
|
|
||||||
} else {
|
} else {
|
||||||
statement += `
|
statement += `AND (state = 'approved' OR commenterHex = $3)`
|
||||||
AND (state = 'approved' OR commenterHex = $3)
|
|
||||||
`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +58,15 @@ func commentList(commenterHex string, domain string, path string, includeUnappro
|
|||||||
comments := []comment{}
|
comments := []comment{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
c := comment{}
|
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
|
return nil, nil, errorInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +132,8 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
commenterHex := "anonymous"
|
commenterHex := "anonymous"
|
||||||
isModerator := false
|
isModerator := false
|
||||||
|
modList := map[string]bool{}
|
||||||
|
|
||||||
if *x.CommenterToken != "anonymous" {
|
if *x.CommenterToken != "anonymous" {
|
||||||
c, err := commenterGetByCommenterToken(*x.CommenterToken)
|
c, err := commenterGetByCommenterToken(*x.CommenterToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -134,11 +148,15 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, mod := range d.Moderators {
|
for _, mod := range d.Moderators {
|
||||||
|
modList[mod.Email] = true
|
||||||
if mod.Email == c.Email {
|
if mod.Email == c.Email {
|
||||||
isModerator = true
|
isModerator = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for _, mod := range d.Moderators {
|
||||||
|
modList[mod.Email] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
domainViewRecord(domain, commenterHex)
|
domainViewRecord(domain, commenterHex)
|
||||||
@@ -149,16 +167,32 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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{
|
bodyMarshal(w, response{
|
||||||
"success": true,
|
"success": true,
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"comments": comments,
|
"comments": comments,
|
||||||
"commenters": commenters,
|
"commenters": _commenters,
|
||||||
"requireModeration": d.RequireModeration,
|
"requireModeration": d.RequireModeration,
|
||||||
"requireIdentification": d.RequireIdentification,
|
"requireIdentification": d.RequireIdentification,
|
||||||
"isFrozen": d.State == "frozen",
|
"isFrozen": d.State == "frozen",
|
||||||
"isModerator": isModerator,
|
"isModerator": isModerator,
|
||||||
"attributes": p,
|
"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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ type commenter struct {
|
|||||||
Photo string `json:"photo"`
|
Photo string `json:"photo"`
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
JoinDate time.Time `json:"joinDate,omitempty"`
|
JoinDate time.Time `json:"joinDate,omitempty"`
|
||||||
|
IsModerator bool `json:"isModerator"`
|
||||||
}
|
}
|
||||||
|
|||||||
34
api/commenter_photo.go
Normal file
34
api/commenter_photo.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -50,6 +50,12 @@ func configParse() error {
|
|||||||
|
|
||||||
"GITHUB_KEY": "",
|
"GITHUB_KEY": "",
|
||||||
"GITHUB_SECRET": "",
|
"GITHUB_SECRET": "",
|
||||||
|
|
||||||
|
"TWITTER_KEY": "",
|
||||||
|
"TWITTER_SECRET": "",
|
||||||
|
|
||||||
|
"GITLAB_KEY": "",
|
||||||
|
"GITLAB_SECRET": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range defaults {
|
for key, value := range defaults {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var version = "v1.6.1"
|
var version = "v1.7.0"
|
||||||
|
|||||||
25
api/cron_sso_token.go
Normal file
25
api/cron_sso_token.go
Normal 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
|
||||||
|
}
|
||||||
@@ -17,4 +17,12 @@ type domain struct {
|
|||||||
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
|
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
|
||||||
Moderators []moderator `json:"moderators"`
|
Moderators []moderator `json:"moderators"`
|
||||||
EmailNotificationPolicy string `json:"emailNotificationPolicy"`
|
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
82
api/domain_clear.go
Normal 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})
|
||||||
|
}
|
||||||
@@ -19,17 +19,6 @@ func domainDelete(domain string) error {
|
|||||||
return errorNoSuchDomain
|
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 = `
|
statement = `
|
||||||
DELETE FROM views
|
DELETE FROM views
|
||||||
WHERE views.domain = $1;
|
WHERE views.domain = $1;
|
||||||
@@ -50,23 +39,9 @@ func domainDelete(domain string) error {
|
|||||||
return errorInternal
|
return errorInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
statement = `
|
// comments, votes, and pages are handled by domainClear
|
||||||
DELETE FROM comments
|
if err = domainClear(domain); err != nil {
|
||||||
WHERE comments.domain = $1;
|
logger.Errorf("cannot clear domain: %v", err)
|
||||||
`
|
|
||||||
_, 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 errorInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,26 @@ func domainGet(dmn string) (domain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
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
|
FROM domains
|
||||||
WHERE domain = $1;
|
WHERE domain = $1;
|
||||||
`
|
`
|
||||||
@@ -16,7 +35,26 @@ func domainGet(dmn string) (domain, error) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
d := domain{}
|
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
|
return d, errorNoSuchDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,26 @@ func domainList(ownerHex string) ([]domain, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement := `
|
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
|
FROM domains
|
||||||
WHERE ownerHex=$1;
|
WHERE ownerHex=$1;
|
||||||
`
|
`
|
||||||
@@ -24,7 +43,26 @@ func domainList(ownerHex string) ([]domain, error) {
|
|||||||
domains := []domain{}
|
domains := []domain{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
d := domain{}
|
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)
|
logger.Errorf("cannot Scan domain: %v", err)
|
||||||
return nil, errorInternal
|
return nil, errorInternal
|
||||||
}
|
}
|
||||||
@@ -63,5 +101,14 @@ func domainListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,6 +11,10 @@ func domainNew(ownerHex string, name string, domain string) error {
|
|||||||
return errorMissingField
|
return errorMissingField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(domain, "/") {
|
||||||
|
return errorInvalidDomain
|
||||||
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
domains (ownerHex, name, domain, creationDate)
|
domains (ownerHex, name, domain, creationDate)
|
||||||
|
|||||||
69
api/domain_sso.go
Normal file
69
api/domain_sso.go
Normal 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})
|
||||||
|
}
|
||||||
@@ -5,13 +5,46 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func domainUpdate(d domain) error {
|
func domainUpdate(d domain) error {
|
||||||
|
if d.SsoProvider && d.SsoUrl == "" {
|
||||||
|
return errorMissingField
|
||||||
|
}
|
||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
UPDATE domains
|
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;
|
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 {
|
if err != nil {
|
||||||
logger.Errorf("cannot update non-moderators: %v", err)
|
logger.Errorf("cannot update non-moderators: %v", err)
|
||||||
return errorInternal
|
return errorInternal
|
||||||
|
|||||||
@@ -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 errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
|
||||||
var errorDatabaseMigration = errors.New("Encountered error applying database migration.")
|
var errorDatabaseMigration = errors.New("Encountered error applying database migration.")
|
||||||
var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.")
|
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.")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
exitIfError(loggerCreate())
|
exitIfError(loggerCreate())
|
||||||
|
exitIfError(versionPrint())
|
||||||
exitIfError(configParse())
|
exitIfError(configParse())
|
||||||
exitIfError(dbConnect(5))
|
exitIfError(dbConnect(5))
|
||||||
exitIfError(migrate())
|
exitIfError(migrate())
|
||||||
@@ -15,6 +16,7 @@ func main() {
|
|||||||
exitIfError(versionCheckStart())
|
exitIfError(versionCheckStart())
|
||||||
exitIfError(domainExportCleanupBegin())
|
exitIfError(domainExportCleanupBegin())
|
||||||
exitIfError(viewsCleanupBegin())
|
exitIfError(viewsCleanupBegin())
|
||||||
|
exitIfError(ssoTokenCleanupBegin())
|
||||||
|
|
||||||
exitIfError(routesServe())
|
exitIfError(routesServe())
|
||||||
}
|
}
|
||||||
|
|||||||
15
api/oauth.go
15
api/oauth.go
@@ -2,18 +2,27 @@ package main
|
|||||||
|
|
||||||
import ()
|
import ()
|
||||||
|
|
||||||
var configuredOauths []string
|
var googleConfigured bool
|
||||||
|
var twitterConfigured bool
|
||||||
|
var githubConfigured bool
|
||||||
|
var gitlabConfigured bool
|
||||||
|
|
||||||
func oauthConfigure() error {
|
func oauthConfigure() error {
|
||||||
configuredOauths = []string{}
|
|
||||||
|
|
||||||
if err := googleOauthConfigure(); err != nil {
|
if err := googleOauthConfigure(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := twitterOauthConfigure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := githubOauthConfigure(); err != nil {
|
if err := githubOauthConfigure(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := gitlabOauthConfigure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func githubOauthConfigure() error {
|
|||||||
Endpoint: github.Endpoint,
|
Endpoint: github.Endpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
configuredOauths = append(configuredOauths, "github")
|
githubConfigured = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
contents, err := ioutil.ReadAll(resp.Body)
|
contents, err := ioutil.ReadAll(resp.Body)
|
||||||
@@ -80,6 +84,23 @@ func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
email = user["email"].(string)
|
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)
|
c, err := commenterGetByEmail("github", email)
|
||||||
if err != nil && err != errorNoSuchCommenter {
|
if err != nil && err != errorNoSuchCommenter {
|
||||||
fmt.Fprintf(w, "Error: %s", err.Error())
|
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?
|
// TODO: in case of returning users, update the information we have on record?
|
||||||
if err == errorNoSuchCommenter {
|
if err == errorNoSuchCommenter {
|
||||||
var link string
|
commenterHex, err = commenterNew(email, name, link, photo, "github", "")
|
||||||
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", "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(w, "Error: %s", err.Error())
|
fmt.Fprintf(w, "Error: %s", err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
42
api/oauth_gitlab.go
Normal file
42
api/oauth_gitlab.go
Normal 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
|
||||||
|
}
|
||||||
96
api/oauth_gitlab_callback.go
Normal file
96
api/oauth_gitlab_callback.go
Normal 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>")
|
||||||
|
}
|
||||||
25
api/oauth_gitlab_redirect.go
Normal file
25
api/oauth_gitlab_redirect.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ func googleOauthConfigure() error {
|
|||||||
Endpoint: google.Endpoint,
|
Endpoint: google.Endpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
configuredOauths = append(configuredOauths, "google")
|
googleConfigured = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
61
api/oauth_sso.go
Normal file
61
api/oauth_sso.go
Normal 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
116
api/oauth_sso_callback.go
Normal 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
88
api/oauth_sso_redirect.go
Normal 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
51
api/oauth_twitter.go
Normal 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
|
||||||
|
}
|
||||||
108
api/oauth_twitter_callback.go
Normal file
108
api/oauth_twitter_callback.go
Normal 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>")
|
||||||
|
}
|
||||||
39
api/oauth_twitter_redirect.go
Normal file
39
api/oauth_twitter_redirect.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -18,8 +18,9 @@ func ownerResetPassword(resetHex string, password string) error {
|
|||||||
|
|
||||||
statement := `
|
statement := `
|
||||||
UPDATE owners SET passwordHash=$1
|
UPDATE owners SET passwordHash=$1
|
||||||
WHERE email IN (
|
WHERE ownerHex = (
|
||||||
SELECT email FROM ownerResetHexes
|
SELECT ownerHex
|
||||||
|
FROM ownerResetHexes
|
||||||
WHERE resetHex=$2
|
WHERE resetHex=$2
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ func apiRouterInit(router *mux.Router) error {
|
|||||||
|
|
||||||
router.HandleFunc("/api/domain/new", domainNewHandler).Methods("POST")
|
router.HandleFunc("/api/domain/new", domainNewHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/domain/delete", domainDeleteHandler).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/list", domainListHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/domain/update", domainUpdateHandler).Methods("POST")
|
router.HandleFunc("/api/domain/update", domainUpdateHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/domain/moderator/new", domainModeratorNewHandler).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/new", commenterNewHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST")
|
router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/commenter/self", commenterSelfHandler).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/get", emailGetHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/email/update", emailUpdateHandler).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/redirect", githubRedirectHandler).Methods("GET")
|
||||||
router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).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/new", commentNewHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST")
|
router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST")
|
||||||
router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST")
|
router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func fileDetemplate(f string) ([]byte, error) {
|
|||||||
x = strings.Replace(x, "[[[.Origin]]]", os.Getenv("ORIGIN"), -1)
|
x = strings.Replace(x, "[[[.Origin]]]", os.Getenv("ORIGIN"), -1)
|
||||||
x = strings.Replace(x, "[[[.CdnPrefix]]]", os.Getenv("CDN_PREFIX"), -1)
|
x = strings.Replace(x, "[[[.CdnPrefix]]]", os.Getenv("CDN_PREFIX"), -1)
|
||||||
x = strings.Replace(x, "[[[.Footer]]]", footer, -1)
|
x = strings.Replace(x, "[[[.Footer]]]", footer, -1)
|
||||||
|
x = strings.Replace(x, "[[[.Version]]]", version, -1)
|
||||||
|
|
||||||
return []byte(x), nil
|
return []byte(x), nil
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ func staticRouterInit(router *mux.Router) error {
|
|||||||
if path.Ext(p) != "" {
|
if path.Ext(p) != "" {
|
||||||
contentType[p] = mime.TypeByExtension(path.Ext(p))
|
contentType[p] = mime.TypeByExtension(path.Ext(p))
|
||||||
} else {
|
} else {
|
||||||
contentType[p] = mime.TypeByExtension("html")
|
contentType[p] = "text/html; charset=utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
|
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func sigintCleanup() int {
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ func smtpConfigure() error {
|
|||||||
password := os.Getenv("SMTP_PASSWORD")
|
password := os.Getenv("SMTP_PASSWORD")
|
||||||
host := os.Getenv("SMTP_HOST")
|
host := os.Getenv("SMTP_HOST")
|
||||||
port := os.Getenv("SMTP_PORT")
|
port := os.Getenv("SMTP_PORT")
|
||||||
if username == "" || password == "" || host == "" || port == "" {
|
if host == "" || port == "" {
|
||||||
logger.Warningf("smtp not configured, no emails will be sent")
|
logger.Warningf("smtp not configured, no emails will be sent")
|
||||||
smtpConfigured = false
|
smtpConfigured = false
|
||||||
return nil
|
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") == "" {
|
if os.Getenv("SMTP_FROM_ADDRESS") == "" {
|
||||||
logger.Errorf("COMMENTO_SMTP_FROM_ADDRESS not set")
|
logger.Errorf("COMMENTO_SMTP_FROM_ADDRESS not set")
|
||||||
smtpConfigured = false
|
smtpConfigured = false
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func htmlTitleRecurse(h *html.Node) string {
|
func htmlTitleRecurse(h *html.Node) string {
|
||||||
|
if h == nil || h.FirstChild == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if h.Type == html.ElementNode && h.Data == "title" {
|
if h.Type == html.ElementNode && h.Data == "title" {
|
||||||
return h.FirstChild.Data
|
return h.FirstChild.Data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func versionPrint() error {
|
||||||
|
logger.Infof("starting Commento %s", version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func versionCheckStart() error {
|
func versionCheckStart() error {
|
||||||
go func() {
|
go func() {
|
||||||
printedError := false
|
printedError := false
|
||||||
|
|||||||
2
db/20190219001130-v1.6.2.sql
Normal file
2
db/20190219001130-v1.6.2.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE config
|
||||||
|
SET version = 'v1.6.0';
|
||||||
16
db/20190418210855-configurable-auth.sql
Normal file
16
db/20190418210855-configurable-auth.sql
Normal 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
10
db/20190420181913-sso.sql
Normal 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 '';
|
||||||
6
db/20190420231030-sso-tokens.sql
Normal file
6
db/20190420231030-sso-tokens.sql
Normal 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
|
||||||
|
);
|
||||||
2
db/20190501201032-v1.7.0.sql
Normal file
2
db/20190501201032-v1.7.0.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE config
|
||||||
|
SET version = 'v1.7.0';
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
<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>
|
<title>Commento: Email Confirmation</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css">
|
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/dashboard.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>
|
<title>Commento: Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -160,39 +159,48 @@
|
|||||||
<ul class="tabs">
|
<ul class="tabs">
|
||||||
<li class="tab-link original current" data-tab="mod-tab-1">General</li>
|
<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-2">Add/Remove Moderators</li>
|
||||||
<li class="tab-link" data-tab="mod-tab-3">Email Settings</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="mod-tab-1" class="content original current">
|
<div id="mod-tab-1" class="content original current">
|
||||||
|
<div class="question">
|
||||||
|
<div class="title">
|
||||||
|
Comment Filtering
|
||||||
|
</div>
|
||||||
|
<div class="answer">
|
||||||
<div class="row no-border commento-round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
<input type="checkbox" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
||||||
<label for="spam-filtering">Automatic spam filtering</label>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border commento-round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
|
<input type="checkbox" v-model="domains[cd].requireModeration" id="require-moderation">
|
||||||
<label for="require-moderation">Require all comments to be approved manually</label>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border commento-round-check">
|
<div class="row no-border commento-round-check">
|
||||||
<input type="checkbox" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
|
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
|
||||||
<label for="allow-anonymous">Allow anonymous comments</label>
|
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
|
||||||
<div class="pitch">
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div class="row no-border commento-round-check indent" v-if="domains[cd].allowAnonymous">
|
<div class="question">
|
||||||
<input type="checkbox" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
|
<div class="title">
|
||||||
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
|
Email Schedule
|
||||||
<div class="pitch">
|
</div>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -223,31 +231,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</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">
|
<input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name">
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="center">
|
||||||
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
|
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,39 +403,67 @@
|
|||||||
<div id="danger-view" class="view hidden">
|
<div id="danger-view" class="view hidden">
|
||||||
<div class="view-inside">
|
<div class="view-inside">
|
||||||
<div class="mid-view">
|
<div class="mid-view">
|
||||||
<div class="tabs-container">
|
<div class="center center-title">
|
||||||
<div class="tab">
|
Danger Zone
|
||||||
<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 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>
|
</div>
|
||||||
|
|
||||||
<button onclick="document.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button>
|
<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>
|
||||||
|
<div class="subtitle">
|
||||||
<div class="box" v-if="domains[cd].state != 'frozen'">
|
Freezing your domain will disable new comments and voting temporarily. You may unfreeze the domain later.
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button onclick="document.location.hash='#freeze-domain-modal'"
|
||||||
|
class="button orange-button">Freeze</button>
|
||||||
</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>
|
</div>
|
||||||
|
<div class="action-button" v-if="domains[cd].state == 'frozen'">
|
||||||
<button id="big-red-button" class="button big-red-button" onclick="document.location.hash='#delete-domain-modal'">Delete Domain</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,8 +480,8 @@
|
|||||||
<div class="modal-subtitle">
|
<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.
|
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>
|
||||||
<div class="modal-contents">
|
<div class="modal-contents center">
|
||||||
<button id="orange-button" class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
|
<button class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,8 +493,21 @@
|
|||||||
<div class="modal-subtitle">
|
<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.
|
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>
|
||||||
<div class="modal-contents">
|
<div class="modal-contents center">
|
||||||
<button id="blue-button" class="button green-button" onclick="window.commento.domainUnfreezeHandler()">Unfreeze Domain</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,8 +519,8 @@
|
|||||||
<div class="modal-subtitle">
|
<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.
|
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>
|
||||||
<div class="modal-contents">
|
<div class="modal-contents center">
|
||||||
<button id="big-red-button" class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
|
<button class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
frontend/fonts/source-sans-300-cyrillic-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-300-cyrillic-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-cyrillic.woff2
Normal file
BIN
frontend/fonts/source-sans-300-cyrillic.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-greek-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-300-greek-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-greek.woff2
Normal file
BIN
frontend/fonts/source-sans-300-greek.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-latin-ext.woff2
Normal file
BIN
frontend/fonts/source-sans-300-latin-ext.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-latin.woff2
Normal file
BIN
frontend/fonts/source-sans-300-latin.woff2
Normal file
Binary file not shown.
BIN
frontend/fonts/source-sans-300-vietnamese.woff2
Normal file
BIN
frontend/fonts/source-sans-300-vietnamese.woff2
Normal file
Binary file not shown.
@@ -19,4 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="copyright">
|
||||||
|
Commento [[[.Version]]]
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/forgot.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/forgot.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
<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>
|
<title>Commento: Reset your Password</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -17,16 +16,22 @@
|
|||||||
|
|
||||||
<div class="auth-form-container">
|
<div class="auth-form-container">
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
|
<form onsubmit="window.commento.sendResetHex(event)">
|
||||||
<div class="form-title">
|
<div class="form-title">
|
||||||
Reset your Password
|
Reset your Password
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">Email Address</div>
|
<div class="label">Email Address</div>
|
||||||
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
|
<input class="input" type="text" name="email" id="email" placeholder="example@example.com">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="err" id="err"></div>
|
<div class="err" id="err"></div>
|
||||||
<div class="msg" id="msg"></div>
|
<div class="msg" id="msg"></div>
|
||||||
<button id="reset-button" class="button" onclick="window.commento.sendResetHex()">Send Reset Password Link</button>
|
|
||||||
|
<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>
|
<a class="link" href="[[[.Origin]]]/login">Suddenly remembered your password? Login.</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const jsCompileMap = {
|
|||||||
"js/logout.js"
|
"js/logout.js"
|
||||||
],
|
],
|
||||||
"js/commento.js": ["js/commento.js"],
|
"js/commento.js": ["js/commento.js"],
|
||||||
|
"js/count.js": ["js/count.js"],
|
||||||
"js/unsubscribe.js": [
|
"js/unsubscribe.js": [
|
||||||
"js/constants.js",
|
"js/constants.js",
|
||||||
"js/utils.js",
|
"js/utils.js",
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
(function(global, document) {
|
(function(global, document) {
|
||||||
"use strict";
|
"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
|
// Do not use other files like utils.js and http.js in the gulpfile to build
|
||||||
// commento.js for the following reasons:
|
// commento.js for the following reasons:
|
||||||
// - We don't use jQuery in the actual JavaScript payload because we need
|
// - 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_CONTENTS = "commento-comment-contents-";
|
||||||
var ID_NAME = "commento-comment-name-";
|
var ID_NAME = "commento-comment-name-";
|
||||||
var ID_SUBMIT_BUTTON = "commento-submit-button-";
|
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";
|
var ID_FOOTER = "commento-footer";
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +64,7 @@
|
|||||||
var cdn = "[[[.CdnPrefix]]]";
|
var cdn = "[[[.CdnPrefix]]]";
|
||||||
var root = null;
|
var root = null;
|
||||||
var cssOverride;
|
var cssOverride;
|
||||||
|
var noFonts;
|
||||||
var autoInit;
|
var autoInit;
|
||||||
var isAuthenticated = false;
|
var isAuthenticated = false;
|
||||||
var comments = [];
|
var comments = [];
|
||||||
@@ -81,10 +76,11 @@
|
|||||||
var isLocked = false;
|
var isLocked = false;
|
||||||
var stickyCommentHex = "none";
|
var stickyCommentHex = "none";
|
||||||
var shownReply = {};
|
var shownReply = {};
|
||||||
var configuredOauths = [];
|
var configuredOauths = {};
|
||||||
var popupBoxType = "login";
|
var popupBoxType = "login";
|
||||||
var oauthButtonsShown = false;
|
var oauthButtonsShown = false;
|
||||||
var selfHex = undefined;
|
var selfHex = undefined;
|
||||||
|
var mobileView = null;
|
||||||
|
|
||||||
|
|
||||||
function $(id) {
|
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) {
|
function onclick(node, f, arg) {
|
||||||
node.addEventListener("click", function() {
|
node.addEventListener("click", function() {
|
||||||
f(arg);
|
f(arg);
|
||||||
@@ -231,7 +240,12 @@
|
|||||||
|
|
||||||
var loggedContainer = create("div");
|
var loggedContainer = create("div");
|
||||||
var loggedInAs = 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 avatar;
|
||||||
var logout = create("div");
|
var logout = create("div");
|
||||||
var color = colorGet(commenter.commenterHex + "-" + commenter.name);
|
var color = colorGet(commenter.commenterHex + "-" + commenter.name);
|
||||||
@@ -249,7 +263,9 @@
|
|||||||
onclick(logout, global.logout);
|
onclick(logout, global.logout);
|
||||||
|
|
||||||
attrSet(loggedContainer, "style", "display: none");
|
attrSet(loggedContainer, "style", "display: none");
|
||||||
|
if (commenter.link !== "undefined") {
|
||||||
attrSet(name, "href", commenter.link);
|
attrSet(name, "href", commenter.link);
|
||||||
|
}
|
||||||
if (commenter.photo === "undefined") {
|
if (commenter.photo === "undefined") {
|
||||||
avatar = create("div");
|
avatar = create("div");
|
||||||
avatar.style["background"] = color;
|
avatar.style["background"] = color;
|
||||||
@@ -257,13 +273,7 @@
|
|||||||
classAdd(avatar, "avatar");
|
classAdd(avatar, "avatar");
|
||||||
} else {
|
} else {
|
||||||
avatar = create("img");
|
avatar = create("img");
|
||||||
if (commenter.provider === "google") {
|
attrSet(avatar, "src", cdn + "/api/commenter/photo?commenterHex=" + commenter.commenterHex);
|
||||||
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);
|
|
||||||
}
|
|
||||||
classAdd(avatar, "avatar-img");
|
classAdd(avatar, "avatar-img");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +307,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
selfLoad(resp.commenter);
|
selfLoad(resp.commenter);
|
||||||
|
global.allShow();
|
||||||
|
|
||||||
call(callback);
|
call(callback);
|
||||||
});
|
});
|
||||||
@@ -320,7 +331,6 @@
|
|||||||
var footer = create("div");
|
var footer = create("div");
|
||||||
var aContainer = create("div");
|
var aContainer = create("div");
|
||||||
var a = create("a");
|
var a = create("a");
|
||||||
var img = create("img");
|
|
||||||
var text = create("span");
|
var text = create("span");
|
||||||
|
|
||||||
footer.id = ID_FOOTER;
|
footer.id = ID_FOOTER;
|
||||||
@@ -328,21 +338,18 @@
|
|||||||
classAdd(footer, "footer");
|
classAdd(footer, "footer");
|
||||||
classAdd(aContainer, "logo-container");
|
classAdd(aContainer, "logo-container");
|
||||||
classAdd(a, "logo");
|
classAdd(a, "logo");
|
||||||
classAdd(img, "logo-svg");
|
|
||||||
classAdd(text, "logo-text");
|
classAdd(text, "logo-text");
|
||||||
|
|
||||||
attrSet(footer, "style", "display: none");
|
|
||||||
attrSet(a, "href", "https://commento.io");
|
attrSet(a, "href", "https://commento.io");
|
||||||
attrSet(a, "target", "_blank");
|
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(a, text);
|
||||||
append(aContainer, a);
|
append(aContainer, a);
|
||||||
append(footer, aContainer);
|
append(footer, aContainer);
|
||||||
append(root, footer);
|
|
||||||
|
return footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -357,6 +364,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
requireIdentification = resp.requireIdentification;
|
requireIdentification = resp.requireIdentification;
|
||||||
@@ -370,8 +379,6 @@
|
|||||||
commenters = Object.assign({}, commenters, resp.commenters)
|
commenters = Object.assign({}, commenters, resp.commenters)
|
||||||
configuredOauths = resp.configuredOauths;
|
configuredOauths = resp.configuredOauths;
|
||||||
|
|
||||||
cssLoad(cdn + "/css/commento.css", "window.commento.loadCssOverride()");
|
|
||||||
|
|
||||||
call(callback);
|
call(callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -386,6 +393,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function errorHide() {
|
||||||
|
var el = $(ID_ERROR);
|
||||||
|
|
||||||
|
attrSet(el, "style", "display: none;");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function errorElementCreate() {
|
function errorElementCreate() {
|
||||||
var el = create("div");
|
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>></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) {
|
function textareaCreate(id) {
|
||||||
var textareaSuperContainer = create("div");
|
var textareaSuperContainer = create("div");
|
||||||
var textareaContainer = create("div");
|
var textareaContainer = create("div");
|
||||||
@@ -414,18 +504,21 @@
|
|||||||
var anonymousCheckbox = create("input");
|
var anonymousCheckbox = create("input");
|
||||||
var anonymousCheckboxLabel = create("label");
|
var anonymousCheckboxLabel = create("label");
|
||||||
var submitButton = create("button");
|
var submitButton = create("button");
|
||||||
|
var markdownButton = create("a");
|
||||||
|
|
||||||
textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
|
textareaSuperContainer.id = ID_SUPER_CONTAINER + id;
|
||||||
textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
|
textareaContainer.id = ID_TEXTAREA_CONTAINER + id;
|
||||||
textarea.id = ID_TEXTAREA + id;
|
textarea.id = ID_TEXTAREA + id;
|
||||||
anonymousCheckbox.id = ID_ANONYMOUS_CHECKBOX + id;
|
anonymousCheckbox.id = ID_ANONYMOUS_CHECKBOX + id;
|
||||||
submitButton.id = ID_SUBMIT_BUTTON + id;
|
submitButton.id = ID_SUBMIT_BUTTON + id;
|
||||||
|
markdownButton.id = ID_MARKDOWN_BUTTON + id;
|
||||||
|
|
||||||
classAdd(textareaContainer, "textarea-container");
|
classAdd(textareaContainer, "textarea-container");
|
||||||
classAdd(anonymousCheckboxContainer, "round-check");
|
classAdd(anonymousCheckboxContainer, "round-check");
|
||||||
classAdd(anonymousCheckboxContainer, "anonymous-checkbox-container");
|
classAdd(anonymousCheckboxContainer, "anonymous-checkbox-container");
|
||||||
classAdd(submitButton, "button");
|
classAdd(submitButton, "button");
|
||||||
classAdd(submitButton, "submit-button");
|
classAdd(submitButton, "submit-button");
|
||||||
|
classAdd(markdownButton, "markdown-button");
|
||||||
classAdd(textareaSuperContainer, "button-margin");
|
classAdd(textareaSuperContainer, "button-margin");
|
||||||
|
|
||||||
attrSet(textarea, "placeholder", "Add a comment");
|
attrSet(textarea, "placeholder", "Add a comment");
|
||||||
@@ -434,9 +527,11 @@
|
|||||||
|
|
||||||
anonymousCheckboxLabel.innerText = "Comment anonymously";
|
anonymousCheckboxLabel.innerText = "Comment anonymously";
|
||||||
submitButton.innerText = "Add Comment";
|
submitButton.innerText = "Add Comment";
|
||||||
|
markdownButton.innerHTML = "<b>M ↓</b> Markdown";
|
||||||
|
|
||||||
textarea.oninput = autoExpander(textarea);
|
textarea.oninput = autoExpander(textarea);
|
||||||
onclick(submitButton, submitAccountDecide, id);
|
onclick(submitButton, submitAccountDecide, id);
|
||||||
|
onclick(markdownButton, markdownHelpShow, id);
|
||||||
|
|
||||||
append(textareaContainer, textarea);
|
append(textareaContainer, textarea);
|
||||||
append(textareaSuperContainer, textareaContainer);
|
append(textareaSuperContainer, textareaContainer);
|
||||||
@@ -446,6 +541,7 @@
|
|||||||
if (!requireIdentification) {
|
if (!requireIdentification) {
|
||||||
append(textareaSuperContainer, anonymousCheckboxContainer);
|
append(textareaSuperContainer, anonymousCheckboxContainer);
|
||||||
}
|
}
|
||||||
|
append(textareaSuperContainer, markdownButton);
|
||||||
|
|
||||||
return textareaSuperContainer;
|
return textareaSuperContainer;
|
||||||
}
|
}
|
||||||
@@ -521,7 +617,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var json = {
|
var json = {
|
||||||
"commenterToken": commenterTokenGet(),
|
"commenterToken": commenterToken,
|
||||||
"domain": parent.location.host,
|
"domain": parent.location.host,
|
||||||
"path": parent.location.pathname,
|
"path": parent.location.pathname,
|
||||||
"parentHex": id,
|
"parentHex": id,
|
||||||
@@ -532,6 +628,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = "";
|
var message = "";
|
||||||
@@ -560,7 +658,7 @@
|
|||||||
"score": 0,
|
"score": 0,
|
||||||
"state": "approved",
|
"state": "approved",
|
||||||
"direction": 0,
|
"direction": 0,
|
||||||
"creationDate": (new Date()).toISOString(),
|
"creationDate": new Date(),
|
||||||
}],
|
}],
|
||||||
}, "root")
|
}, "root")
|
||||||
|
|
||||||
@@ -591,9 +689,9 @@
|
|||||||
"#da7c30",
|
"#da7c30",
|
||||||
"#3e9651",
|
"#3e9651",
|
||||||
"#cc2529",
|
"#cc2529",
|
||||||
"#535154",
|
|
||||||
"#6b4c9a",
|
|
||||||
"#922428",
|
"#922428",
|
||||||
|
"#6b4c9a",
|
||||||
|
"#535154",
|
||||||
];
|
];
|
||||||
|
|
||||||
var total = 0;
|
var total = 0;
|
||||||
@@ -652,16 +750,24 @@
|
|||||||
cur.sort(function(a, b) {
|
cur.sort(function(a, b) {
|
||||||
if (a.commentHex === stickyCommentHex) {
|
if (a.commentHex === stickyCommentHex) {
|
||||||
return -Infinity;
|
return -Infinity;
|
||||||
}
|
} else if (b.commentHex === stickyCommentHex) {
|
||||||
if (b.commentHex === stickyCommentHex) {
|
|
||||||
return Infinity;
|
return Infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a.score !== b.score) {
|
||||||
return b.score - a.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");
|
var cards = create("div");
|
||||||
cur.forEach(function(comment) {
|
cur.forEach(function(comment) {
|
||||||
var mobileView = root.getBoundingClientRect()["width"] < 450;
|
|
||||||
var commenter = commenters[comment.commenterHex];
|
var commenter = commenters[comment.commenterHex];
|
||||||
var avatar;
|
var avatar;
|
||||||
var card = create("div");
|
var card = create("div");
|
||||||
@@ -727,27 +833,29 @@
|
|||||||
} else {
|
} else {
|
||||||
sticky.title = "Sticky";
|
sticky.title = "Sticky";
|
||||||
}
|
}
|
||||||
|
timeago.title = comment.creationDate.toString();
|
||||||
|
|
||||||
card.style["borderLeft"] = "2px solid " + color;
|
card.style["borderLeft"] = "2px solid " + color;
|
||||||
name.innerText = commenter.name;
|
name.innerText = commenter.name;
|
||||||
text.innerHTML = comment.html;
|
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);
|
score.innerText = scorify(comment.score);
|
||||||
|
|
||||||
if (commenter.photo === "undefined") {
|
if (commenter.photo === "undefined") {
|
||||||
avatar = create("div");
|
avatar = create("div");
|
||||||
avatar.style["background"] = color;
|
avatar.style["background"] = color;
|
||||||
|
|
||||||
|
if (comment.commenterHex === "anonymous") {
|
||||||
|
avatar.innerHTML = "?";
|
||||||
|
avatar.style["font-weight"] = "bold";
|
||||||
|
} else {
|
||||||
avatar.innerHTML = commenter.name[0].toUpperCase();
|
avatar.innerHTML = commenter.name[0].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
classAdd(avatar, "avatar");
|
classAdd(avatar, "avatar");
|
||||||
} else {
|
} else {
|
||||||
avatar = create("img");
|
avatar = create("img");
|
||||||
if (commenter.provider === "google") {
|
attrSet(avatar, "src", cdn + "/api/commenter/photo?commenterHex=" + commenter.commenterHex);
|
||||||
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);
|
|
||||||
}
|
|
||||||
classAdd(avatar, "avatar-img");
|
classAdd(avatar, "avatar-img");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,6 +863,9 @@
|
|||||||
if (isModerator && comment.state !== "approved") {
|
if (isModerator && comment.state !== "approved") {
|
||||||
classAdd(card, "dark-card");
|
classAdd(card, "dark-card");
|
||||||
}
|
}
|
||||||
|
if (commenter.isModerator) {
|
||||||
|
classAdd(name, "moderator");
|
||||||
|
}
|
||||||
if (comment.state === "flagged") {
|
if (comment.state === "flagged") {
|
||||||
classAdd(name, "flagged");
|
classAdd(name, "flagged");
|
||||||
}
|
}
|
||||||
@@ -803,7 +914,9 @@
|
|||||||
onclick(sticky, global.commentSticky, comment.commentHex);
|
onclick(sticky, global.commentSticky, comment.commentHex);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
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 {
|
} else {
|
||||||
onclick(upvote, global.loginBoxShow, null);
|
onclick(upvote, global.loginBoxShow, null);
|
||||||
onclick(downvote, global.loginBoxShow, null);
|
onclick(downvote, global.loginBoxShow, null);
|
||||||
@@ -884,6 +997,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
var card = $(ID_CARD + commentHex);
|
var card = $(ID_CARD + commentHex);
|
||||||
@@ -907,6 +1022,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
var card = $(ID_CARD + commentHex);
|
var card = $(ID_CARD + commentHex);
|
||||||
@@ -925,6 +1042,9 @@
|
|||||||
|
|
||||||
|
|
||||||
function upDownOnclickSet(upvote, downvote, commentHex, direction) {
|
function upDownOnclickSet(upvote, downvote, commentHex, direction) {
|
||||||
|
upvote = removeAllEventListeners(upvote);
|
||||||
|
downvote = removeAllEventListeners(downvote);
|
||||||
|
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
onclick(upvote, global.vote, [commentHex, [1, 0]]);
|
onclick(upvote, global.vote, [commentHex, [1, 0]]);
|
||||||
onclick(downvote, global.vote, [commentHex, [1, -1]]);
|
onclick(downvote, global.vote, [commentHex, [1, -1]]);
|
||||||
@@ -935,6 +1055,8 @@
|
|||||||
onclick(upvote, global.vote, [commentHex, [0, 1]]);
|
onclick(upvote, global.vote, [commentHex, [0, 1]]);
|
||||||
onclick(downvote, global.vote, [commentHex, [0, -1]]);
|
onclick(downvote, global.vote, [commentHex, [0, -1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [upvote, downvote];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -953,7 +1075,9 @@
|
|||||||
"direction": newDirection,
|
"direction": newDirection,
|
||||||
};
|
};
|
||||||
|
|
||||||
upDownOnclickSet(upvote, downvote, commentHex, newDirection);
|
var upDown = upDownOnclickSet(upvote, downvote, commentHex, newDirection);
|
||||||
|
upvote = upDown[0];
|
||||||
|
downvote = upDown[1];
|
||||||
|
|
||||||
classRemove(upvote, "upvoted");
|
classRemove(upvote, "upvoted");
|
||||||
classRemove(downvote, "downvoted");
|
classRemove(downvote, "downvoted");
|
||||||
@@ -968,7 +1092,13 @@
|
|||||||
post(origin + "/api/comment/vote", json, function(resp) {
|
post(origin + "/api/comment/vote", json, function(resp) {
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
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;
|
return;
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -990,6 +1120,7 @@
|
|||||||
|
|
||||||
replyButton.title = "Cancel reply";
|
replyButton.title = "Cancel reply";
|
||||||
|
|
||||||
|
replyButton = removeAllEventListeners(replyButton);
|
||||||
onclick(replyButton, global.replyCollapse, id);
|
onclick(replyButton, global.replyCollapse, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1006,6 +1137,7 @@
|
|||||||
|
|
||||||
replyButton.title = "Reply to this comment";
|
replyButton.title = "Reply to this comment";
|
||||||
|
|
||||||
|
replyButton = removeAllEventListeners(replyButton);
|
||||||
onclick(replyButton, global.replyShow, id)
|
onclick(replyButton, global.replyShow, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1023,6 +1155,7 @@
|
|||||||
|
|
||||||
button.title = "Expand children";
|
button.title = "Expand children";
|
||||||
|
|
||||||
|
button = removeAllEventListeners(button);
|
||||||
onclick(button, global.commentUncollapse, id);
|
onclick(button, global.commentUncollapse, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,6 +1173,7 @@
|
|||||||
|
|
||||||
button.title = "Collapse children";
|
button.title = "Collapse children";
|
||||||
|
|
||||||
|
button = removeAllEventListeners(button);
|
||||||
onclick(button, global.commentCollapse, id);
|
onclick(button, global.commentCollapse, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,6 +1189,9 @@
|
|||||||
if (!(parentHex in parentMap)) {
|
if (!(parentHex in parentMap)) {
|
||||||
parentMap[parentHex] = [];
|
parentMap[parentHex] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comment.creationDate = new Date(comment.creationDate);
|
||||||
|
|
||||||
parentMap[parentHex].push(comment);
|
parentMap[parentHex].push(comment);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1118,6 +1255,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieSet("commentoCommenterToken", resp.commenterToken);
|
cookieSet("commentoCommenterToken", resp.commenterToken);
|
||||||
@@ -1167,10 +1306,14 @@
|
|||||||
global.popupRender = function(id) {
|
global.popupRender = function(id) {
|
||||||
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
var loginBoxContainer = $(ID_LOGIN_BOX_CONTAINER);
|
||||||
var loginBox = create("div");
|
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 oauthSubtitle = create("div");
|
||||||
var oauthButtonsContainer = create("div");
|
var oauthButtonsContainer = create("div");
|
||||||
var oauthButtons = create("div");
|
var oauthButtons = create("div");
|
||||||
var hr = create("hr");
|
var hr2 = create("hr");
|
||||||
var emailSubtitle = create("div");
|
var emailSubtitle = create("div");
|
||||||
var emailContainer = create("div");
|
var emailContainer = create("div");
|
||||||
var email = create("div");
|
var email = create("div");
|
||||||
@@ -1186,7 +1329,7 @@
|
|||||||
emailButton.id = ID_LOGIN_BOX_EMAIL_BUTTON;
|
emailButton.id = ID_LOGIN_BOX_EMAIL_BUTTON;
|
||||||
loginLink.id = ID_LOGIN_BOX_LOGIN_LINK;
|
loginLink.id = ID_LOGIN_BOX_LOGIN_LINK;
|
||||||
loginLinkContainer.id = ID_LOGIN_BOX_LOGIN_LINK_CONTAINER;
|
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;
|
oauthSubtitle.id = ID_LOGIN_BOX_OAUTH_PRETEXT;
|
||||||
oauthButtonsContainer.id = ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER;
|
oauthButtonsContainer.id = ID_LOGIN_BOX_OAUTH_BUTTONS_CONTAINER;
|
||||||
|
|
||||||
@@ -1199,6 +1342,9 @@
|
|||||||
classAdd(emailButton, "email-button");
|
classAdd(emailButton, "email-button");
|
||||||
classAdd(loginLinkContainer, "login-link-container");
|
classAdd(loginLinkContainer, "login-link-container");
|
||||||
classAdd(loginLink, "login-link");
|
classAdd(loginLink, "login-link");
|
||||||
|
classAdd(ssoSubtitle, "login-box-subtitle");
|
||||||
|
classAdd(ssoButtonContainer, "oauth-buttons-container");
|
||||||
|
classAdd(ssoButton, "oauth-buttons");
|
||||||
classAdd(oauthSubtitle, "login-box-subtitle");
|
classAdd(oauthSubtitle, "login-box-subtitle");
|
||||||
classAdd(oauthButtonsContainer, "oauth-buttons-container");
|
classAdd(oauthButtonsContainer, "oauth-buttons-container");
|
||||||
classAdd(oauthButtons, "oauth-buttons");
|
classAdd(oauthButtons, "oauth-buttons");
|
||||||
@@ -1209,6 +1355,7 @@
|
|||||||
emailSubtitle.innerText = "Login with your email address";
|
emailSubtitle.innerText = "Login with your email address";
|
||||||
emailButton.innerText = "Continue";
|
emailButton.innerText = "Continue";
|
||||||
oauthSubtitle.innerText = "Proceed with social login";
|
oauthSubtitle.innerText = "Proceed with social login";
|
||||||
|
ssoSubtitle.innerText = "Proceed with " + parent.location.host + " authentication";
|
||||||
|
|
||||||
onclick(emailButton, global.passwordAsk, id);
|
onclick(emailButton, global.passwordAsk, id);
|
||||||
onclick(loginLink, global.popupSwitch);
|
onclick(loginLink, global.popupSwitch);
|
||||||
@@ -1219,37 +1366,68 @@
|
|||||||
attrSet(emailInput, "placeholder", "Email address");
|
attrSet(emailInput, "placeholder", "Email address");
|
||||||
attrSet(emailInput, "type", "text");
|
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");
|
var button = create("button");
|
||||||
|
|
||||||
classAdd(button, "button");
|
classAdd(button, "button");
|
||||||
classAdd(button, configuredOauths[i] + "-button");
|
classAdd(button, provider + "-button");
|
||||||
|
|
||||||
button.innerText = configuredOauths[i];
|
button.innerText = provider;
|
||||||
|
|
||||||
onclick(button, global.commentoAuth, {"provider": configuredOauths[i], "id": id});
|
onclick(button, global.commentoAuth, {"provider": provider, "id": id});
|
||||||
|
|
||||||
append(oauthButtons, button);
|
append(oauthButtons, button);
|
||||||
|
numOauthConfigured++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (configuredOauths["sso"]) {
|
||||||
|
var button = create("button");
|
||||||
|
|
||||||
|
classAdd(button, "button");
|
||||||
|
classAdd(button, "sso-button");
|
||||||
|
|
||||||
|
button.innerText = "Login with Single Sign-On";
|
||||||
|
|
||||||
|
onclick(button, global.commentoAuth, {"provider": "sso", "id": id});
|
||||||
|
|
||||||
|
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(loginBox, oauthSubtitle);
|
||||||
append(oauthButtonsContainer, oauthButtons);
|
append(oauthButtonsContainer, oauthButtons);
|
||||||
append(loginBox, oauthButtonsContainer);
|
append(loginBox, oauthButtonsContainer);
|
||||||
append(loginBox, hr);
|
|
||||||
oauthButtonsShown = true;
|
oauthButtonsShown = true;
|
||||||
} else {
|
} else {
|
||||||
oauthButtonsShown = false;
|
oauthButtonsShown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
append(loginBox, emailSubtitle);
|
|
||||||
append(email, emailInput);
|
append(email, emailInput);
|
||||||
append(email, emailButton);
|
append(email, emailButton);
|
||||||
append(emailContainer, email);
|
append(emailContainer, email);
|
||||||
append(loginBox, emailContainer);
|
|
||||||
|
|
||||||
append(loginLinkContainer, loginLink);
|
append(loginLinkContainer, loginLink);
|
||||||
|
|
||||||
|
if (numOauthConfigured > 0 && configuredOauths["commento"]) {
|
||||||
|
append(loginBox, hr2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuredOauths["commento"]) {
|
||||||
|
append(loginBox, emailSubtitle);
|
||||||
|
append(loginBox, emailContainer);
|
||||||
append(loginBox, loginLinkContainer);
|
append(loginBox, loginLinkContainer);
|
||||||
|
}
|
||||||
|
|
||||||
append(loginBox, close);
|
append(loginBox, close);
|
||||||
|
|
||||||
@@ -1286,15 +1464,14 @@
|
|||||||
global.loginBoxClose();
|
global.loginBoxClose();
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieSet("commentoCommenterToken", resp.commenterToken);
|
cookieSet("commentoCommenterToken", resp.commenterToken);
|
||||||
|
|
||||||
selfLoad(resp.commenter);
|
selfLoad(resp.commenter);
|
||||||
var loggedContainer = $(ID_LOGGED_CONTAINER);
|
global.allShow();
|
||||||
if (loggedContainer) {
|
|
||||||
attrSet(loggedContainer, "style", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
remove($(ID_LOGIN));
|
remove($(ID_LOGIN));
|
||||||
if (id !== null) {
|
if (id !== null) {
|
||||||
@@ -1334,6 +1511,8 @@
|
|||||||
global.loginBoxClose();
|
global.loginBoxClose();
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
loginUP(email.value, password.value, id);
|
loginUP(email.value, password.value, id);
|
||||||
@@ -1440,6 +1619,8 @@
|
|||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
errorShow(resp.message);
|
errorShow(resp.message);
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
errorHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
call(callback);
|
call(callback);
|
||||||
@@ -1527,7 +1708,7 @@
|
|||||||
if (cssOverride === undefined) {
|
if (cssOverride === undefined) {
|
||||||
global.allShow();
|
global.allShow();
|
||||||
} else {
|
} else {
|
||||||
cssLoad(cssOverride, "window.allShow()");
|
cssLoad(cssOverride, "window.commento.allShow()");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1536,7 +1717,6 @@
|
|||||||
var mainArea = $(ID_MAIN_AREA);
|
var mainArea = $(ID_MAIN_AREA);
|
||||||
var modTools = $(ID_MOD_TOOLS);
|
var modTools = $(ID_MOD_TOOLS);
|
||||||
var loggedContainer = $(ID_LOGGED_CONTAINER);
|
var loggedContainer = $(ID_LOGGED_CONTAINER);
|
||||||
var footer = $(ID_FOOTER);
|
|
||||||
|
|
||||||
attrSet(mainArea, "style", "");
|
attrSet(mainArea, "style", "");
|
||||||
|
|
||||||
@@ -1547,11 +1727,6 @@
|
|||||||
if (loggedContainer) {
|
if (loggedContainer) {
|
||||||
attrSet(loggedContainer, "style", "");
|
attrSet(loggedContainer, "style", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
attrSet(footer, "style", "");
|
|
||||||
|
|
||||||
nameWidthFix();
|
|
||||||
loadHash();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1594,13 +1769,16 @@
|
|||||||
if (ID_ROOT === undefined) {
|
if (ID_ROOT === undefined) {
|
||||||
ID_ROOT = "commento";
|
ID_ROOT = "commento";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noFonts = attrGet(scripts[i], "data-no-fonts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadHash() {
|
function loadHash() {
|
||||||
if (window.location.hash && window.location.hash.startsWith("#commento-")) {
|
if (window.location.hash) {
|
||||||
|
if (window.location.hash.startsWith("#commento-")) {
|
||||||
var el = $(ID_CARD + window.location.hash.split("-")[1]);
|
var el = $(ID_CARD + window.location.hash.split("-")[1]);
|
||||||
if (el === null) {
|
if (el === null) {
|
||||||
return;
|
return;
|
||||||
@@ -1608,6 +1786,9 @@
|
|||||||
|
|
||||||
classAdd(el, "highlighted-card");
|
classAdd(el, "highlighted-card");
|
||||||
el.scrollIntoView(true);
|
el.scrollIntoView(true);
|
||||||
|
} else if (window.location.hash.startsWith("#commento")) {
|
||||||
|
root.scrollIntoView(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,6 +1800,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mobileView === null) {
|
||||||
|
mobileView = root.getBoundingClientRect()["width"] < 450;
|
||||||
|
}
|
||||||
|
|
||||||
classAdd(root, "root");
|
classAdd(root, "root");
|
||||||
|
|
||||||
loginBoxCreate();
|
loginBoxCreate();
|
||||||
@@ -1627,13 +1812,22 @@
|
|||||||
|
|
||||||
mainAreaCreate();
|
mainAreaCreate();
|
||||||
|
|
||||||
|
var footer = footerLoad();
|
||||||
|
cssLoad(cdn + "/css/commento.css", "window.commento.loadCssOverride()");
|
||||||
|
|
||||||
|
if (noFonts !== "true") {
|
||||||
|
classAdd(root, "root-font");
|
||||||
|
}
|
||||||
|
|
||||||
selfGet(function() {
|
selfGet(function() {
|
||||||
commentsGet(function() {
|
commentsGet(function() {
|
||||||
modToolsCreate();
|
modToolsCreate();
|
||||||
rootCreate(function() {
|
rootCreate(function() {
|
||||||
commentsRender();
|
commentsRender();
|
||||||
footerLoad();
|
append(root, footer);
|
||||||
attrSet(root, "style", "");
|
loadHash();
|
||||||
|
global.allShow();
|
||||||
|
nameWidthFix();
|
||||||
call(callback);
|
call(callback);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1684,4 +1878,4 @@
|
|||||||
readyLoad();
|
readyLoad();
|
||||||
|
|
||||||
|
|
||||||
}(window, document));
|
}(window.commento, document));
|
||||||
|
|||||||
96
frontend/js/count.js
Normal file
96
frontend/js/count.js
Normal 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));
|
||||||
@@ -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.
|
// Freezes a domain.
|
||||||
global.domainFreezeHandler = function() {
|
global.domainFreezeHandler = function() {
|
||||||
var data = global.dashboard.$data;
|
var data = global.dashboard.$data;
|
||||||
|
|||||||
@@ -102,6 +102,8 @@
|
|||||||
|
|
||||||
global.vs("domains", resp.domains);
|
global.vs("domains", resp.domains);
|
||||||
|
|
||||||
|
global.vs("configuredOauths", resp.configuredOauths);
|
||||||
|
|
||||||
if (callback !== undefined) {
|
if (callback !== undefined) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -118,14 +120,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
global.post(global.origin + "/api/domain/update", json, function(resp) {
|
global.post(global.origin + "/api/domain/update", json, function(resp) {
|
||||||
if (callback !== undefined) {
|
|
||||||
callback(resp.success);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
global.globalErrorShow(resp.message);
|
global.globalErrorShow(resp.message);
|
||||||
return;
|
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));
|
} (window.commento, document));
|
||||||
|
|||||||
@@ -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));
|
} (window.commento, document));
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
{
|
{
|
||||||
"id": "general",
|
"id": "general",
|
||||||
"text": "General",
|
"text": "General",
|
||||||
"meaning": "Email settings, data export",
|
"meaning": "Names, authentication, and export",
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"open": global.generalOpen,
|
"open": global.generalOpen,
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
{
|
{
|
||||||
"id": "danger",
|
"id": "danger",
|
||||||
"text": "Danger Zone",
|
"text": "Danger Zone",
|
||||||
"meaning": "Delete or freeze domain",
|
"meaning": "Here be dragons",
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"open": global.dangerOpen,
|
"open": global.dangerOpen,
|
||||||
},
|
},
|
||||||
@@ -72,6 +72,9 @@
|
|||||||
// list of domains dynamically loaded; obviously mutable
|
// list of domains dynamically loaded; obviously mutable
|
||||||
domains: [{show: false, viewsLast30Days: global.numberify(0), commentsLast30Days: global.numberify(0), moderators: []}],
|
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
|
// whether or not to show the settings column; mutable because we do not
|
||||||
// show the column until a domain has been selected
|
// show the column until a domain has been selected
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
(document);
|
(document);
|
||||||
|
|
||||||
// Talks to the API and sends an reset email.
|
// 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) {
|
var allOk = global.unfilledMark(["#email"], function(el) {
|
||||||
el.css("border-bottom", "1px solid red");
|
el.css("border-bottom", "1px solid red");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,9 @@
|
|||||||
|
|
||||||
|
|
||||||
// Logs the user in and redirects to the dashboard.
|
// 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) {
|
var allOk = global.unfilledMark(["#email", "#password"], function(el) {
|
||||||
el.css("border-bottom", "1px solid red");
|
el.css("border-bottom", "1px solid red");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
(function (global, document) {
|
(function (global, document) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
global.resetPassword = function() {
|
global.resetPassword = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
var allOk = global.unfilledMark(["#password", "#password2"], function(el) {
|
var allOk = global.unfilledMark(["#password", "#password2"], function(el) {
|
||||||
el.css("border-bottom", "1px solid red");
|
el.css("border-bottom", "1px solid red");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
// Signs up the user and redirects to either the login page or the email
|
// 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
|
// confirmation, depending on whether or not SMTP is configured in the
|
||||||
// backend.
|
// backend.
|
||||||
global.signup = function() {
|
global.signup = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
if ($("#password").val() !== $("#password2").val()) {
|
if ($("#password").val() !== $("#password2").val()) {
|
||||||
global.textSet("#err", "The two passwords don't match");
|
global.textSet("#err", "The two passwords don't match");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/login.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/login.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
<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>
|
<title>Commento: Login</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
|
|
||||||
<div class="auth-form-container">
|
<div class="auth-form-container">
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
|
<form onsubmit="window.commento.login(event)">
|
||||||
<div class="form-title">
|
<div class="form-title">
|
||||||
Login to continue
|
Login to continue
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +42,8 @@
|
|||||||
<div class="err" id="err"></div>
|
<div class="err" id="err"></div>
|
||||||
<div class="msg" id="msg"></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]]]/forgot">Trouble logging in? Reset your password.</a>
|
||||||
<a class="link" href="[[[.Origin]]]/signup">Don't have an account yet? Sign up.</a>
|
<a class="link" href="[[[.Origin]]]/signup">Don't have an account yet? Sign up.</a>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/reset.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/reset.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
<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>
|
<title>Commento: Reset your Password</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@
|
|||||||
|
|
||||||
<div class="auth-form-container">
|
<div class="auth-form-container">
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
|
<form onsubmit="window.commento.resetPassword(event)">
|
||||||
<div class="form-title">
|
<div class="form-title">
|
||||||
Reset your Password
|
Reset your Password
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
|
|
||||||
<div class="err" id="err"></div>
|
<div class="err" id="err"></div>
|
||||||
<div class="msg" id="msg"></div>
|
<div class="msg" id="msg"></div>
|
||||||
<button id="reset-button" class="button" onclick="window.commento.resetPassword()">Reset Password</button>
|
<button id="reset-button" class="button" type="submit">Reset Password</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,14 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #555;
|
color: #555;
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
display: table;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin-left: 48px;
|
margin-left: 48px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-flagged::after {
|
.commento-flagged::after {
|
||||||
@@ -39,6 +40,19 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
padding: 2px 6px 2px 6px;
|
padding: 2px 6px 2px 6px;
|
||||||
border-radius: 100px;
|
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 {
|
.commento-subtitle {
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: $red-1;
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
line-height: 1.5;
|
font-size: 13px;
|
||||||
color: $red-6;
|
|
||||||
padding: 2px;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $blue-6;
|
color: $blue-6;
|
||||||
border-bottom: 1px solid $blue-6;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus {
|
blockquote {
|
||||||
box-shadow: 0 0 0 1px rgba(87, 85, 217, .2);
|
margin: 0 0 0 8px;
|
||||||
|
padding: 0 0 0 5px;
|
||||||
|
border-left: 2px solid $gray-5;
|
||||||
|
color: $gray-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-button {
|
.commento-button {
|
||||||
@@ -36,7 +34,6 @@ a:focus {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
width: 100px;
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
.commento-footer {
|
.commento-footer {
|
||||||
margin: 36px 0px 12px 0px;
|
margin: 36px 0px 12px 0px;
|
||||||
border-top: 1px solid $gray-1;
|
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
|
||||||
.commento-logo-container {
|
.commento-logo-container {
|
||||||
@@ -16,19 +15,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 3px;
|
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 {
|
.commento-logo-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: $gray-6;
|
color: $gray-7;
|
||||||
display: inline;
|
display: inline;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ textarea::placeholder {
|
|||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
white-space: pre-wrap;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -67,7 +67,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.commento-button-margin {
|
.commento-button-margin {
|
||||||
padding-bottom: 60px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-anonymous-checkbox-container {
|
.commento-anonymous-checkbox-container {
|
||||||
@@ -90,3 +90,34 @@ textarea {
|
|||||||
margin-top: -1px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
left: 48px;
|
left: 48px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,8 @@
|
|||||||
.commento-login-box {
|
.commento-login-box {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
min-height: 125px;
|
min-height: 100px;
|
||||||
background: $gray-1;
|
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;
|
z-index: 100;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.commento-oauth-buttons {
|
.commento-oauth-buttons {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -16,12 +14,35 @@
|
|||||||
background: #dd4b39;
|
background: #dd4b39;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-github-button {
|
.commento-github-button {
|
||||||
background: #000000;
|
background: #000000;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 13px;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
@import "source-sans.scss";
|
@import "source-sans.scss";
|
||||||
|
|
||||||
.commento-root-min-height {
|
.commento-root-min-height {
|
||||||
min-height: 350px;
|
min-height: 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commento-root-font {
|
||||||
|
* {
|
||||||
|
font-family: 'Source Sans Pro', sans-serif !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-root {
|
.commento-root {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 0px;
|
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;
|
font-size: 15px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #50596c;
|
color: #50596c;
|
||||||
@@ -30,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.commento-blurred {
|
.commento-blurred {
|
||||||
filter: blur(4px);
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commento-main-area {
|
.commento-main-area {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "colors-main.scss";
|
@import "colors-main.scss";
|
||||||
|
@import "source-sans.scss";
|
||||||
|
|
||||||
html {
|
html, input, button, textarea {
|
||||||
font-family: 'Source Sans Pro', sans-serif;
|
font-family: 'Source Sans Pro', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $gray-7;
|
color: $gray-7;
|
||||||
|
|||||||
@@ -3,47 +3,6 @@
|
|||||||
@import "checkbox.scss";
|
@import "checkbox.scss";
|
||||||
@import "button.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 {
|
.global-error, .global-ok {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
@@ -71,9 +30,12 @@ body {
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
border-bottom: 1px solid $gray-4;
|
border-bottom: 1px solid $gray-1;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $gray-7;
|
||||||
|
font-size: 14px;
|
||||||
background: none;
|
background: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@@ -83,8 +45,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.current {
|
li.current {
|
||||||
border-bottom: 1px solid $blue-6;
|
|
||||||
transition: all 0.1s;
|
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";
|
@import "email-main.scss";
|
||||||
|
|
||||||
.mod-emails-container {
|
.mod-emails-container {
|
||||||
@@ -281,7 +281,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setting-subtitle {
|
.setting-subtitle {
|
||||||
color: $gray-5;
|
color: $gray-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.super-setting {
|
.super-setting {
|
||||||
@@ -306,23 +306,17 @@ body {
|
|||||||
|
|
||||||
.pane-setting:hover {
|
.pane-setting:hover {
|
||||||
color: $gray-6;
|
color: $gray-6;
|
||||||
-webkit-box-shadow: inset -2px 0px 0 -1px $gray-4;
|
background: $gray-1;
|
||||||
-moz-box-shadow: inset -2px 0px 0 -1px $gray-4;
|
|
||||||
box-shadow: inset -2px 0px 0 -1px $gray-4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
color: $blue-6;
|
color: $blue-7;
|
||||||
-webkit-box-shadow: inset -2px 0px 0 -1px $blue-6;
|
background: $gray-2;
|
||||||
-moz-box-shadow: inset -2px 0px 0 -1px $blue-6;
|
|
||||||
box-shadow: inset -2px 0px 0 -1px $blue-6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected:hover {
|
.selected:hover {
|
||||||
color: $blue-7;
|
color: $blue-7;
|
||||||
-webkit-box-shadow: inset -2px 0px 0 -1px $blue-6;
|
background: $gray-2;
|
||||||
-moz-box-shadow: inset -2px 0px 0 -1px $blue-6;
|
|
||||||
box-shadow: inset -2px 0px 0 -1px $blue-6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,14 +367,37 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
font-size: 15px;
|
padding: 8px 0px 8px 0px;
|
||||||
|
margin: 8px 0px 8px 0px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
color: $gray-7;
|
color: $gray-7;
|
||||||
margin-bottom: 10px;
|
font-size: 14px;
|
||||||
|
width: 35%;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $orange-8;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
@@ -527,56 +544,11 @@ body {
|
|||||||
.input::placeholder {
|
.input::placeholder {
|
||||||
color: $gray-4;
|
color: $gray-4;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.theme {
|
.monospace {
|
||||||
display: block;
|
font-family: monospace;
|
||||||
width: calc(100% - 20px);
|
font-size: 11px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.no-border {
|
||||||
@@ -585,7 +557,7 @@ body {
|
|||||||
|
|
||||||
.indent {
|
.indent {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
padding-left: 32px;
|
padding-left: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
@@ -667,12 +639,11 @@ foreignObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.red-button {
|
.red-button {
|
||||||
border: 1px solid $gray-3;
|
color: $red-8;
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.red-button:hover {
|
.red-button:hover {
|
||||||
border: 1px solid $red-6;
|
color: $red-7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.green-button {
|
.green-button {
|
||||||
@@ -688,7 +659,7 @@ foreignObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.orange-button {
|
.orange-button {
|
||||||
color: $orange-7;
|
color: $orange-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orange-button:hover {
|
.orange-button:hover {
|
||||||
@@ -793,6 +764,6 @@ foreignObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
|
||||||
.commento-input {
|
.commento-input {
|
||||||
|
display: inline;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: $white;
|
background: $white;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -2,22 +2,88 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-display: swap;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic-ext.woff2) format('woff2');
|
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;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-display: swap;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-cryllic.woff2) format('woff2');
|
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;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
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;
|
font-weight: 400;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek-ext.woff2) format('woff2');
|
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;
|
unicode-range: U+1F00-1FFF;
|
||||||
@@ -26,6 +92,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek.woff2) format('woff2');
|
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-greek.woff2) format('woff2');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
@@ -34,6 +101,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-vietnamese.woff2) format('woff2');
|
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;
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
@@ -42,6 +110,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin-ext.woff2) format('woff2');
|
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;
|
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-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url([[[.CdnPrefix]]]/fonts/source-sans-400-latin.woff2) format('woff2');
|
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;
|
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-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
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;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
/* cyrillic */
|
/* cyrillic */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cryllic.woff2) format('woff2');
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-cyrillic.woff2) format('woff2');
|
||||||
unicode-range: U+0700-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
/* greek-ext */
|
/* greek-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek-ext.woff2) format('woff2');
|
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;
|
unicode-range: U+1F00-1FFF;
|
||||||
@@ -82,6 +155,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek.woff2) format('woff2');
|
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-greek.woff2) format('woff2');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
@@ -90,6 +164,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-vietnamese.woff2) format('woff2');
|
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;
|
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||||
@@ -98,6 +173,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin-ext.woff2) format('woff2');
|
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;
|
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-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url([[[.CdnPrefix]]]/fonts/source-sans-700-latin.woff2) format('woff2');
|
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;
|
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;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/signup.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/signup.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
<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>
|
<title>Commento: Signup</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -24,6 +23,7 @@
|
|||||||
|
|
||||||
<div class="auth-form-container">
|
<div class="auth-form-container">
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
|
<form onsubmit="window.commento.signup(event)">
|
||||||
<div class="form-title">
|
<div class="form-title">
|
||||||
Create an account
|
Create an account
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,8 @@
|
|||||||
<p class="cent">
|
<p class="cent">
|
||||||
</p>
|
</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>
|
<a class="link" href="[[[.Origin]]]/login">Already have an account? Login instead.</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<script src="[[[.CdnPrefix]]]/js/unsubscribe.js"></script>
|
<script src="[[[.CdnPrefix]]]/js/unsubscribe.js"></script>
|
||||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/unsubscribe.css">
|
<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>
|
<title>Commento: Unsubscribe</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ ctrl_c() {
|
|||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|
||||||
version=devel
|
if [[ "$1" == "" ]]; then
|
||||||
|
version=devel
|
||||||
|
else
|
||||||
|
version=$1
|
||||||
|
fi
|
||||||
|
|
||||||
binary_pid=
|
binary_pid=
|
||||||
if make $version -j$(($(nproc) + 1)); then
|
if make $version -j$(($(nproc) + 1)); then
|
||||||
|
|||||||
@@ -5,40 +5,6 @@
|
|||||||
<title>You have {{ .Subject }}</title>
|
<title>You have {{ .Subject }}</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@media only screen and (max-width:600px) {
|
@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 {
|
.logo {
|
||||||
display: block;
|
display: block;
|
||||||
float: none;
|
float: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user