Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e434f59f9a | ||
|
|
a4fbf67d73 | ||
|
|
1aea90cb07 | ||
|
|
20b6660fa9 | ||
|
|
815628c5ee | ||
|
|
6caa3e312c | ||
|
|
94829d9b83 | ||
|
|
7be22b091f | ||
|
|
fff5e5c0e1 | ||
|
|
f1ece27c99 | ||
|
|
5e48da6940 | ||
|
|
28fe1aaa89 | ||
|
|
f846935a2a | ||
|
|
42b452b9f8 | ||
|
|
514535a607 | ||
|
|
55f24b2de2 | ||
|
|
24d76c2fb6 | ||
|
|
f2ff2b4940 | ||
|
|
6d1563e22a | ||
|
|
9a3c181442 | ||
|
|
010b7336cd | ||
|
|
00c197e2ee | ||
|
|
c6a98d93e4 | ||
|
|
edd8aae7a7 | ||
|
|
3677d43aab | ||
|
|
0cdba65e48 | ||
|
|
022fc06257 | ||
|
|
61bc73e705 | ||
|
|
e0cf9a89f9 | ||
|
|
bc92df8083 | ||
|
|
71947bbe2c | ||
|
|
5a029e2786 | ||
|
|
7f9a39c330 | ||
|
|
633ccf427c | ||
|
|
51e4608c19 | ||
|
|
612e620ffc | ||
|
|
642076a231 | ||
|
|
f63639782c | ||
|
|
02615088ff | ||
|
|
5aa3bc86eb | ||
|
|
d5769d56c1 | ||
|
|
8eb0bc147c | ||
|
|
96589a2658 | ||
|
|
34e39edcda | ||
|
|
6d00a8e3aa | ||
|
|
c29b3a7a25 | ||
|
|
a99bf15332 | ||
|
|
7074800ecc | ||
|
|
80fb09d941 | ||
|
|
afabc25037 | ||
|
|
c9e7a3f40a | ||
|
|
4d82106aff | ||
|
|
4c0e261a8e | ||
|
|
9e3935b3b2 | ||
|
|
e4f71fe402 | ||
|
|
06c71f4e65 | ||
|
|
9fcf67d667 | ||
|
|
d1318daaca | ||
|
|
87a0c577bb | ||
|
|
bcc81e1ad8 | ||
|
|
1f8f3b3a36 | ||
|
|
610b61831d | ||
|
|
cf0b394b05 | ||
|
|
a36b11f07d | ||
|
|
93c9ce0cad | ||
|
|
af88db42b2 | ||
|
|
0c6ccdc0a1 | ||
|
|
6d3f8171e5 | ||
|
|
3f1c570e84 | ||
|
|
cd88ae264e | ||
|
|
b2abcae319 | ||
|
|
41a5c675bf | ||
|
|
ac9f896a22 | ||
|
|
36d57914b2 | ||
|
|
800ba5dd0d | ||
|
|
a793f7b3b4 | ||
|
|
0d6dfb8319 | ||
|
|
c5d2e17615 | ||
|
|
93595f3877 | ||
|
|
c21329ac4e | ||
|
|
8500a3f7c6 | ||
|
|
4ffdd2cfb6 | ||
|
|
b4f2ba41be | ||
|
|
8ebc0cd965 | ||
|
|
405d10766a | ||
|
|
2a713c22f1 | ||
|
|
d6ccb7338c | ||
|
|
4d799182da | ||
|
|
f54f4d0afd | ||
|
|
988a9fb1a1 | ||
|
|
283a32e2bb | ||
|
|
330131f390 | ||
|
|
299649cea2 | ||
|
|
0a03a2c6fc |
@@ -3,14 +3,17 @@ stages:
|
||||
- go-fmt
|
||||
- go-test
|
||||
- build-src
|
||||
- aws-upload-tags
|
||||
- build-docker
|
||||
- docker-registry
|
||||
- docker-registry-master
|
||||
- docker-registry-tags
|
||||
|
||||
check-dco:
|
||||
stage: check-dco
|
||||
image: debian:buster
|
||||
except:
|
||||
- master
|
||||
- tags
|
||||
script:
|
||||
- apt update
|
||||
- apt install -y curl git jq
|
||||
@@ -19,21 +22,37 @@ check-dco:
|
||||
build-src:
|
||||
stage: build-src
|
||||
image: debian:buster
|
||||
variables:
|
||||
GOPATH: $CI_PROJECT_DIR
|
||||
except:
|
||||
- master
|
||||
- tags
|
||||
before_script:
|
||||
- bash $CI_PROJECT_DIR/scripts/gitlab-ci-build-prescript
|
||||
script:
|
||||
- apt update
|
||||
- apt install -y curl gnupg git make golang
|
||||
- curl -sL https://deb.nodesource.com/setup_10.x | bash -
|
||||
- apt update
|
||||
- apt install -y nodejs
|
||||
- npm install -g html-minifier@3.5.7 uglify-js@3.4.1 sass@1.5.1
|
||||
- mkdir -p src/gitlab.com/commento && cd src/gitlab.com/commento && ln -s $CI_PROJECT_DIR && cd $CI_PROJECT_NAME
|
||||
- export GOPATH=/go
|
||||
- export PATH=$PATH:/go/bin
|
||||
- cd /go/src/$CI_PROJECT_NAME
|
||||
- make devel
|
||||
- make prod
|
||||
|
||||
aws-upload-tags:
|
||||
stage: aws-upload-tags
|
||||
image: debian:buster
|
||||
environment: aws-upload-tags
|
||||
variables:
|
||||
AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
|
||||
only:
|
||||
- tags
|
||||
before_script:
|
||||
- bash $CI_PROJECT_DIR/scripts/gitlab-ci-build-prescript
|
||||
script:
|
||||
- export GOPATH=/go
|
||||
- export PATH=$PATH:/go/bin
|
||||
- cd /go/src/$CI_PROJECT_NAME
|
||||
- make prod
|
||||
- cd build/prod && tar -zcvf /commento-linux-amd64-$(git describe --tags).tgz .
|
||||
- aws s3 cp /commento-linux-amd64-$(git describe --tags).tgz s3://commento-release/
|
||||
|
||||
build-docker:
|
||||
stage: build-docker
|
||||
image: docker:stable
|
||||
@@ -41,8 +60,9 @@ build-docker:
|
||||
- docker:dind
|
||||
except:
|
||||
- master
|
||||
- tags
|
||||
script:
|
||||
- docker build -t commento-ce .
|
||||
- docker build -t commento .
|
||||
|
||||
go-test:
|
||||
stage: go-test
|
||||
@@ -54,11 +74,17 @@ go-test:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: commento_test
|
||||
COMMENTO_POSTGRES: postgres://postgres:postgres@postgres/commento_test?sslmode=disable
|
||||
GOPATH: $CI_PROJECT_DIR
|
||||
except:
|
||||
- master
|
||||
- tags
|
||||
before_script:
|
||||
- mkdir -p /go/src /go/bin /go/pkg
|
||||
- export GOPATH=/go
|
||||
- export PATH=$PATH:/go/bin
|
||||
- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
- ln -s $CI_PROJECT_DIR /go/src/$CI_PROJECT_NAME
|
||||
script:
|
||||
- mkdir -p src/gitlab.com/commento && cd src/gitlab.com/commento && ln -s $CI_PROJECT_DIR && cd $CI_PROJECT_NAME
|
||||
- cd /go/src/$CI_PROJECT_NAME
|
||||
- make test
|
||||
|
||||
go-fmt:
|
||||
@@ -66,20 +92,35 @@ go-fmt:
|
||||
image: golang:1.10.2
|
||||
except:
|
||||
- master
|
||||
- tags
|
||||
script:
|
||||
- cd api
|
||||
- test -z $(go fmt)
|
||||
|
||||
docker-registry:
|
||||
stage: docker-registry
|
||||
docker-registry-master:
|
||||
stage: docker-registry-master
|
||||
image: docker:stable
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- master@commento/commento-ce
|
||||
- master@commento/commento
|
||||
before_script:
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
|
||||
script:
|
||||
- docker pull registry.gitlab.com/commento/commento-ce:latest || true
|
||||
- docker build --cache-from registry.gitlab.com/commento/commento-ce:latest --tag registry.gitlab.com/commento/commento-ce:latest .
|
||||
- docker push registry.gitlab.com/commento/commento-ce:latest
|
||||
- docker pull registry.gitlab.com/commento/commento:latest || true
|
||||
- docker build --cache-from registry.gitlab.com/commento/commento:latest --tag registry.gitlab.com/commento/commento:latest .
|
||||
- docker push registry.gitlab.com/commento/commento:latest
|
||||
|
||||
docker-registry-tags:
|
||||
stage: docker-registry-tags
|
||||
image: docker:stable
|
||||
services:
|
||||
- docker:dind
|
||||
only:
|
||||
- tags
|
||||
before_script:
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
|
||||
script:
|
||||
- apk add git
|
||||
- docker build --tag registry.gitlab.com/commento/commento:$(git describe --tags) .
|
||||
- docker push registry.gitlab.com/commento/commento:$(git describe --tags)
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,10 +1,11 @@
|
||||
# backend build (api server)
|
||||
FROM golang:1.10.2-alpine AS api-build
|
||||
|
||||
COPY ./api /go/src/commento-ce/api
|
||||
WORKDIR /go/src/commento-ce/api
|
||||
COPY ./api /go/src/commento/api
|
||||
WORKDIR /go/src/commento/api
|
||||
|
||||
RUN apk update && apk add bash make git
|
||||
RUN apk update && apk add bash make git curl
|
||||
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
|
||||
RUN make prod -j$(($(nproc) + 1))
|
||||
|
||||
@@ -12,8 +13,8 @@ RUN make prod -j$(($(nproc) + 1))
|
||||
# frontend build (html, js, css, images)
|
||||
FROM node:10.3.0-alpine AS frontend-build
|
||||
|
||||
COPY ./frontend /commento-ce/frontend/
|
||||
WORKDIR /commento-ce/frontend/
|
||||
COPY ./frontend /commento/frontend/
|
||||
WORKDIR /commento/frontend/
|
||||
|
||||
RUN apk update && apk add bash make
|
||||
RUN npm install -g html-minifier@3.5.7 uglify-js@3.4.1 sass@1.5.1
|
||||
@@ -24,8 +25,8 @@ RUN make prod -j$(($(nproc) + 1))
|
||||
# templates build
|
||||
FROM alpine:3.7 AS templates-build
|
||||
|
||||
COPY ./templates /commento-ce/templates
|
||||
WORKDIR /commento-ce/templates
|
||||
COPY ./templates /commento/templates
|
||||
WORKDIR /commento/templates
|
||||
|
||||
RUN apk update && apk add bash make
|
||||
|
||||
@@ -35,8 +36,8 @@ RUN make prod -j$(($(nproc) + 1))
|
||||
# db build
|
||||
FROM alpine:3.7 AS db-build
|
||||
|
||||
COPY ./db /commento-ce/db
|
||||
WORKDIR /commento-ce/db
|
||||
COPY ./db /commento/db
|
||||
WORKDIR /commento/db
|
||||
|
||||
RUN apk update && apk add bash make
|
||||
|
||||
@@ -46,19 +47,19 @@ RUN make prod -j$(($(nproc) + 1))
|
||||
# final image
|
||||
FROM alpine:3.7
|
||||
|
||||
COPY --from=api-build /go/src/commento-ce/api/build/prod/commento-ce /commento-ce/commento-ce
|
||||
COPY --from=frontend-build /commento-ce/frontend/build/prod/*.html /commento-ce/
|
||||
COPY --from=frontend-build /commento-ce/frontend/build/prod/css/*.css /commento-ce/css/
|
||||
COPY --from=frontend-build /commento-ce/frontend/build/prod/js/*.js /commento-ce/js/
|
||||
COPY --from=frontend-build /commento-ce/frontend/build/prod/images/* /commento-ce/images/
|
||||
COPY --from=templates-build /commento-ce/templates/build/prod/templates/ /commento-ce/templates/
|
||||
COPY --from=db-build /commento-ce/db/build/prod/db/ /commento-ce/db/
|
||||
COPY --from=api-build /go/src/commento/api/build/prod/commento /commento/commento
|
||||
COPY --from=frontend-build /commento/frontend/build/prod/*.html /commento/
|
||||
COPY --from=frontend-build /commento/frontend/build/prod/css/*.css /commento/css/
|
||||
COPY --from=frontend-build /commento/frontend/build/prod/js/*.js /commento/js/
|
||||
COPY --from=frontend-build /commento/frontend/build/prod/images/* /commento/images/
|
||||
COPY --from=templates-build /commento/templates/build/prod/templates/ /commento/templates/
|
||||
COPY --from=db-build /commento/db/build/prod/db/ /commento/db/
|
||||
|
||||
RUN apk update && apk add ca-certificates --no-cache
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
WORKDIR /commento-ce/
|
||||
WORKDIR /commento/
|
||||
|
||||
ENV COMMENTO_BIND_ADDRESS="0.0.0.0"
|
||||
ENTRYPOINT ["/commento-ce/commento-ce"]
|
||||
ENTRYPOINT ["/commento/commento"]
|
||||
|
||||
72
README.md
72
README.md
@@ -4,41 +4,69 @@
|
||||
|
||||
<p align="center"><b>A bloat-free and privacy-focused discussion platform.</b></p>
|
||||
|
||||
Commento is a discussion platform that you can embed on your blog, news articles, and any place where you want your readers to add comments. Commento is fast, lightweight, and privacy-focused; we'll never sell your data, show ads, embed third-party tracking scripts, or inject affiliate links.
|
||||
### What is Commento?
|
||||
|
||||
#### Features
|
||||
Commento allows you to foster discussion on your website – if you have a blog, you can embed Commento if you want your readers to add comments. It's fast and bloat-free, has a modern interface, and is reasonably secure. Unlike most alternatives, Commento is lightweight and privacy-focused; I'll never sell your data, show ads, embed third-party tracking scripts, or inject affiliate links.
|
||||
|
||||
- Privacy-focused
|
||||
- Super lightweight, allowing for fast pageloads
|
||||
- Automatic spam filtering
|
||||
- Review and approve or delete comments through the moderation interface
|
||||
- Modern interface with a clean design
|
||||
- OAuth support (Google login, for example)
|
||||
- Custom CSS theming
|
||||
- Import from existing services (like Disqus)
|
||||
- Completely free and open source (MIT Expat license)
|
||||
### Frequently Asked Questions
|
||||
|
||||
#### Editions
|
||||
**I don't want to install and manage Commento on a server.**
|
||||
You can use [Commento.io](https://commento.io), the cloud version of Commento, where I do the server hosting, updates, and security and performance tuning for you. To make the hosted service self-sustainable, it is not free. You may choose the plan that best matches your financial situation and needs – all plans have all features.
|
||||
|
||||
There are three editions of Commento.
|
||||
**What features does Commento have?**
|
||||
Commento comes with a lot of useful features out-of-the-box: rich text support, upvotes and downvotes, automatic spam detection, moderation tools, sticky comments, thread locking, OAuth login, email notifications, and more!
|
||||
|
||||
- **Commento Community Edition (CE)** is open source software that's freely available under the MIT license.
|
||||
- [**Commento Enterprise Edition (EE)**](https://commento.io/pricing#self-hosted) includes extra features geared towards organizations that want to self-host.
|
||||
- [**Commento Hosted**](https://commento.io) is a hosted version of Commento for those who don't want to host and manage servers. This is currently in private beta and you can [add yourself to the waiting list here](https://commento.io).
|
||||
**What does Commento look like? Do you have a demo?**
|
||||
Check out [demo.commento.io](https://demo.commento.io) to play around with a live demo of Commento.
|
||||
|
||||
#### Installation and Configuration
|
||||
**How is Commento different from Disqus, Facebook Comments, and the rest?**
|
||||
Most other products in this space do not respect your privacy; showing adverts is their primary business model and that nearly always comes at the users' cost. There is no free lunch. Commento is also orders of magnitude lighter than alternatives – while Disqus and Facebook take megabytes of download to load, Commento is just 11 kB.
|
||||
|
||||
See our [documentation on how to install Commento](http://docs.commento.io/installation.html) to get started. We offer several ways to install the software, including a Docker image.
|
||||
**Is Commento free software?**
|
||||
Yes. Commento is made [freely available](https://gitlab.com/commento/commento) under the [MIT license](https://gitlab.com/commento/commento/blob/master/LICENSE). And it will always stay that way.
|
||||
|
||||
Once you've installed the software, you need to configure it with various environment variables before starting the service. To learn more about this, refer to our documentation on [configuring Commento](https://docs.commento.io/configuration.html).
|
||||
**Disqus has a free plan. Why is the [cloud version](https://commento.io) not free of cost?**
|
||||
When I say Commento is free, I mean [free as in freedom](https://www.gnu.org/philosophy/free-sw.en.html). The cloud version is not offered free of cost because servers cost money and offering the service for free would not be sustainable. Unlike most alternatives, Commento does not operate on adverts and shady tactics; you're the customer, not the product.
|
||||
|
||||
#### Contributing
|
||||
**I have nothing to hide. Why should I care about my privacy?**
|
||||
The thing about privacy is that once you give up control over your information, you can't get it back. You may be fine with having your personal information sold to unknown third-parties today, but when your insurance company uses this information against you tomorrow, you'll regret it. And you'll have no recourse to correct this. Read [this Wikipedia article](https://en.wikipedia.org/wiki/Nothing_to_hide_argument) for more information.
|
||||
|
||||
Commento is possible only because of its community. If this is your first contribution to Commento, please go through the [development documentation](https://docs.commento.io/contributing.html) before you begin.
|
||||
<div><p style="margin: 0px 0px"><b>As a blog owner, why should I worry about my readers' privacy?</b><br>
|
||||
Good question. For starters, your readers value their privacy. Not caring about them is disrespectful and you will end up alienating your audience; they won't come back. But even if you ignore this, you have bigger questions to answer:</p>
|
||||
<ul>
|
||||
<li><b>Legality</b>: Did you know that Disqus still isn't GDPR-compliant (according to their <a href="https://help.disqus.com/terms-and-policies/privacy-faq" title="At the time of writing (28 December 2018)" rel="nofollow">privacy policy</a>)?</li>
|
||||
<li><b>Security</b>: What happens when a random third-party script is injected into your website?</li>
|
||||
<li><b>Performance</b>: Did you know that half a second increase in page load time results in a 20% decrease in engagement and site traffic?</li>
|
||||
<li><b>Ownership</b>: Who owns the content when your readers create comments?</li>
|
||||
</ul></div>
|
||||
|
||||
**Who's behind this? Are you an evil corporation?**
|
||||
My name is <a href="https://adtac.in">Adhityaa</a>, and I created the project. As someone who's still a student, I promise you I'm neither evil nor a corporation. But I'm not the only one – dozens of people have contributed to the project and Commento would not exist without these wonderful people.
|
||||
|
||||
**Okay, how do I get started?**
|
||||
Glad you asked! You have two options – self-hosting Commento on your own server or using the [cloud version](https://commento.io). Start [from here](https://docs.commento.io/getting-started/) to decide which option is right for you and proceed from there.
|
||||
|
||||
### Installation
|
||||
|
||||
See our [documentation on how to install Commento](https://docs.commento.io/installation/) to get started.
|
||||
|
||||
### Contributing
|
||||
|
||||
Commento is possible only because of its community. If this is your first contribution to Commento, please go through the [documentation](https://docs.commento.io/contributing/) before you begin.
|
||||
|
||||
Help will always be given to those who ask for it. We use IRC for chat to collaborate with other developers. You're invited to [hang out with us](https://irc.commento.io) in the `#commento-dev` channel on freenode if you want to contribute to Commento!
|
||||
|
||||
#### License
|
||||
### Sponsors
|
||||
|
||||
Commento development is partially sponsored by [Mozilla](https://mozilla.org) and [DigitalOcean](https://www.digitalocean.com/) independently.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.mozilla.org/en-US/"><img src="https://user-images.githubusercontent.com/7521600/32265838-d05b2d08-bf0a-11e7-92e1-2cb183eae616.png" title="Mozilla" height="40"></a>
|
||||
|
||||
<a href="https://www.digitalocean.com"><img src="https://user-images.githubusercontent.com/7521600/32265839-d093c7da-bf0a-11e7-8d99-96a940041d06.png" title="DigitalOcean" height="40"></a>
|
||||
</p>
|
||||
|
||||
### License
|
||||
|
||||
```
|
||||
Copyright 2018 Commento, Inc.
|
||||
|
||||
13
Gopkg.lock → api/Gopkg.lock
generated
13
Gopkg.lock → api/Gopkg.lock
generated
@@ -9,6 +9,14 @@
|
||||
revision = "64a2037ec6be8a4b0c1d1f706ed35b428b989239"
|
||||
version = "v0.26.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9769b231d8f5ff406a012aa7f293e45ed69d11617832a1c3c7b8c6ce1558a2a1"
|
||||
name = "github.com/adtac/go-akismet"
|
||||
packages = ["akismet"]
|
||||
pruneopts = "UT"
|
||||
revision = "0ca9e1023047c869ecd4bd3c20780511597a4a77"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:15042ad3498153684d09f393bbaec6b216c8eec6d61f63dff711de7d64ed8861"
|
||||
name = "github.com/golang/protobuf"
|
||||
@@ -108,10 +116,11 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bea0314c10bd362ab623af4880d853b5bad3b63d0ab9945c47e461b8d04203ed"
|
||||
digest = "1:82e6e4dc5ab71680d89684e4649be630fdeeaf81feb8e88e4a56273a0cd4d966"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [
|
||||
".",
|
||||
"github",
|
||||
"google",
|
||||
"internal",
|
||||
"jws",
|
||||
@@ -143,6 +152,7 @@
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/adtac/go-akismet/akismet",
|
||||
"github.com/gorilla/handlers",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/lib/pq",
|
||||
@@ -152,6 +162,7 @@
|
||||
"github.com/russross/blackfriday",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/oauth2",
|
||||
"golang.org/x/oauth2/github",
|
||||
"golang.org/x/oauth2/google",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
10
api/Makefile
10
api/Makefile
@@ -7,9 +7,9 @@ PROD_BUILD_DIR = $(BUILD_DIR)/prod
|
||||
GO_SRC_DIR = .
|
||||
GO_SRC_FILES = $(wildcard $(GO_SRC_DIR)/*.go)
|
||||
GO_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)
|
||||
GO_DEVEL_BUILD_BINARY = $(GO_DEVEL_BUILD_DIR)/commento-ce
|
||||
GO_DEVEL_BUILD_BINARY = $(GO_DEVEL_BUILD_DIR)/commento
|
||||
GO_PROD_BUILD_DIR = $(PROD_BUILD_DIR)
|
||||
GO_PROD_BUILD_BINARY = $(GO_PROD_BUILD_DIR)/commento-ce
|
||||
GO_PROD_BUILD_BINARY = $(GO_PROD_BUILD_DIR)/commento
|
||||
|
||||
devel: devel-go
|
||||
|
||||
@@ -25,15 +25,15 @@ clean:
|
||||
# later down the line).
|
||||
|
||||
devel-go:
|
||||
go get .
|
||||
dep ensure
|
||||
go build -i -v -o $(GO_DEVEL_BUILD_BINARY)
|
||||
|
||||
prod-go:
|
||||
go get .
|
||||
dep ensure
|
||||
go build -i -v -o $(GO_PROD_BUILD_BINARY)
|
||||
|
||||
test-go:
|
||||
go get .
|
||||
dep ensure
|
||||
go test -v .
|
||||
|
||||
$(shell mkdir -p $(GO_DEVEL_BUILD_DIR) $(GO_PROD_BUILD_DIR))
|
||||
|
||||
31
api/akismet.go
Normal file
31
api/akismet.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/adtac/go-akismet/akismet"
|
||||
"os"
|
||||
)
|
||||
|
||||
func isSpam(domain string, userIp string, userAgent string, name string, email string, url string, markdown string) bool {
|
||||
akismetKey := os.Getenv("AKISMET_KEY")
|
||||
if akismetKey == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
res, err := akismet.Check(&akismet.Comment{
|
||||
Blog: domain,
|
||||
UserIP: userIp,
|
||||
UserAgent: userAgent,
|
||||
CommentType: "comment",
|
||||
CommentAuthor: name,
|
||||
CommentAuthorEmail: email,
|
||||
CommentAuthorURL: url,
|
||||
CommentContent: markdown,
|
||||
}, akismetKey)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error: cannot validate commenet using Akismet: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
43
api/comment_count.go
Normal file
43
api/comment_count.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func commentCount(domain string, path string) (int, error) {
|
||||
// path can be empty
|
||||
if domain == "" {
|
||||
return 0, errorMissingField
|
||||
}
|
||||
|
||||
p, err := pageGet(domain, path)
|
||||
if err != nil {
|
||||
return 0, errorInternal
|
||||
}
|
||||
|
||||
return p.CommentCount, nil
|
||||
}
|
||||
|
||||
func commentCountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
Domain *string `json:"domain"`
|
||||
Path *string `json:"path"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
domain := domainStrip(*x.Domain)
|
||||
path := *x.Path
|
||||
|
||||
count, err := commentCount(domain, path)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true, "count": count})
|
||||
}
|
||||
54
api/comment_count_test.go
Normal file
54
api/comment_count_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCommentCountBasics(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "http://example.com/photo.jpg", "google", "")
|
||||
|
||||
commentNew(commenterHex, "example.com", "/path.html", "root", "**foo**", "approved", time.Now().UTC())
|
||||
commentNew(commenterHex, "example.com", "/path.html", "root", "**bar**", "approved", time.Now().UTC())
|
||||
commentNew(commenterHex, "example.com", "/path.html", "root", "**baz**", "unapproved", time.Now().UTC())
|
||||
|
||||
count, err := commentCount("example.com", "/path.html")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error counting comments: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("expected count=2 got count=%d", count)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentCountNewPage(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
count, err := commentCount("example.com", "/path.html")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error counting comments: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if count != 0 {
|
||||
t.Errorf("expected count=0 got count=%d", count)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentCountEmpty(t *testing.T) {
|
||||
if _, err := commentCount("example.com", ""); err != nil {
|
||||
t.Errorf("unexpected error counting comments on empty path: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := commentCount("", ""); err == nil {
|
||||
t.Errorf("expected error not found counting comments with empty everything")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
)
|
||||
|
||||
func commentList(commenterHex string, domain string, path string, includeUnapproved bool) ([]comment, map[string]commenter, error) {
|
||||
if commenterHex == "" || domain == "" || path == "" {
|
||||
// path can be empty
|
||||
if commenterHex == "" || domain == "" {
|
||||
return nil, nil, errorMissingField
|
||||
}
|
||||
|
||||
@@ -111,6 +112,12 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := pageGet(domain, path)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
commenterHex := "anonymous"
|
||||
isModerator := false
|
||||
if *x.CommenterToken != "anonymous" {
|
||||
@@ -151,6 +158,7 @@ func commentListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
"requireIdentification": d.RequireIdentification,
|
||||
"isFrozen": d.State == "frozen",
|
||||
"isModerator": isModerator,
|
||||
"attributes": p,
|
||||
"configuredOauths": configuredOauths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ func commentNew(commenterHex string, domain string, path string, parentHex strin
|
||||
return "", errorMissingField
|
||||
}
|
||||
|
||||
p, err := pageGet(domain, path)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot get page attributes: %v", err)
|
||||
return "", errorInternal
|
||||
}
|
||||
|
||||
if p.IsLocked {
|
||||
return "", errorThreadLocked
|
||||
}
|
||||
|
||||
commentHex, err := randomHex(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -31,6 +41,10 @@ func commentNew(commenterHex string, domain string, path string, parentHex strin
|
||||
return "", errorInternal
|
||||
}
|
||||
|
||||
if err = pageNew(domain, path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return commentHex, nil
|
||||
}
|
||||
|
||||
@@ -64,19 +78,28 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// logic: (empty column indicates the value doesn't matter)
|
||||
// | anonymous | moderator | requireIdentification | requireModeration | approved? |
|
||||
// |-----------+-----------+-----------------------+-------------------+-----------|
|
||||
// | yes | | | | no |
|
||||
// | no | yes | | | yes |
|
||||
// | no | no | | yes | yes |
|
||||
// | no | no | | no | no |
|
||||
// | anonymous | moderator | requireIdentification | requireModeration | moderateAllAnonymous | approved? |
|
||||
// |-----------+-----------+-----------------------+-------------------+----------------------+-----------|
|
||||
// | yes | | | | no | yes |
|
||||
// | yes | | | | yes | no |
|
||||
// | no | yes | | | | yes |
|
||||
// | no | no | | yes | | yes |
|
||||
// | no | no | | no | | no |
|
||||
|
||||
var commenterHex string
|
||||
var state string
|
||||
|
||||
if *x.CommenterToken == "anonymous" {
|
||||
state = "unapproved"
|
||||
commenterHex = "anonymous"
|
||||
if isSpam(*x.Domain, getIp(r), getUserAgent(r), "Anonymous", "", "", *x.Markdown) {
|
||||
state = "flagged"
|
||||
} else {
|
||||
if d.ModerateAllAnonymous {
|
||||
state = "unapproved"
|
||||
} else {
|
||||
state = "approved"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c, err := commenterGetByCommenterToken(*x.CommenterToken)
|
||||
if err != nil {
|
||||
@@ -98,10 +121,14 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if isModerator {
|
||||
state = "approved"
|
||||
} else {
|
||||
if d.RequireModeration {
|
||||
state = "unapproved"
|
||||
if isSpam(*x.Domain, getIp(r), getUserAgent(r), c.Name, c.Email, c.Link, *x.Markdown) {
|
||||
state = "flagged"
|
||||
} else {
|
||||
state = "approved"
|
||||
if d.RequireModeration {
|
||||
state = "unapproved"
|
||||
} else {
|
||||
state = "approved"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,5 +139,5 @@ func commentNewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "approved": state == "approved"})
|
||||
bodyMarshal(w, response{"success": true, "commentHex": commentHex, "state": state, "html": markdownToHtml(*x.Markdown)})
|
||||
}
|
||||
|
||||
@@ -56,3 +56,18 @@ func TestCommentNewUpvoted(t *testing.T) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentNewThreadLocked(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
pageNew("example.com", "/path.html")
|
||||
p, _ := pageGet("example.com", "/path.html")
|
||||
p.IsLocked = true
|
||||
pageUpdate(p)
|
||||
|
||||
_, err := commentNew("temp-commenter-hex", "example.com", "/path.html", "root", "**foo**", "approved", time.Now().UTC())
|
||||
if err == nil {
|
||||
t.Errorf("expected error not found creating a new comment on a locked thread")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,13 @@ func configParse() error {
|
||||
"SMTP_PORT": "",
|
||||
"SMTP_FROM_ADDRESS": "",
|
||||
|
||||
"AKISMET_KEY": "",
|
||||
|
||||
"GOOGLE_KEY": "",
|
||||
"GOOGLE_SECRET": "",
|
||||
|
||||
"GITHUB_KEY": "",
|
||||
"GITHUB_SECRET": "",
|
||||
}
|
||||
|
||||
for key, value := range defaults {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
package main
|
||||
|
||||
var edition = "ce"
|
||||
var version = "v1.0.0"
|
||||
var version = "v1.5.0"
|
||||
|
||||
25
api/cron_domain_export_cleanup.go
Normal file
25
api/cron_domain_export_cleanup.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func domainExportCleanupBegin() error {
|
||||
go func() {
|
||||
for {
|
||||
statement := `
|
||||
DELETE FROM exports
|
||||
WHERE creationDate < $1;
|
||||
`
|
||||
_, err := db.Exec(statement, time.Now().UTC().AddDate(0, 0, -7))
|
||||
if err != nil {
|
||||
logger.Errorf("error cleaning up export rows: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Hour)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
25
api/cron_views_cleanup.go
Normal file
25
api/cron_views_cleanup.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func viewsCleanupBegin() error {
|
||||
go func() {
|
||||
for {
|
||||
statement := `
|
||||
DELETE FROM views
|
||||
WHERE viewDate < $1;
|
||||
`
|
||||
_, err := db.Exec(statement, time.Now().UTC().AddDate(0, 0, -45))
|
||||
if err != nil {
|
||||
logger.Errorf("error cleaning up views: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(24 * time.Hour)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -14,5 +14,6 @@ type domain struct {
|
||||
AutoSpamFilter bool `json:"autoSpamFilter"`
|
||||
RequireModeration bool `json:"requireModeration"`
|
||||
RequireIdentification bool `json:"requireIdentification"`
|
||||
ModerateAllAnonymous bool `json:"moderateAllAnonymous"`
|
||||
Moderators []moderator `json:"moderators"`
|
||||
}
|
||||
|
||||
@@ -60,6 +60,16 @@ func domainDelete(domain string) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
151
api/domain_export.go
Normal file
151
api/domain_export.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func domainExportBeginError(email string, toName string, domain string, err error) {
|
||||
// we're not using err at the moment because it's all errorInternal
|
||||
if err2 := smtpDomainExportError(email, toName, domain); err2 != nil {
|
||||
logger.Errorf("cannot send domain export error email for %s: %v", domain, err2)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func domainExportBegin(email string, toName string, domain string) {
|
||||
type dataExport struct {
|
||||
Version int `json:"version"`
|
||||
Comments []comment `json:"comments"`
|
||||
Commenters []commenter `json:"commenters"`
|
||||
}
|
||||
|
||||
e := dataExport{Version: 1, Comments: []comment{}, Commenters: []commenter{}}
|
||||
|
||||
statement := `
|
||||
SELECT commentHex, domain, path, commenterHex, markdown, parentHex, score, state, creationDate
|
||||
FROM comments
|
||||
WHERE domain = $1;
|
||||
`
|
||||
rows1, err := db.Query(statement, domain)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot select comments while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
defer rows1.Close()
|
||||
|
||||
for rows1.Next() {
|
||||
c := comment{}
|
||||
if err = rows1.Scan(&c.CommentHex, &c.Domain, &c.Path, &c.CommenterHex, &c.Markdown, &c.ParentHex, &c.Score, &c.State, &c.CreationDate); err != nil {
|
||||
logger.Errorf("cannot scan comment while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
e.Comments = append(e.Comments, c)
|
||||
}
|
||||
|
||||
statement = `
|
||||
SELECT commenters.commenterHex, commenters.email, commenters.name, commenters.link, commenters.photo, commenters.provider, commenters.joinDate
|
||||
FROM commenters, comments
|
||||
WHERE comments.domain = $1 AND commenters.commenterHex = comments.commenterHex;
|
||||
`
|
||||
rows2, err := db.Query(statement, domain)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot select commenters while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
defer rows2.Close()
|
||||
|
||||
for rows2.Next() {
|
||||
c := commenter{}
|
||||
if err := rows2.Scan(&c.CommenterHex, &c.Email, &c.Name, &c.Link, &c.Photo, &c.Provider, &c.JoinDate); err != nil {
|
||||
logger.Errorf("cannot scan commenter while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
e.Commenters = append(e.Commenters, c)
|
||||
}
|
||||
|
||||
je, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot marshall JSON while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
gje, err := gzipStatic(je)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot gzip JSON while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
exportHex, err := randomHex(32)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot generate exportHex while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
statement = `
|
||||
INSERT INTO
|
||||
exports (exportHex, binData, domain, creationDate)
|
||||
VALUES ($1, $2, $3 , $4 );
|
||||
`
|
||||
_, err = db.Exec(statement, exportHex, gje, domain, time.Now().UTC())
|
||||
if err != nil {
|
||||
logger.Errorf("error inserting expiry binary data while exporting %s: %v", domain, err)
|
||||
domainExportBeginError(email, toName, domain, errorInternal)
|
||||
return
|
||||
}
|
||||
|
||||
err = smtpDomainExport(email, toName, domain, exportHex)
|
||||
if err != nil {
|
||||
logger.Errorf("error sending data export email for %s: %v", domain, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func domainExportBeginHandler(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
|
||||
}
|
||||
|
||||
if !smtpConfigured {
|
||||
bodyMarshal(w, response{"success": false, "message": errorSmtpNotConfigured.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
o, err := ownerGetByOwnerToken(*x.OwnerToken)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
isOwner, err := domainOwnershipVerify(o.OwnerHex, *x.Domain)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !isOwner {
|
||||
bodyMarshal(w, response{"success": false, "message": errorNotAuthorised.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
go domainExportBegin(o.Email, o.Name, *x.Domain)
|
||||
|
||||
bodyMarshal(w, response{"success": true})
|
||||
}
|
||||
33
api/domain_export_download.go
Normal file
33
api/domain_export_download.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func domainExportDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
exportHex := r.FormValue("exportHex")
|
||||
if exportHex == "" {
|
||||
fmt.Fprintf(w, "Error: empty exportHex\n")
|
||||
return
|
||||
}
|
||||
|
||||
statement := `
|
||||
SELECT domain, binData, creationDate
|
||||
FROM exports
|
||||
WHERE exportHex = $1;
|
||||
`
|
||||
row := db.QueryRow(statement, exportHex)
|
||||
|
||||
var domain string
|
||||
var binData []byte
|
||||
var creationDate time.Time
|
||||
if err := row.Scan(&domain, &binData, &creationDate); err != nil {
|
||||
fmt.Fprintf(w, "Error: that exportHex does not exist\n")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s-%v.gz"`, domain, creationDate.Unix()))
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Write(binData)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ func domainGet(dmn string) (domain, error) {
|
||||
}
|
||||
|
||||
statement := `
|
||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification
|
||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous
|
||||
FROM domains
|
||||
WHERE domain = $1;
|
||||
`
|
||||
@@ -16,7 +16,7 @@ func domainGet(dmn string) (domain, error) {
|
||||
|
||||
var err error
|
||||
d := domain{}
|
||||
if err = row.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification); 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); err != nil {
|
||||
return d, errorNoSuchDomain
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ type disqusThread struct {
|
||||
|
||||
type disqusAuthor struct {
|
||||
XMLName xml.Name `xml:"author"`
|
||||
IsAnonymous bool `xml:"isAnonymous"`
|
||||
Name string `xml:"name"`
|
||||
Email string `xml:"email"`
|
||||
IsAnonymous bool `xml:"isAnonymous"`
|
||||
Username string `xml:"username"`
|
||||
}
|
||||
|
||||
type disqusThreadId struct {
|
||||
@@ -43,7 +43,6 @@ type disqusPost struct {
|
||||
Id string `xml:"http://disqus.com/disqus-internals id,attr"`
|
||||
ThreadId disqusThreadId `xml:"thread"`
|
||||
ParentId disqusParentId `xml:"parent"`
|
||||
PostId disqusPostId `xml:"post"`
|
||||
Message string `xml:"message"`
|
||||
CreationDate time.Time `xml:"createdAt"`
|
||||
IsDeleted bool `xml:"isDeleted"`
|
||||
@@ -98,24 +97,26 @@ func domainImportDisqus(domain string, url string) (int, error) {
|
||||
|
||||
// Map Disqus emails to commenterHex (if not available, create a new one
|
||||
// with a random password that can be reset later).
|
||||
commenterHex := make(map[string]string)
|
||||
commenterHex := map[string]string{}
|
||||
for _, post := range x.Posts {
|
||||
if post.IsDeleted || post.IsSpam {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := commenterHex[post.Author.Email]; ok {
|
||||
email := post.Author.Username + "@disqus.com"
|
||||
|
||||
if _, ok := commenterHex[email]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := commenterGetByEmail("commento", post.Author.Email)
|
||||
c, err := commenterGetByEmail("commento", email)
|
||||
if err != nil && err != errorNoSuchCommenter {
|
||||
logger.Errorf("cannot get commenter by email: %v", err)
|
||||
return 0, errorInternal
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
commenterHex[post.Author.Email] = c.CommenterHex
|
||||
commenterHex[email] = c.CommenterHex
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
|
||||
return 0, errorInternal
|
||||
}
|
||||
|
||||
commenterHex[post.Author.Email], err = commenterNew(post.Author.Email, post.Author.Name, "undefined", "undefined", "commento", randomPassword)
|
||||
commenterHex[email], err = commenterNew(email, post.Author.Name, "undefined", "undefined", "commento", randomPassword)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -134,12 +135,17 @@ func domainImportDisqus(domain string, url string) (int, error) {
|
||||
// For each Disqus post, create a Commento comment. Attempt to convert the
|
||||
// HTML to markdown.
|
||||
numImported := 0
|
||||
disqusIdMap := make(map[string]string)
|
||||
disqusIdMap := map[string]string{}
|
||||
for _, post := range x.Posts {
|
||||
if post.IsDeleted || post.IsSpam {
|
||||
continue
|
||||
}
|
||||
|
||||
cHex := "anonymous"
|
||||
if !post.Author.IsAnonymous {
|
||||
cHex = commenterHex[post.Author.Username+"@disqus.com"]
|
||||
}
|
||||
|
||||
parentHex := "root"
|
||||
if val, ok := disqusIdMap[post.ParentId.Id]; ok {
|
||||
parentHex = val
|
||||
@@ -148,7 +154,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
|
||||
// TODO: restrict the list of tags to just the basics: <a>, <b>, <i>, <code>
|
||||
// Especially remove <img> (convert it to <a>).
|
||||
commentHex, err := commentNew(
|
||||
commenterHex[post.Author.Email],
|
||||
cHex,
|
||||
domain,
|
||||
pathStrip(threads[post.ThreadId.Id].URL),
|
||||
parentHex,
|
||||
@@ -159,7 +165,7 @@ func domainImportDisqus(domain string, url string) (int, error) {
|
||||
return numImported, err
|
||||
}
|
||||
|
||||
disqusIdMap[post.PostId.Id] = commentHex
|
||||
disqusIdMap[post.Id] = commentHex
|
||||
numImported += 1
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ func domainList(ownerHex string) ([]domain, error) {
|
||||
}
|
||||
|
||||
statement := `
|
||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification
|
||||
SELECT domain, ownerHex, name, creationDate, state, importedComments, autoSpamFilter, requireModeration, requireIdentification, moderateAllAnonymous
|
||||
FROM domains
|
||||
WHERE ownerHex=$1;
|
||||
`
|
||||
@@ -24,7 +24,7 @@ func domainList(ownerHex string) ([]domain, error) {
|
||||
domains := []domain{}
|
||||
for rows.Next() {
|
||||
d := domain{}
|
||||
if err = rows.Scan(&d.Domain, &d.OwnerHex, &d.Name, &d.CreationDate, &d.State, &d.ImportedComments, &d.AutoSpamFilter, &d.RequireModeration, &d.RequireIdentification); 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); err != nil {
|
||||
logger.Errorf("cannot Scan domain: %v", err)
|
||||
return nil, errorInternal
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
func domainUpdate(d domain) error {
|
||||
statement := `
|
||||
UPDATE domains
|
||||
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6
|
||||
SET name=$2, state=$3, autoSpamFilter=$4, requireModeration=$5, requireIdentification=$6, moderateAllAnonymous=$7
|
||||
WHERE domain=$1;
|
||||
`
|
||||
|
||||
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification)
|
||||
_, err := db.Exec(statement, d.Domain, d.Name, d.State, d.AutoSpamFilter, d.RequireModeration, d.RequireIdentification, d.ModerateAllAnonymous)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot update non-moderators: %v", err)
|
||||
return errorInternal
|
||||
|
||||
@@ -41,3 +41,4 @@ var errorSelfVote = errors.New("You cannot vote on your own comment.")
|
||||
var errorInvalidConfigFile = errors.New("Invalid config file.")
|
||||
var errorInvalidConfigValue = errors.New("Invalid config value.")
|
||||
var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.")
|
||||
var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.")
|
||||
|
||||
@@ -11,6 +11,8 @@ func main() {
|
||||
exitIfError(markdownRendererCreate())
|
||||
exitIfError(sigintCleanupSetup())
|
||||
exitIfError(versionCheckStart())
|
||||
exitIfError(domainExportCleanupBegin())
|
||||
exitIfError(viewsCleanupBegin())
|
||||
|
||||
exitIfError(routesServe())
|
||||
}
|
||||
|
||||
@@ -11,5 +11,9 @@ func oauthConfigure() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := githubOauthConfigure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
43
api/oauth_github.go
Normal file
43
api/oauth_github.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
"os"
|
||||
)
|
||||
|
||||
var githubConfig *oauth2.Config
|
||||
|
||||
func githubOauthConfigure() error {
|
||||
githubConfig = nil
|
||||
if os.Getenv("GITHUB_KEY") == "" && os.Getenv("GITHUB_SECRET") == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.Getenv("GITHUB_KEY") == "" {
|
||||
logger.Errorf("COMMENTO_GITHUB_KEY not configured, but COMMENTO_GITHUB_SECRET is set")
|
||||
return errorOauthMisconfigured
|
||||
}
|
||||
|
||||
if os.Getenv("GITHUB_SECRET") == "" {
|
||||
logger.Errorf("COMMENTO_GITHUB_SECRET not configured, but COMMENTO_GITHUB_KEY is set")
|
||||
return errorOauthMisconfigured
|
||||
}
|
||||
|
||||
logger.Infof("loading github OAuth config")
|
||||
|
||||
githubConfig = &oauth2.Config{
|
||||
RedirectURL: os.Getenv("ORIGIN") + "/api/oauth/github/callback",
|
||||
ClientID: os.Getenv("GITHUB_KEY"),
|
||||
ClientSecret: os.Getenv("GITHUB_SECRET"),
|
||||
Scopes: []string{
|
||||
"read:user",
|
||||
"user:email",
|
||||
},
|
||||
Endpoint: github.Endpoint,
|
||||
}
|
||||
|
||||
configuredOauths = append(configuredOauths, "github")
|
||||
|
||||
return nil
|
||||
}
|
||||
115
api/oauth_github_callback.go
Normal file
115
api/oauth_github_callback.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/oauth2"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func githubGetPrimaryEmail(accessToken string) (string, error) {
|
||||
resp, err := http.Get("https://api.github.com/user/emails?access_token=" + accessToken)
|
||||
defer resp.Body.Close()
|
||||
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", errorCannotReadResponse
|
||||
}
|
||||
|
||||
user := []map[string]interface{}{}
|
||||
if err := json.Unmarshal(contents, &user); err != nil {
|
||||
logger.Errorf("error unmarshaling github user: %v", err)
|
||||
return "", errorInternal
|
||||
}
|
||||
|
||||
nonPrimaryEmail := ""
|
||||
for _, email := range user {
|
||||
nonPrimaryEmail = email["email"].(string)
|
||||
if email["primary"].(bool) {
|
||||
return email["email"].(string), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nonPrimaryEmail, nil
|
||||
}
|
||||
|
||||
func githubCallbackHandler(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 := githubConfig.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
email, err := githubGetPrimaryEmail(token.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get("https://api.github.com/user?access_token=" + token.AccessToken)
|
||||
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 email == "" {
|
||||
if user["email"] == nil {
|
||||
fmt.Fprintf(w, "Error: no email address returned by Github")
|
||||
return
|
||||
}
|
||||
|
||||
email = user["email"].(string)
|
||||
}
|
||||
|
||||
c, err := commenterGetByEmail("github", 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 {
|
||||
var link string
|
||||
if val, ok := user["html_url"]; ok {
|
||||
link = val.(string)
|
||||
} else {
|
||||
link = "undefined"
|
||||
}
|
||||
|
||||
commenterHex, err = commenterNew(email, user["name"].(string), link, user["avatar_url"].(string), "github", "")
|
||||
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_github_redirect.go
Normal file
25
api/oauth_github_redirect.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func githubRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if githubConfig == nil {
|
||||
logger.Errorf("github oauth access attempt without configuration")
|
||||
fmt.Fprintf(w, "error: this website has not configured github OAuth")
|
||||
return
|
||||
}
|
||||
|
||||
commenterToken := r.FormValue("commenterToken")
|
||||
|
||||
_, err := commenterGetByCommenterToken(commenterToken)
|
||||
if err != nil && err != errorNoSuchToken {
|
||||
fmt.Fprintf(w, "error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
url := githubConfig.AuthCodeURL(commenterToken)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
@@ -39,7 +39,14 @@ func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := commenterGetByEmail("google", user["email"].(string))
|
||||
if user["email"] == nil {
|
||||
fmt.Fprintf(w, "Error: no email address returned by Github")
|
||||
return
|
||||
}
|
||||
|
||||
email := user["email"].(string)
|
||||
|
||||
c, err := commenterGetByEmail("google", email)
|
||||
if err != nil && err != errorNoSuchCommenter {
|
||||
fmt.Fprintf(w, "Error: %s", err.Error())
|
||||
return
|
||||
@@ -49,14 +56,6 @@ func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// TODO: in case of returning users, update the information we have on record?
|
||||
if err == errorNoSuchCommenter {
|
||||
var email string
|
||||
if _, ok := user["email"]; ok {
|
||||
email = user["email"].(string)
|
||||
} else {
|
||||
fmt.Fprintf(w, "Error: %s", errorInvalidEmail.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var link string
|
||||
if val, ok := user["link"]; ok {
|
||||
link = val.(string)
|
||||
|
||||
@@ -31,8 +31,8 @@ func ownerGetByOwnerToken(ownerToken string) (owner, error) {
|
||||
statement := `
|
||||
SELECT ownerHex, email, name, confirmedEmail, joinDate
|
||||
FROM owners
|
||||
WHERE email IN (
|
||||
SELECT email FROM ownerSessions
|
||||
WHERE ownerHex IN (
|
||||
SELECT ownerHex FROM ownerSessions
|
||||
WHERE ownerToken = $1
|
||||
);
|
||||
`
|
||||
|
||||
@@ -16,6 +16,10 @@ func ownerNew(email string, name string, password string) (string, error) {
|
||||
return "", errorNewOwnerForbidden
|
||||
}
|
||||
|
||||
if _, err := ownerGetByEmail(email); err == nil {
|
||||
return "", errorEmailAlreadyExists
|
||||
}
|
||||
|
||||
ownerHex, err := randomHex(32)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot generate ownerHex: %v", err)
|
||||
|
||||
11
api/page.go
Normal file
11
api/page.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import ()
|
||||
|
||||
type page struct {
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
IsLocked bool `json:"isLocked"`
|
||||
CommentCount int `json:"commentCount"`
|
||||
StickyCommentHex string `json:"stickyCommentHex"`
|
||||
}
|
||||
36
api/page_get.go
Normal file
36
api/page_get.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func pageGet(domain string, path string) (page, error) {
|
||||
// path can be empty
|
||||
if domain == "" {
|
||||
return page{}, errorMissingField
|
||||
}
|
||||
|
||||
statement := `
|
||||
SELECT isLocked, commentCount, stickyCommentHex
|
||||
FROM pages
|
||||
WHERE domain=$1 AND path=$2;
|
||||
`
|
||||
row := db.QueryRow(statement, domain, path)
|
||||
|
||||
p := page{Domain: domain, Path: path}
|
||||
if err := row.Scan(&p.IsLocked, &p.CommentCount, &p.StickyCommentHex); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// If there haven't been any comments, there won't be a record for this
|
||||
// page. The sane thing to do is return defaults.
|
||||
// TODO: the defaults are hard-coded in two places: here and the schema
|
||||
p.IsLocked = false
|
||||
p.CommentCount = 0
|
||||
p.StickyCommentHex = "none"
|
||||
} else {
|
||||
logger.Errorf("error scanning page: %v", err)
|
||||
return page{}, errorInternal
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
43
api/page_get_test.go
Normal file
43
api/page_get_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPageGetBasics(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
pageNew("example.com", "/path.html")
|
||||
|
||||
p, err := pageGet("example.com", "/path.html")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error getting page: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if p.IsLocked != false {
|
||||
t.Errorf("expected p.IsLocked=false got %v", p.IsLocked)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := pageGet("example.com", "/path2.html"); err != nil {
|
||||
t.Errorf("unexpected error getting page with non-existant record: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageGetEmpty(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
pageNew("example.com", "")
|
||||
|
||||
if _, err := pageGet("example.com", ""); err != nil {
|
||||
t.Errorf("unexpected error getting page with empty path: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := pageGet("", "/path.html"); err == nil {
|
||||
t.Errorf("exepected error not found when getting page with empty domain")
|
||||
return
|
||||
}
|
||||
}
|
||||
24
api/page_new.go
Normal file
24
api/page_new.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import ()
|
||||
|
||||
func pageNew(domain string, path string) error {
|
||||
// path can be empty
|
||||
if domain == "" {
|
||||
return errorMissingField
|
||||
}
|
||||
|
||||
statement := `
|
||||
INSERT INTO
|
||||
pages (domain, path)
|
||||
VALUES ($1, $2 )
|
||||
ON CONFLICT DO NOTHING;
|
||||
`
|
||||
_, err := db.Exec(statement, domain, path)
|
||||
if err != nil {
|
||||
logger.Errorf("error inserting new page: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
43
api/page_new_test.go
Normal file
43
api/page_new_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPageNewBasics(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
if err := pageNew("example.com", "/path.html"); err != nil {
|
||||
t.Errorf("unexpected error creating page: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageNewEmpty(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
if err := pageNew("example.com", ""); err != nil {
|
||||
t.Errorf("unexpected error creating page with empty path: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := pageNew("", "/path.html"); err == nil {
|
||||
t.Errorf("expected error not found creating page with empty domain")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageNewUnique(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
if err := pageNew("example.com", "/path.html"); err != nil {
|
||||
t.Errorf("unexpected error creating page: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// no error should be returned when trying to duplicate insert
|
||||
if err := pageNew("example.com", "/path.html"); err != nil {
|
||||
t.Errorf("unexpected error creating same page twice: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
72
api/page_update.go
Normal file
72
api/page_update.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func pageUpdate(p page) error {
|
||||
if p.Domain == "" {
|
||||
return errorMissingField
|
||||
}
|
||||
|
||||
// fields to not update:
|
||||
// commentCount
|
||||
statement := `
|
||||
INSERT INTO
|
||||
pages (domain, path, isLocked, stickyCommentHex)
|
||||
VALUES ($1, $2, $3, $4 )
|
||||
ON CONFLICT (domain, path) DO
|
||||
UPDATE SET isLocked = $3, stickyCommentHex = $4;
|
||||
`
|
||||
_, err := db.Exec(statement, p.Domain, p.Path, p.IsLocked, p.StickyCommentHex)
|
||||
if err != nil {
|
||||
logger.Errorf("error setting page attributes: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pageUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
CommenterToken *string `json:"commenterToken"`
|
||||
Domain *string `json:"domain"`
|
||||
Path *string `json:"path"`
|
||||
Attributes *page `json:"attributes"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c, err := commenterGetByCommenterToken(*x.CommenterToken)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
domain := domainStrip(*x.Domain)
|
||||
|
||||
isModerator, err := isDomainModerator(domain, c.Email)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !isModerator {
|
||||
bodyMarshal(w, response{"success": false, "message": errorNotModerator.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
(*x.Attributes).Domain = *x.Domain
|
||||
(*x.Attributes).Path = *x.Path
|
||||
|
||||
if err = pageUpdate(*x.Attributes); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true})
|
||||
}
|
||||
43
api/page_update_test.go
Normal file
43
api/page_update_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPageUpdateBasics(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
commenterHex, _ := commenterNew("test@example.com", "Test", "undefined", "https://example.com/photo.jpg", "google", "")
|
||||
|
||||
commentNew(commenterHex, "example.com", "/path.html", "root", "**foo**", "unapproved", time.Now().UTC())
|
||||
|
||||
p, _ := pageGet("example.com", "/path.html")
|
||||
if p.IsLocked != false {
|
||||
t.Errorf("expected IsLocked=false got %v", p.IsLocked)
|
||||
return
|
||||
}
|
||||
|
||||
p.IsLocked = true
|
||||
|
||||
if err := pageUpdate(p); err != nil {
|
||||
t.Errorf("unexpected error updating page: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p, _ = pageGet("example.com", "/path.html")
|
||||
if p.IsLocked != true {
|
||||
t.Errorf("expected IsLocked=true got %v", p.IsLocked)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageUpdateEmpty(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
p := page{Domain: "", Path: "", IsLocked: false}
|
||||
if err := pageUpdate(p); err == nil {
|
||||
t.Errorf("expected error not found updating page with empty everything")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ func apiRouterInit(router *mux.Router) error {
|
||||
router.HandleFunc("/api/domain/moderator/delete", domainModeratorDeleteHandler).Methods("POST")
|
||||
router.HandleFunc("/api/domain/statistics", domainStatisticsHandler).Methods("POST")
|
||||
router.HandleFunc("/api/domain/import/disqus", domainImportDisqusHandler).Methods("POST")
|
||||
router.HandleFunc("/api/domain/export/begin", domainExportBeginHandler).Methods("POST")
|
||||
router.HandleFunc("/api/domain/export/download", domainExportDownloadHandler).Methods("GET")
|
||||
|
||||
router.HandleFunc("/api/commenter/token/new", commenterTokenNewHandler).Methods("GET")
|
||||
router.HandleFunc("/api/commenter/new", commenterNewHandler).Methods("POST")
|
||||
@@ -29,11 +31,17 @@ func apiRouterInit(router *mux.Router) error {
|
||||
router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET")
|
||||
router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET")
|
||||
|
||||
router.HandleFunc("/api/oauth/github/redirect", githubRedirectHandler).Methods("GET")
|
||||
router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).Methods("GET")
|
||||
|
||||
router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST")
|
||||
router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST")
|
||||
router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST")
|
||||
router.HandleFunc("/api/comment/vote", commentVoteHandler).Methods("POST")
|
||||
router.HandleFunc("/api/comment/approve", commentApproveHandler).Methods("POST")
|
||||
router.HandleFunc("/api/comment/delete", commentDeleteHandler).Methods("POST")
|
||||
|
||||
router.HandleFunc("/api/page/update", pageUpdateHandler).Methods("POST")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,39 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func redirectLogin(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, os.Getenv("ORIGIN")+"/login", 301)
|
||||
}
|
||||
|
||||
type staticAssetPlugs struct {
|
||||
Origin string
|
||||
}
|
||||
|
||||
type staticHtmlPlugs struct {
|
||||
type staticPlugs struct {
|
||||
Origin string
|
||||
CdnPrefix string
|
||||
Footer template.HTML
|
||||
Footer string
|
||||
}
|
||||
|
||||
var asset map[string][]byte = make(map[string][]byte)
|
||||
var contentType map[string]string = make(map[string]string)
|
||||
var footer string
|
||||
var compress bool
|
||||
|
||||
func fileDetemplate(f string) ([]byte, error) {
|
||||
contents, err := ioutil.ReadFile(f)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read file %s: %v", f, err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
x := string(contents)
|
||||
x = strings.Replace(x, "[[[.Origin]]]", os.Getenv("ORIGIN"), -1)
|
||||
x = strings.Replace(x, "[[[.CdnPrefix]]]", os.Getenv("CDN_PREFIX"), -1)
|
||||
x = strings.Replace(x, "[[[.Footer]]]", footer, -1)
|
||||
|
||||
return []byte(x), nil
|
||||
}
|
||||
|
||||
func footerInit() error {
|
||||
contents, err := fileDetemplate(os.Getenv("STATIC") + "/footer.html")
|
||||
if err != nil {
|
||||
logger.Errorf("cannot init footer: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
footer = string(contents)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileLoad(f string) ([]byte, error) {
|
||||
b, err := fileDetemplate(f)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot load file %s: %v", f, err)
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
if !compress {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return gzipStatic(b)
|
||||
}
|
||||
|
||||
func staticRouterInit(router *mux.Router) error {
|
||||
asset := make(map[string][]byte)
|
||||
gzippedAsset := make(map[string][]byte)
|
||||
var err error
|
||||
|
||||
for _, dir := range []string{"js", "css", "images"} {
|
||||
sl := string(os.PathSeparator)
|
||||
dir = sl + dir
|
||||
subdir := pathStrip(os.Getenv("ORIGIN"))
|
||||
|
||||
if err = footerInit(); err != nil {
|
||||
logger.Errorf("error initialising static router: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dir := range []string{"/js", "/css", "/images"} {
|
||||
files, err := ioutil.ReadDir(os.Getenv("STATIC") + dir)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read directory %s%s: %v", os.Getenv("STATIC"), dir, err)
|
||||
@@ -41,98 +83,47 @@ func staticRouterInit(router *mux.Router) error {
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
p := dir + sl + file.Name()
|
||||
|
||||
contents, err := ioutil.ReadFile(os.Getenv("STATIC") + p)
|
||||
f := dir + "/" + file.Name()
|
||||
asset[subdir+f], err = fileLoad(os.Getenv("STATIC") + f)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read file %s%s: %v", os.Getenv("STATIC"), p, err)
|
||||
logger.Errorf("cannot detemplate %s%s: %v", os.Getenv("STATIC"), f, err)
|
||||
return err
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if dir == "/js" {
|
||||
prefix = "window.commentoOrigin='" + os.Getenv("ORIGIN") + "';\n"
|
||||
prefix += "window.commentoCdn='" + os.Getenv("CDN_PREFIX") + "';\n"
|
||||
}
|
||||
|
||||
gzip := (os.Getenv("GZIP_STATIC") == "true")
|
||||
|
||||
subdir := pathStrip(os.Getenv("ORIGIN"))
|
||||
|
||||
asset[subdir+p] = []byte(prefix + string(contents))
|
||||
if gzip {
|
||||
gzippedAsset[subdir+p], err = gzipStatic(asset[subdir+p])
|
||||
if err != nil {
|
||||
logger.Errorf("error gzipping %s: %v", p, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// faster than checking inside the handler
|
||||
if !gzip {
|
||||
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
|
||||
w.Write(asset[r.URL.Path])
|
||||
})
|
||||
} else {
|
||||
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Write(gzippedAsset[r.URL.Path])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer, err := ioutil.ReadFile(os.Getenv("STATIC") + string(os.PathSeparator) + "footer.html")
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read file footer.html: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
pages := []string{
|
||||
"login",
|
||||
"forgot",
|
||||
"reset-password",
|
||||
"signup",
|
||||
"confirm-email",
|
||||
"dashboard",
|
||||
"logout",
|
||||
}
|
||||
|
||||
html := make(map[string]string)
|
||||
for _, page := range pages {
|
||||
sl := string(os.PathSeparator)
|
||||
page = sl + page
|
||||
file := page + ".html"
|
||||
|
||||
contents, err := ioutil.ReadFile(os.Getenv("STATIC") + file)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot read file %s%s: %v", os.Getenv("STATIC"), file, err)
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := template.New(page).Delims("[[[", "]]]").Parse(string(contents))
|
||||
if err != nil {
|
||||
logger.Errorf("cannot parse %s%s template: %v", os.Getenv("STATIC"), file, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
t.Execute(&buf, &staticHtmlPlugs{
|
||||
Origin: os.Getenv("ORIGIN"),
|
||||
CdnPrefix: os.Getenv("CDN_PREFIX"),
|
||||
Footer: template.HTML(string(footer)),
|
||||
})
|
||||
|
||||
subdir := pathStrip(os.Getenv("ORIGIN"))
|
||||
|
||||
html[subdir+page] = buf.String()
|
||||
"/login",
|
||||
"/forgot",
|
||||
"/reset-password",
|
||||
"/signup",
|
||||
"/confirm-email",
|
||||
"/dashboard",
|
||||
"/logout",
|
||||
}
|
||||
|
||||
for _, page := range pages {
|
||||
router.HandleFunc("/"+page, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, html[r.URL.Path])
|
||||
f := page + ".html"
|
||||
asset[subdir+page], err = fileLoad(os.Getenv("STATIC") + f)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot detemplate %s%s: %v", os.Getenv("STATIC"), f, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for p, _ := range asset {
|
||||
if path.Ext(p) != "" {
|
||||
contentType[p] = mime.TypeByExtension(path.Ext(p))
|
||||
} else {
|
||||
contentType[p] = mime.TypeByExtension("html")
|
||||
}
|
||||
|
||||
router.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", contentType[r.URL.Path])
|
||||
if compress {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
w.Write(asset[r.URL.Path])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
29
api/smtp_domain_export.go
Normal file
29
api/smtp_domain_export.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
"os"
|
||||
)
|
||||
|
||||
type domainExportPlugs struct {
|
||||
Origin string
|
||||
Domain string
|
||||
ExportHex string
|
||||
}
|
||||
|
||||
func smtpDomainExport(to string, toName string, domain string, exportHex string) error {
|
||||
var header bytes.Buffer
|
||||
headerTemplate.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "Commento Data Export"})
|
||||
|
||||
var body bytes.Buffer
|
||||
templates["domain-export"].Execute(&body, &domainExportPlugs{Origin: os.Getenv("ORIGIN"), ExportHex: exportHex})
|
||||
|
||||
err := smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
|
||||
if err != nil {
|
||||
logger.Errorf("cannot send data export email: %v", err)
|
||||
return errorCannotSendEmail
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
28
api/smtp_domain_export_error.go
Normal file
28
api/smtp_domain_export_error.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
"os"
|
||||
)
|
||||
|
||||
type domainExportErrorPlugs struct {
|
||||
Origin string
|
||||
Domain string
|
||||
}
|
||||
|
||||
func smtpDomainExportError(to string, toName string, domain string) error {
|
||||
var header bytes.Buffer
|
||||
headerTemplate.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "Commento Data Export"})
|
||||
|
||||
var body bytes.Buffer
|
||||
templates["data-export-error"].Execute(&body, &domainExportPlugs{Origin: os.Getenv("ORIGIN")})
|
||||
|
||||
err := smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
|
||||
if err != nil {
|
||||
logger.Errorf("cannot send data export error email: %v", err)
|
||||
return errorCannotSendEmail
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var headerTemplate *template.Template
|
||||
@@ -20,9 +20,9 @@ var templates map[string]*template.Template
|
||||
func smtpTemplatesLoad() error {
|
||||
var err error
|
||||
headerTemplate, err = template.New("header").Parse(`MIME-Version: 1.0
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
From: {{.FromAddress}}
|
||||
From: Commento <{{.FromAddress}}>
|
||||
To: {{.ToName}} <{{.ToAddress}}>
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Subject: {{.Subject}}
|
||||
|
||||
`)
|
||||
@@ -31,7 +31,7 @@ Subject: {{.Subject}}
|
||||
return errorMalformedTemplate
|
||||
}
|
||||
|
||||
names := []string{"confirm-hex", "reset-hex"}
|
||||
names := []string{"confirm-hex", "reset-hex", "domain-export", "domain-export-error"}
|
||||
|
||||
templates = make(map[string]*template.Template)
|
||||
|
||||
@@ -39,9 +39,9 @@ Subject: {{.Subject}}
|
||||
for _, name := range names {
|
||||
var err error
|
||||
templates[name] = template.New(name)
|
||||
templates[name], err = template.ParseFiles(fmt.Sprintf("%s/templates/%s.html", os.Getenv("STATIC"), name))
|
||||
templates[name], err = template.ParseFiles(fmt.Sprintf("%s/templates/%s.txt", os.Getenv("STATIC"), name))
|
||||
if err != nil {
|
||||
logger.Errorf("cannot parse %s/templates/%s.html: %v", os.Getenv("STATIC"), name, err)
|
||||
logger.Errorf("cannot parse %s/templates/%s.txt: %v", os.Getenv("STATIC"), name, err)
|
||||
return errorMalformedTemplate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,3 +43,16 @@ func bodyMarshal(w http.ResponseWriter, x map[string]interface{}) error {
|
||||
w.Write(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIp(r *http.Request) string {
|
||||
ip := r.RemoteAddr
|
||||
if r.Header.Get("X-Forwarded-For") != "" {
|
||||
ip = r.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func getUserAgent(r *http.Request) string {
|
||||
return r.Header.Get("User-Agent")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -14,13 +13,12 @@ func versionCheckStart() error {
|
||||
go func() {
|
||||
printedError := false
|
||||
errorCount := 0
|
||||
latestSeen := ""
|
||||
|
||||
for {
|
||||
time.Sleep(5 * time.Minute)
|
||||
|
||||
data := url.Values{
|
||||
"origin": {os.Getenv("ORIGIN")},
|
||||
"edition": {edition},
|
||||
"version": {version},
|
||||
}
|
||||
|
||||
@@ -65,8 +63,9 @@ func versionCheckStart() error {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.NewUpdate {
|
||||
if r.NewUpdate && r.Latest != latestSeen {
|
||||
logger.Infof("New update available! Latest version: %s", r.Latest)
|
||||
latestSeen = r.Latest
|
||||
}
|
||||
|
||||
errorCount = 0
|
||||
|
||||
9
db/20180922181651-page-attributes.sql
Normal file
9
db/20180922181651-page-attributes.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Introduces page attributes
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
domain TEXT NOT NULL ,
|
||||
path TEXT NOT NULL ,
|
||||
isLocked BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX pagesUniqueIndex ON pages(domain, path);
|
||||
15
db/20180923002745-comment-count.sql
Normal file
15
db/20180923002745-comment-count.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE pages
|
||||
ADD commentCount INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE OR REPLACE FUNCTION commentsInsertTriggerFunction() RETURNS TRIGGER AS $trigger$
|
||||
BEGIN
|
||||
UPDATE pages
|
||||
SET commentCount = commentCount + 1
|
||||
WHERE domain = new.domain AND path = new.path;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$trigger$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER commentsInsertTrigger AFTER INSERT ON comments
|
||||
FOR EACH ROW EXECUTE PROCEDURE commentsInsertTriggerFunction();
|
||||
10
db/20180923004309-comment-count-build.sql
Normal file
10
db/20180923004309-comment-count-build.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Build the comments count column
|
||||
|
||||
UPDATE pages
|
||||
SET commentCount = subquery.commentCount
|
||||
FROM (
|
||||
SELECT COUNT(commentHex) as commentCount
|
||||
FROM comments
|
||||
WHERE state = 'approved'
|
||||
GROUP BY (domain, path)
|
||||
) as subquery;
|
||||
7
db/20181007230906-store-version.sql
Normal file
7
db/20181007230906-store-version.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
version TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
config (version)
|
||||
VALUES ('v1.1.3');
|
||||
2
db/20181007231407-v1.1.4.sql
Normal file
2
db/20181007231407-v1.1.4.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATE config
|
||||
SET version = 'v1.2.0';
|
||||
2
db/20181218183803-sticky-comments.sql
Normal file
2
db/20181218183803-sticky-comments.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE pages
|
||||
ADD stickyCommentHex TEXT NOT NULL DEFAULT 'none';
|
||||
2
db/20181228114101-v1.4.0.sql
Normal file
2
db/20181228114101-v1.4.0.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATE config
|
||||
SET version = 'v1.4.0';
|
||||
2
db/20181228114101-v1.4.1.sql
Normal file
2
db/20181228114101-v1.4.1.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATE config
|
||||
SET version = 'v1.4.1';
|
||||
4
db/20190122235525-anonymous-moderation-default.sql
Normal file
4
db/20190122235525-anonymous-moderation-default.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Allow the owner to change whether anonymous comments are put into moderation by default.
|
||||
|
||||
ALTER TABLE domains
|
||||
ADD COLUMN moderateAllAnonymous BOOLEAN DEFAULT true;
|
||||
2
db/20190123002724-v1.4.2.sql
Normal file
2
db/20190123002724-v1.4.2.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATE config
|
||||
SET version = 'v1.4.2';
|
||||
8
db/20190131002240-export.sql
Normal file
8
db/20190131002240-export.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- add export feature
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exports (
|
||||
exportHex TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
binData BYTEA NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
creationDate TIMESTAMP NOT NULL
|
||||
);
|
||||
2
db/20190204180609-v1.5.0.sql
Normal file
2
db/20190204180609-v1.5.0.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATE config
|
||||
SET version = 'v1.5.0';
|
||||
@@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
server:
|
||||
image: registry.gitlab.com/commento/commento-ce
|
||||
image: registry.gitlab.com/commento/commento
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
networks:
|
||||
- db_network
|
||||
volumes:
|
||||
- postgres_data_volume:/var/lib/postgres
|
||||
- postgres_data_volume:/var/lib/postgresql/data
|
||||
|
||||
networks:
|
||||
db_network:
|
||||
|
||||
28
etc/bsd-rc/commento
Executable file
28
etc/bsd-rc/commento
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: commento
|
||||
# REQUIRE: LOGIN postgresql
|
||||
# KEYWORD: shutdown
|
||||
|
||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
desc="Commento daemon"
|
||||
name=commento
|
||||
rcvar=commento_enable
|
||||
|
||||
load_rc_config $name
|
||||
|
||||
: ${commento_enable:=NO}
|
||||
|
||||
commento_env="COMMENTO_ORIGIN=https://commento.example.com \
|
||||
COMMENTO_PORT=8080 \
|
||||
COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable \
|
||||
COMMENTO_STATIC=/usr/local/share/commento"
|
||||
commento_user=www
|
||||
|
||||
command="/usr/local/bin/commento"
|
||||
command_args=" >> /var/log/commento/${name}.log 2>&1 &"
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# PROVIDE: commento_ce
|
||||
# REQUIRE: LOGIN postgresql
|
||||
# KEYWORD: shutdown
|
||||
|
||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
desc="Commento CE daemon"
|
||||
name=commento_ce
|
||||
rcvar=commento_ce_enable
|
||||
|
||||
load_rc_config $name
|
||||
|
||||
: ${commento_ce_enable:=NO}
|
||||
|
||||
commento_ce_env="COMMENTO_ORIGIN=https://commento.example.com \
|
||||
COMMENTO_PORT=8080 \
|
||||
COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable \
|
||||
COMMENTO_STATIC=/usr/local/share/commento-ce"
|
||||
commento_ce_user=www
|
||||
|
||||
command="/usr/local/bin/commento-ce"
|
||||
command_args=" >> /var/log/commento_ce/${name}.log 2>&1 &"
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -1,14 +1,14 @@
|
||||
[Unit]
|
||||
Description=Commento-CE daemon service
|
||||
Description=Commento daemon service
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/commento-ce
|
||||
ExecStart=/usr/bin/commento
|
||||
Environment=COMMENTO_ORIGIN=https://commento.example.com
|
||||
Environment=COMMENTO_PORT=8080
|
||||
Environment=COMMENTO_POSTGRES=postgres://commento:commento@db:5432/commento?sslmode=disable
|
||||
Environment=COMMENTO_STATIC=/usr/share/commento-ce
|
||||
Environment=COMMENTO_STATIC=/usr/share/commento
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
33
frontend/.eslintrc
Normal file
33
frontend/.eslintrc
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"$": true
|
||||
},
|
||||
"rules": {
|
||||
"no-bitwise": 2,
|
||||
"camelcase": 2,
|
||||
"brace-style": ["error", "1tbs"],
|
||||
"curly": ["error", "all"],
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"indent": ["error", 2],
|
||||
"no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"functions": false
|
||||
}
|
||||
],
|
||||
"new-cap": 2,
|
||||
"no-caller": 2,
|
||||
"quotes": [
|
||||
2,
|
||||
"double"
|
||||
],
|
||||
"no-unused-vars": 2,
|
||||
"strict": [
|
||||
2,
|
||||
"function"
|
||||
]
|
||||
}
|
||||
}
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.sass-cache
|
||||
node_modules/
|
||||
|
||||
@@ -1,117 +1,13 @@
|
||||
SHELL = bash
|
||||
|
||||
# list of JS files to be built
|
||||
JS_BUILD = jquery.js vue.js highlight.js chartist.js login.js forgot.js reset.js signup.js dashboard.js logout.js commento.js
|
||||
|
||||
jquery.js = jquery.js
|
||||
vue.js = vue.js
|
||||
highlight.js = highlight.js
|
||||
chartist.js = chartist.js
|
||||
login.js = utils.js http.js auth-common.js login.js
|
||||
forgot.js = utils.js http.js forgot.js
|
||||
reset.js = utils.js http.js reset.js
|
||||
signup.js = utils.js http.js auth-common.js signup.js
|
||||
dashboard.js = utils.js http.js errors.js self.js dashboard.js dashboard-setting.js dashboard-domain.js dashboard-installation.js dashboard-general.js dashboard-moderation.js dashboard-statistics.js dashboard-import.js dashboard-danger.js
|
||||
logout.js = utils.js logout.js
|
||||
commento.js = commento.js
|
||||
|
||||
# for each file in $(JS_BUILD), list its composition
|
||||
|
||||
BUILD_DIR = build
|
||||
DEVEL_BUILD_DIR = $(BUILD_DIR)/devel
|
||||
PROD_BUILD_DIR = $(BUILD_DIR)/prod
|
||||
GULP = node_modules/.bin/gulp
|
||||
|
||||
HTML_SRC_DIR = .
|
||||
HTML_SRC_FILES = $(wildcard $(HTML_SRC_DIR)/*.html)
|
||||
HTML_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)
|
||||
HTML_DEVEL_BUILD_FILES = $(patsubst $(HTML_SRC_DIR)/%, $(HTML_DEVEL_BUILD_DIR)/%, $(HTML_SRC_FILES))
|
||||
HTML_PROD_BUILD_DIR = $(PROD_BUILD_DIR)
|
||||
HTML_PROD_BUILD_FILES = $(patsubst $(HTML_SRC_DIR)/%, $(HTML_PROD_BUILD_DIR)/%, $(HTML_SRC_FILES))
|
||||
devel:
|
||||
yarn install
|
||||
$(GULP) devel
|
||||
|
||||
HTML_MINIFIER = html-minifier
|
||||
HTML_MINIFIER_FLAGS = --collapse-whitespace --remove-comments
|
||||
|
||||
JS_SRC_DIR = js
|
||||
JS_SRC_FILES = $(wildcard $(JS_SRC_DIR)/*.js)
|
||||
JS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/js
|
||||
JS_DEVEL_BUILD_FILES = $(addprefix $(JS_DEVEL_BUILD_DIR)/, $(JS_BUILD))
|
||||
JS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/js
|
||||
JS_PROD_BUILD_FILES = $(addprefix $(JS_PROD_BUILD_DIR)/, $(JS_BUILD))
|
||||
|
||||
JS_MINIFIER = uglifyjs
|
||||
JS_MINIFIER_FLAGS = --compress --mangle
|
||||
|
||||
SASS_SRC_DIR = sass
|
||||
SASS_SRC_FILES = $(wildcard $(SASS_SRC_DIR)/*.scss)
|
||||
CSS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/css
|
||||
CSS_DEVEL_BUILD_FILES = $(patsubst $(SASS_SRC_DIR)/%.scss, $(CSS_DEVEL_BUILD_DIR)/%.css, $(SASS_SRC_FILES))
|
||||
CSS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/css
|
||||
CSS_PROD_BUILD_FILES = $(patsubst $(SASS_SRC_DIR)/%.scss, $(CSS_PROD_BUILD_DIR)/%.css, $(SASS_SRC_FILES))
|
||||
|
||||
CSS = sass
|
||||
CSS_DEVEL_FLAGS =
|
||||
CSS_PROD_FLAGS = $(CSS_DEVEL_FLAGS) --style compressed
|
||||
|
||||
IMGS_SRC_DIR = images
|
||||
IMGS_SRC_FILES = $(wildcard $(IMGS_SRC_DIR)/*)
|
||||
IMGS_DEVEL_BUILD_DIR = $(DEVEL_BUILD_DIR)/images
|
||||
IMGS_DEVEL_BUILD_FILES = $(patsubst $(IMGS_SRC_DIR)/%, $(IMGS_DEVEL_BUILD_DIR)/%, $(IMGS_SRC_FILES))
|
||||
IMGS_PROD_BUILD_DIR = $(PROD_BUILD_DIR)/images
|
||||
IMGS_PROD_BUILD_FILES = $(patsubst $(IMGS_SRC_DIR)/%, $(IMGS_PROD_BUILD_DIR)/%, $(IMGS_SRC_FILES))
|
||||
|
||||
devel: devel-html devel-js devel-css devel-imgs
|
||||
|
||||
prod: devel prod-html prod-js prod-css prod-imgs
|
||||
prod:
|
||||
yarn install
|
||||
$(GULP) prod
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILD_DIR);
|
||||
|
||||
devel-html: $(HTML_DEVEL_BUILD_FILES)
|
||||
|
||||
$(HTML_DEVEL_BUILD_FILES): $(HTML_DEVEL_BUILD_DIR)/%.html: $(HTML_SRC_DIR)/%.html
|
||||
cp $^ $@;
|
||||
|
||||
prod-html: $(HTML_PROD_BUILD_FILES)
|
||||
|
||||
$(HTML_PROD_BUILD_FILES): $(HTML_PROD_BUILD_DIR)/%.html: $(HTML_DEVEL_BUILD_DIR)/%.html
|
||||
$(HTML_MINIFIER) $(HTML_MINIFIER_FLAGS) -o $@ $^;
|
||||
|
||||
devel-js: $(JS_DEVEL_BUILD_FILES)
|
||||
|
||||
.SECONDEXPANSION:
|
||||
$(JS_DEVEL_BUILD_FILES): $(JS_DEVEL_BUILD_DIR)/%.js: $$(addprefix $$(JS_SRC_DIR)/, $$(%.js))
|
||||
>$@; \
|
||||
for f in $^; do \
|
||||
printf "// %s\n" "$$f" >>$@; \
|
||||
cat $$f >>$@; \
|
||||
printf "\n" >>$@; \
|
||||
done;
|
||||
|
||||
prod-js: $(JS_PROD_BUILD_FILES)
|
||||
|
||||
$(JS_PROD_BUILD_FILES): $(JS_PROD_BUILD_DIR)/%.js: $(JS_DEVEL_BUILD_DIR)/%.js
|
||||
$(JS_MINIFIER) $(JS_MINIFIER_FLAGS) -o $@ $^;
|
||||
|
||||
devel-css: $(CSS_DEVEL_BUILD_FILES)
|
||||
|
||||
$(CSS_DEVEL_BUILD_FILES): $(CSS_DEVEL_BUILD_DIR)/%.css: $(SASS_SRC_DIR)/%.scss $(SASS_SRC_FILES)
|
||||
$(CSS) $(CSS_DEVEL_FLAGS) $< $@;
|
||||
|
||||
prod-css: $(CSS_PROD_BUILD_FILES)
|
||||
|
||||
$(CSS_PROD_BUILD_FILES): $(CSS_PROD_BUILD_DIR)/%.css: $(SASS_SRC_DIR)/%.scss
|
||||
$(CSS) $(CSS_PROD_FLAGS) $^ $@;
|
||||
|
||||
$(shell mkdir -p $(HTML_DEVEL_BUILD_DIR) $(JS_DEVEL_BUILD_DIR) $(CSS_DEVEL_BUILD_DIR) $(HTML_PROD_BUILD_DIR) $(JS_PROD_BUILD_DIR) $(CSS_PROD_BUILD_DIR))
|
||||
|
||||
devel-imgs: $(IMGS_DEVEL_BUILD_FILES)
|
||||
|
||||
$(IMGS_DEVEL_BUILD_FILES): $(IMGS_DEVEL_BUILD_DIR)/%: $(IMGS_SRC_DIR)/%
|
||||
cp $^ $@;
|
||||
|
||||
prod-imgs: $(IMGS_PROD_BUILD_FILES)
|
||||
|
||||
$(IMGS_PROD_BUILD_FILES): $(IMGS_PROD_BUILD_DIR)/%: $(IMGS_SRC_DIR)/%
|
||||
cp $^ $@
|
||||
|
||||
$(shell mkdir -p $(HTML_DEVEL_BUILD_DIR) $(JS_DEVEL_BUILD_DIR) $(CSS_DEVEL_BUILD_DIR) $(IMGS_DEVEL_BUILD_DIR) $(HTML_PROD_BUILD_DIR) $(JS_PROD_BUILD_DIR) $(CSS_PROD_BUILD_DIR) $(IMGS_PROD_BUILD_DIR))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<head>
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0">
|
||||
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||
<title>Commento: Email Confirmation</title>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script src="[[[.CdnPrefix]]]/js/highlight.js"></script>
|
||||
<script src="[[[.CdnPrefix]]]/js/chartist.js"></script>
|
||||
<script src="[[[.CdnPrefix]]]/js/dashboard.js"></script>
|
||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/chartist.css">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/dashboard.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||
@@ -21,10 +22,10 @@
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.selfGet(function() {
|
||||
window.vueConstruct(function() {
|
||||
window.navbarFill();
|
||||
window.domainRefresh();
|
||||
window.commento.selfGet(function() {
|
||||
window.commento.vueConstruct(function() {
|
||||
window.commento.navbarFill();
|
||||
window.commento.domainRefresh();
|
||||
$(document).ready(function(){
|
||||
$("ul.tabs li").click(function(){
|
||||
var tab_id = $(this).attr("data-tab");
|
||||
@@ -51,13 +52,13 @@
|
||||
It's so quiet in here.
|
||||
</div>
|
||||
|
||||
<div class="pane-setting" v-for="domain in domains" v-on:click="window.domainSelect(domain.domain)" id="{{domain.hex}}" v-bind:class="{selected: domain.selected}" v-if="domain.show">
|
||||
<div class="pane-setting" v-for="domain in domains" v-on:click="window.commento.domainSelect(domain.domain)" id="{{domain.hex}}" v-bind:class="{selected: domain.selected}" v-if="domain.show">
|
||||
<div class="pane-setting-inside">
|
||||
<div class="setting-title">{{domain.name}}</div>
|
||||
<div class="setting-subtitle">{{domain.domain}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-setting" id="domain-add" onclick="window.location.hash='#new-domain-modal'">
|
||||
<div class="pane-setting" id="domain-add" onclick="document.location.hash='#new-domain-modal'">
|
||||
<div class="pane-setting-inside super-setting">
|
||||
<div class="super-setting-title">+</div>
|
||||
<div class="super-setting-text">New Domain</div>
|
||||
@@ -66,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pane-middle">
|
||||
<div v-if="showSettings" class="pane-setting" v-for="setting in settings" v-on:click="window.settingSelect(setting.id)" id="{{setting.id}}" v-bind:class="{selected: setting.selected}">
|
||||
<div v-if="showSettings" class="pane-setting" v-for="setting in settings" v-on:click="window.commento.settingSelect(setting.id)" id="{{setting.id}}" v-bind:class="{selected: setting.selected}">
|
||||
<div class="pane-setting-inside">
|
||||
<div class="setting-title">{{setting.text}}</div>
|
||||
<div class="setting-subtitle">{{setting.meaning}}</div>
|
||||
@@ -81,23 +82,29 @@
|
||||
<!-- Installation -->
|
||||
<div id="installation-view" class="view hidden">
|
||||
<div class="view-inside">
|
||||
<div class="large-view">
|
||||
<div class="mid-view">
|
||||
<div class="tabs-container">
|
||||
<div class="tab">
|
||||
<ul class="tabs">
|
||||
<li class="tab-link original current" data-tab="install-tab-1">Universal Snippet</li>
|
||||
<li class="tab-link original current" data-tab="installation-tab-1">Universal Snippet</li>
|
||||
</ul>
|
||||
|
||||
<div id="install-tab-1" class="content original current">
|
||||
<div class="import-text">
|
||||
<div id="installation-tab-1" class="content original current">
|
||||
<div class="normal-text">
|
||||
Copy the following piece of HTML code and paste it where you'd like Commento to load.
|
||||
</div>
|
||||
|
||||
<pre><code id="code-div" class="html"></code></pre>
|
||||
|
||||
<div class="text">
|
||||
<div class="normal-text">
|
||||
And that's it. All your settings, themes, and comments would be automagically loaded. Commento is mobile-responsive too, as it simply fills the container it is put in.
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="normal-text">
|
||||
Read the Commento documentation <a href="https://docs.commento.io/configuration/">on configuration</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,6 +119,11 @@
|
||||
<div class="center center-title">
|
||||
Analytics
|
||||
</div>
|
||||
|
||||
<div class="normal-text">
|
||||
Anonymous statistics such as monthly pageviews and number of comments
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="number">
|
||||
<div class="digits gray-digits">{{domains[cd].viewsLast30Days.zeros}}</div>
|
||||
@@ -142,28 +154,69 @@
|
||||
<!-- moderation -->
|
||||
<div id="moderation-view" class="view hidden">
|
||||
<div class="view-inside">
|
||||
<div class="small-view mid-view">
|
||||
<div class="mid-view">
|
||||
<div class="tabs-container">
|
||||
<div class="tab">
|
||||
<ul class="tabs">
|
||||
<li class="tab-link original current" data-tab="mod-tab-1">Moderator List</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>
|
||||
</ul>
|
||||
|
||||
<div id="mod-tab-1" class="content original current">
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
||||
<label for="spam-filtering">Automatic spam filtering</label>
|
||||
<div class="pitch">
|
||||
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. This is strongly recommended. Requires backend configuration.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].requireModeration" id="require-moderation">
|
||||
<label for="require-moderation">Require all comments to be approved manually</label>
|
||||
<div class="pitch">
|
||||
Enabling this would require a moderator to approve all comments. This is generally recommended if your site doesn't receive too much traffic.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].allowAnonymous" id="allow-anonymous">
|
||||
<label for="allow-anonymous">Allow anonymous comments</label>
|
||||
<div class="pitch">
|
||||
Enabling this would allow your readers to comment anonymously. Disabling would require the to authenticate themselves (using their Google account, for example). Recommended.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-border round-check indent" v-if="domains[cd].allowAnonymous">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].moderateAllAnonymous" id="moderate-all-anonymous">
|
||||
<label for="moderate-all-anonymous">Require anonymous comments to be approved manually</label>
|
||||
<div class="pitch">
|
||||
Enabling this would require a moderator to approve all anonymous comments. This is recommended if most of your spam comments are from anonymous users.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mod-tab-2" class="content">
|
||||
<div class="pitch">
|
||||
Moderators have the power to approve and delete comments. To make someone a moderator, add their email address down below. Once added, shiny new moderation buttons will appear on each comment for that person on each page on this domain.
|
||||
Moderators have the power to approve/delete comments and lock threads. Once you add an user as a moderator, shiny new buttons will appear on each comment on each page when they log in.<br><br>
|
||||
|
||||
You're still the only administrator and the only person who can add and remove moderators. Moderators do not have access to this dashboard. Their access is restricted to pages on your website.
|
||||
</div>
|
||||
<div class="commento-email-container">
|
||||
<div class="commento-email">
|
||||
<input class="commento-input" type="text" id="new-mod" placeholder="Email">
|
||||
<button id="new-mod-button" class="commento-email-button" onclick="window.moderatorNewHandler()">Add moderator</button>
|
||||
<button id="new-mod-button" class="commento-email-button" onclick="window.commento.moderatorNewHandler()">Add moderator</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mod-emails-container">
|
||||
<div class="content">
|
||||
<div class="mod-email" v-for="email in domains[cd].moderators" v-if="domains[cd].moderators.length > 0">
|
||||
<div class="email">{{email.email}}</div>
|
||||
<div class="delete" v-on:click="window.moderatorDeleteHandler(email.email)">Delete</div>
|
||||
<div class="delete" v-on:click="window.commento.moderatorDeleteHandler(email.email)">Delete</div>
|
||||
<div class="date">added {{email.timeAgo}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,47 +228,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Settings -->
|
||||
<!-- Configure Domain -->
|
||||
<div id="general-view" class="view hidden">
|
||||
<div class="view-inside">
|
||||
<div class="small-mid-view">
|
||||
<div class="center center-title">
|
||||
General Settings
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="label">Website Name</div>
|
||||
<input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name">
|
||||
</div>
|
||||
<div class="mid-view">
|
||||
<div class="tabs-container">
|
||||
<div class="tab">
|
||||
<ul class="tabs">
|
||||
<li class="tab-link original current" data-tab="configure-tab-1">General</li>
|
||||
<!-- <li class="tab-link" data-tab="configure-tab-2">Email Settings</li> -->
|
||||
<li class="tab-link" data-tab="configure-tab-3">Export Data</li>
|
||||
</ul>
|
||||
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].autoSpamFilter" id="spam-filtering">
|
||||
<label for="spam-filtering">Automatic spam filtering</label>
|
||||
<div class="pitch">
|
||||
Commento uses Akismet's advanced spam detection to automatically identify and remove spam comments. We strongly recommended you have this enabled.
|
||||
<div id="configure-tab-1" class="content original current">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="label">Website Name</div>
|
||||
<input class="input gray-input" id="cur-domain-name" type="text" :placeholder="domains[cd].origName" v-model="domains[cd].name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<button id="save-general-button" onclick="window.commento.generalSaveHandler()" class="button">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div id="configure-tab-2" class="content">
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div id="configure-tab-3" class="content">
|
||||
<div class="normal-text">
|
||||
You can export an archive of this domain's data (which includes all comments and commenters) in the JSON format. To initiate and queue an archive request, click the button below. You will receive an email containing the archive once it's ready.<br><br>
|
||||
|
||||
Please note that this requires valid SMTP settings in order to send emails.<br><br>
|
||||
|
||||
<div class="center">
|
||||
<button id="domain-export-button" onclick="window.commento.domainExportBegin()" class="button">Initiate Data Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].requireModeration" id="require-moderation">
|
||||
<label for="require-moderation">Require all comments to be approved manually</label>
|
||||
<div class="pitch">
|
||||
Enabling this would require a moderator to approve every comment. Moderators can manually delete comments even if this is not enabled.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-border round-check">
|
||||
<input type="checkbox" class="switch" v-model="domains[cd].requireIdentification" id="require-identification">
|
||||
<label for="require-identification">Require identification</label>
|
||||
<div class="pitch">
|
||||
Enabling this would require all commenters to authenticate themselves (using their Google account, for example). Disabling would allow anonymous comments.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="new-domain-error" class="modal-error-box"></div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<button id="save-general-button" onclick="window.generalSaveHandler()" class="button">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +277,7 @@
|
||||
<!-- Import Comments -->
|
||||
<div id="import-view" class="view hidden">
|
||||
<div class="view-inside">
|
||||
<div class="large-view">
|
||||
<div class="mid-view">
|
||||
<div class="tabs-container">
|
||||
<div class="tab">
|
||||
<ul class="tabs">
|
||||
@@ -232,32 +285,41 @@
|
||||
</ul>
|
||||
|
||||
<div id="install-tab-1" class="content original current">
|
||||
<div class="import-text">
|
||||
If you're currently using Disqus and want to import all your comments into Commento, you can do so:
|
||||
<div class="normal-text">
|
||||
If you're currently using Disqus, you can import all comments into Commento:
|
||||
<ul>
|
||||
<li>
|
||||
Go to <a href="http://disqus.com/admin/discussions/export/">the admin export section</a> in Disqus and click on <b>Export Comments</b>. This should start the process of exporting your comments.
|
||||
Go to <a href="http://disqus.com/admin/discussions/export/">the admin export section</a> in Disqus and click on <b>Export Comments</b>. This should start the process of exporting your comments in the background.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
After a while, you'll receive an email from Disqus with a link to a compressed archive of all comments and associated data. Copy and paste that link here and start the import process:
|
||||
You'll receive an email from Disqus with a link to a compressed archive of all comments and associated data. Copy and paste that link here to start the import process:
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="commento-email-container">
|
||||
<div class="commento-email">
|
||||
<input class="commento-input" type="text" id="disqus-url" placeholder="https://media.disqus.com/uploads/...">
|
||||
<button id="disqus-import-button" class="commento-email-button" onclick="importDisqus()">Import</button>
|
||||
<button id="disqus-import-button" class="commento-email-button" onclick="window.commento.importDisqus()">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="subtext-container">
|
||||
<div class="subtext">
|
||||
<div>Note: it is strongly recommended you do this only once. Multiple imports for the same domain may have unintended effects.</div>
|
||||
<div>By using this service, you grant Commento the permission to download and process your Disqus information.</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<br>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
We'll automatically download this file, extract it, parse it and import comments into Commento. The URL information will be preserved. By using this service, you grant Commento the permission to download and process your Disqus information.
|
||||
Commento will automatically download this file, extract it, parse it and import comments into Commento. URL information, comment authors, text formatting, and nested replies will be preserved.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
It is strongly recommended you do this only once. Importing multiple times may have unintended effects.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -286,7 +348,7 @@
|
||||
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>
|
||||
|
||||
<button onclick="window.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button>
|
||||
<button onclick="document.location.hash='#unfreeze-domain-modal'" class="button green-button">Unfreeze Domain</button>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="domains[cd].state != 'frozen'">
|
||||
@@ -294,7 +356,7 @@
|
||||
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="window.location.hash='#freeze-domain-modal'" class="button orange-button">Freeze Domain</button>
|
||||
<button id="orange-button" onclick="document.location.hash='#freeze-domain-modal'" class="button orange-button">Freeze Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,7 +367,7 @@
|
||||
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>
|
||||
|
||||
<button id="big-red-button" class="button big-red-button" onclick="window.location.hash='#delete-domain-modal'">Delete Domain</button>
|
||||
<button id="big-red-button" class="button big-red-button" onclick="document.location.hash='#delete-domain-modal'">Delete Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,7 +385,7 @@
|
||||
Are you absolutely sure you want to freeze your domain, thereby making it read-only? You can choose to unfreeze later; this is temporary.
|
||||
</div>
|
||||
<div class="modal-contents">
|
||||
<button id="orange-button" class="button orange-button" onclick="window.domainFreezeHandler()">Freeze Domain</button>
|
||||
<button id="orange-button" class="button orange-button" onclick="window.commento.domainFreezeHandler()">Freeze Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -336,7 +398,7 @@
|
||||
Are you absolutely sure you want to unfreeze your domain? This will re-allow new comments. You can choose to freeze again in the future.
|
||||
</div>
|
||||
<div class="modal-contents">
|
||||
<button id="blue-button" class="button green-button" onclick="window.domainUnfreezeHandler()">Unfreeze Domain</button>
|
||||
<button id="blue-button" class="button green-button" onclick="window.commento.domainUnfreezeHandler()">Unfreeze Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +411,7 @@
|
||||
Are you absolutely sure? This will permanently delete all comments and there is literally no way to retrieve your data once you do this.
|
||||
</div>
|
||||
<div class="modal-contents">
|
||||
<button id="big-red-button" class="button big-red-button" onclick="window.domainDeleteHandler()">Delete Domain</button>
|
||||
<button id="big-red-button" class="button big-red-button" onclick="window.commento.domainDeleteHandler()">Delete Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,7 +427,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Website Name</div>
|
||||
<input class="input gray-input" id="new-domain-name" type="text" placeholder="Billie Joe's Blog">
|
||||
<input class="input gray-input" id="new-domain-name" type="text" placeholder="My Blog">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Website Domain</div>
|
||||
@@ -374,7 +436,7 @@
|
||||
</div>
|
||||
<div id="new-domain-error" class="modal-error-box"></div>
|
||||
<div class="center">
|
||||
<button id="add-site-button" onclick="window.domainNewHandler()" class="button">Add Domain</button>
|
||||
<button id="add-site-button" onclick="window.commento.domainNewHandler()" class="button">Add Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0">
|
||||
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||
<script src="[[[.CdnPrefix]]]/js/forgot.js"></script>
|
||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||
<title>Commento: Reset your Password</title>
|
||||
@@ -25,7 +26,7 @@
|
||||
</div>
|
||||
<div class="err" id="err"></div>
|
||||
<div class="msg" id="msg"></div>
|
||||
<button id="reset-button" class="button" onclick="sendResetHex()">Send Reset Password Link</button>
|
||||
<button id="reset-button" class="button" onclick="window.commento.sendResetHex()">Send Reset Password Link</button>
|
||||
<a class="link" href="[[[.Origin]]]/login">Suddenly remembered your password? Login.</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
140
frontend/gulpfile.js
Normal file
140
frontend/gulpfile.js
Normal file
@@ -0,0 +1,140 @@
|
||||
"use strict";
|
||||
|
||||
const gulp = require("gulp");
|
||||
const sass = require("gulp-sass");
|
||||
const sourcemaps = require("gulp-sourcemaps");
|
||||
const cleanCss = require("gulp-clean-css");
|
||||
const htmlMinifier = require("gulp-html-minifier");
|
||||
const uglify = require("gulp-uglify");
|
||||
const concat = require("gulp-concat");
|
||||
const rename = require("gulp-rename");
|
||||
const eslint = require("gulp-eslint");
|
||||
|
||||
const develPath = "build/devel/";
|
||||
const prodPath = "build/prod/";
|
||||
const scssSrc = "./sass/*.scss";
|
||||
const cssDir = "css/";
|
||||
const imagesDir = "images/";
|
||||
const imagesGlob = imagesDir + "**/*";
|
||||
const jsDir = "js/";
|
||||
const jsGlob = jsDir + "*.js";
|
||||
const htmlGlob = "./*.html";
|
||||
|
||||
const jsCompileMap = {
|
||||
"js/jquery.js": ["node_modules/jquery/dist/jquery.min.js"],
|
||||
"js/vue.js": ["node_modules/vue/dist/vue.min.js"],
|
||||
"js/highlight.js": ["node_modules/highlightjs/highlight.pack.min.js"],
|
||||
"js/chartist.js": ["node_modules/chartist/dist/chartist.min.js"],
|
||||
"js/login.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/http.js",
|
||||
"js/auth-common.js",
|
||||
"js/login.js"
|
||||
],
|
||||
"js/forgot.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/http.js",
|
||||
"js/forgot.js"
|
||||
],
|
||||
"js/reset.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/http.js",
|
||||
"js/reset.js"
|
||||
],
|
||||
"js/signup.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/http.js",
|
||||
"js/auth-common.js",
|
||||
"js/signup.js"
|
||||
],
|
||||
"js/dashboard.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/http.js",
|
||||
"js/errors.js",
|
||||
"js/self.js",
|
||||
"js/dashboard.js",
|
||||
"js/dashboard-setting.js",
|
||||
"js/dashboard-domain.js",
|
||||
"js/dashboard-installation.js",
|
||||
"js/dashboard-general.js",
|
||||
"js/dashboard-moderation.js",
|
||||
"js/dashboard-statistics.js",
|
||||
"js/dashboard-import.js",
|
||||
"js/dashboard-danger.js",
|
||||
"js/dashboard-export.js",
|
||||
],
|
||||
"js/logout.js": [
|
||||
"js/constants.js",
|
||||
"js/utils.js",
|
||||
"js/logout.js"
|
||||
],
|
||||
"js/commento.js": ["js/commento.js"],
|
||||
};
|
||||
|
||||
gulp.task("scss-devel", function () {
|
||||
return gulp.src(scssSrc)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({outputStyle: "expanded"}).on("error", sass.logError))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(develPath + cssDir));
|
||||
});
|
||||
|
||||
gulp.task("scss-prod", function () {
|
||||
return gulp.src(scssSrc)
|
||||
.pipe(sass({outputStyle: "compressed"}).on("error", sass.logError))
|
||||
.pipe(cleanCss({compatibility: "ie8", level: 2}))
|
||||
.pipe(gulp.dest(prodPath + cssDir));
|
||||
});
|
||||
|
||||
gulp.task("html-devel", function () {
|
||||
gulp.src([htmlGlob]).pipe(gulp.dest(develPath));
|
||||
});
|
||||
|
||||
gulp.task("html-prod", function () {
|
||||
gulp.src(htmlGlob)
|
||||
.pipe(htmlMinifier({collapseWhitespace: true, removeComments: true}))
|
||||
.pipe(gulp.dest(prodPath))
|
||||
});
|
||||
|
||||
gulp.task("images-devel", function () {
|
||||
gulp.src([imagesGlob]).pipe(gulp.dest(develPath + imagesDir));
|
||||
});
|
||||
|
||||
gulp.task("images-prod", function () {
|
||||
gulp.src([imagesGlob]).pipe(gulp.dest(prodPath + imagesDir));
|
||||
});
|
||||
|
||||
gulp.task("js-devel", function () {
|
||||
for (let outputFile in jsCompileMap) {
|
||||
gulp.src(jsCompileMap[outputFile])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat(outputFile))
|
||||
.pipe(rename(outputFile))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(develPath))
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task("js-prod", function () {
|
||||
for (let outputFile in jsCompileMap) {
|
||||
gulp.src(jsCompileMap[outputFile])
|
||||
.pipe(concat(outputFile))
|
||||
.pipe(rename(outputFile))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(prodPath))
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task("lint", function () {
|
||||
return gulp.src(jsGlob)
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.failAfterError())
|
||||
});
|
||||
|
||||
gulp.task("devel", ["scss-devel", "html-devel", "images-devel", "lint", "js-devel"]);
|
||||
gulp.task("prod", ["scss-prod", "html-prod", "images-prod", "lint", "js-prod"]);
|
||||
@@ -1,20 +1,22 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
// Redirect the user to the dashboard if there's a cookie. If the cookie is
|
||||
// invalid, they would be redirected back to the login page *after* the
|
||||
// cookie is deleted.
|
||||
global.loggedInRedirect = function() {
|
||||
if (global.cookieGet("commentoOwnerToken") !== undefined)
|
||||
document.location = global.commentoOrigin + "/dashboard";
|
||||
if (global.cookieGet("commentoOwnerToken") !== undefined) {
|
||||
document.location = global.origin + "/dashboard";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Prefills the email field from the URL parameter.
|
||||
global.prefillEmail = function() {
|
||||
if (paramGet("email") != undefined) {
|
||||
$("#email").val(paramGet("email"));
|
||||
if (global.paramGet("email") !== undefined) {
|
||||
$("#email").val(global.paramGet("email"));
|
||||
$("#password").click();
|
||||
}
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
13
frontend/js/constants.js
Normal file
13
frontend/js/constants.js
Normal file
@@ -0,0 +1,13 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
if (window.commento === undefined) {
|
||||
window.commento = {};
|
||||
}
|
||||
|
||||
window.commento.origin = "[[[.Origin]]]";
|
||||
window.commento.cdn = "[[[.CdnPrefix]]]";
|
||||
|
||||
} (window, document));
|
||||
@@ -1,4 +1,5 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
// Opens the danger zone.
|
||||
global.dangerOpen = function() {
|
||||
@@ -11,9 +12,10 @@
|
||||
global.domainDeleteHandler = function() {
|
||||
var data = global.dashboard.$data;
|
||||
|
||||
domainDelete(data.domains[data.cd].domain, function(success) {
|
||||
if (success)
|
||||
document.location = global.commentoOrigin + '/dashboard';
|
||||
global.domainDelete(data.domains[data.cd].domain, function(success) {
|
||||
if (success) {
|
||||
document.location = global.origin + "/dashboard";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +25,7 @@
|
||||
var data = global.dashboard.$data;
|
||||
|
||||
data.domains[data.cd].state = "frozen"
|
||||
domainUpdate(data.domains[data.cd])
|
||||
global.domainUpdate(data.domains[data.cd])
|
||||
document.location.hash = "#modal-close";
|
||||
}
|
||||
|
||||
@@ -33,9 +35,9 @@
|
||||
var data = global.dashboard.$data;
|
||||
|
||||
data.domains[data.cd].state = "unfrozen"
|
||||
domainUpdate(data.domains[data.cd])
|
||||
global.domainUpdate(data.domains[data.cd])
|
||||
document.location.hash = "#modal-close";
|
||||
}
|
||||
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
// Selects a domain.
|
||||
global.domainSelect = function(domain) {
|
||||
@@ -6,19 +7,19 @@
|
||||
var domains = data.domains;
|
||||
|
||||
for (var i = 0; i < domains.length; i++) {
|
||||
if (domains[i].domain == domain) {
|
||||
vs("frozen", domains[i].state == "frozen");
|
||||
if (domains[i].domain === domain) {
|
||||
global.vs("frozen", domains[i].state === "frozen");
|
||||
domains[i].selected = true;
|
||||
data.cd = i;
|
||||
data.importedComments = domains[i].importedComments;
|
||||
}
|
||||
else
|
||||
} else {
|
||||
domains[i].selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
data.showSettings = true;
|
||||
|
||||
settingDeselectAll();
|
||||
global.settingDeselectAll();
|
||||
$(".view").hide();
|
||||
};
|
||||
|
||||
@@ -28,8 +29,9 @@
|
||||
var data = global.dashboard.$data;
|
||||
var domains = data.domains;
|
||||
|
||||
for (var i = 0; i < domains.length; i++)
|
||||
for (var i = 0; i < domains.length; i++) {
|
||||
domains[i].selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@
|
||||
}
|
||||
|
||||
global.buttonDisable("#add-site-button");
|
||||
global.post(global.commentoOrigin + "/api/domain/new", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/new", json, function(resp) {
|
||||
global.buttonEnable("#add-site-button");
|
||||
|
||||
$("#new-domain-name").val("");
|
||||
@@ -69,15 +71,15 @@
|
||||
"ownerToken": global.cookieGet("commentoOwnerToken"),
|
||||
};
|
||||
|
||||
global.post(global.commentoOrigin + "/api/domain/list", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/list", json, function(resp) {
|
||||
if (!resp.success) {
|
||||
global.globalErrorShow(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
resp.domains = resp.domains.sort(function(a, b) {
|
||||
var x = a.creationDate; var y = b.creationDate;
|
||||
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||
var x = a.creationDate; var y = b.creationDate;
|
||||
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||
});
|
||||
|
||||
for (var i = 0; i < resp.domains.length; i++) {
|
||||
@@ -89,6 +91,8 @@
|
||||
|
||||
resp.domains[i].viewsLast30Days = global.numberify(0);
|
||||
resp.domains[i].commentsLast30Days = global.numberify(0);
|
||||
|
||||
resp.domains[i].allowAnonymous = !resp.domains[i].requireIdentification;
|
||||
|
||||
for (var j = 0; j < resp.domains[i].moderators.length; j++) {
|
||||
resp.domains[i].moderators[j].timeAgo = global.timeSince(
|
||||
@@ -98,22 +102,25 @@
|
||||
|
||||
global.vs("domains", resp.domains);
|
||||
|
||||
if (callback !== undefined)
|
||||
if (callback !== undefined) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Updates a domain with the backend.
|
||||
global.domainUpdate = function(domain, callback) {
|
||||
domain.requireIdentification = !domain.allowAnonymous;
|
||||
var json = {
|
||||
"ownerToken": global.cookieGet("commentoOwnerToken"),
|
||||
"domain": domain,
|
||||
};
|
||||
|
||||
global.post(global.commentoOrigin + "/api/domain/update", json, function(resp) {
|
||||
if (callback !== undefined)
|
||||
global.post(global.origin + "/api/domain/update", json, function(resp) {
|
||||
if (callback !== undefined) {
|
||||
callback(resp.success);
|
||||
}
|
||||
|
||||
if (!resp.success) {
|
||||
global.globalErrorShow(resp.message);
|
||||
@@ -130,15 +137,16 @@
|
||||
"domain": domain,
|
||||
};
|
||||
|
||||
global.post(global.commentoOrigin + "/api/domain/delete", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/delete", json, function(resp) {
|
||||
if (!resp.success) {
|
||||
global.globalErrorShow(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback !== undefined)
|
||||
if (callback !== undefined) {
|
||||
callback(resp.success);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
26
frontend/js/dashboard-export.js
Normal file
26
frontend/js/dashboard-export.js
Normal file
@@ -0,0 +1,26 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
global.domainExportBegin = function() {
|
||||
var data = global.dashboard.$data;
|
||||
|
||||
var json = {
|
||||
"ownerToken": global.cookieGet("commentoOwnerToken"),
|
||||
"domain": data.domains[data.cd].domain,
|
||||
}
|
||||
|
||||
global.buttonDisable("#domain-export-button");
|
||||
global.post(global.origin + "/api/domain/export/begin", json, function(resp) {
|
||||
global.buttonEnable("#domain-export-button");
|
||||
if (!resp.success) {
|
||||
global.globalErrorShow(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
global.globalOKShow("Data export operation has been successfully queued. You will receive an email.");
|
||||
});
|
||||
};
|
||||
|
||||
} (window.commento, document));
|
||||
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Opens the general settings window.
|
||||
global.generalOpen = function() {
|
||||
@@ -16,4 +19,4 @@
|
||||
});
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Opens the import window.
|
||||
global.importOpen = function() {
|
||||
@@ -6,7 +9,6 @@
|
||||
$("#import-view").show();
|
||||
}
|
||||
|
||||
|
||||
global.importDisqus = function() {
|
||||
var url = $("#disqus-url").val();
|
||||
var data = global.dashboard.$data;
|
||||
@@ -18,7 +20,7 @@
|
||||
}
|
||||
|
||||
global.buttonDisable("#disqus-import-button");
|
||||
global.post(global.commentoOrigin + "/api/domain/import/disqus", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/import/disqus", json, function(resp) {
|
||||
global.buttonEnable("#disqus-import-button");
|
||||
|
||||
if (!resp.success) {
|
||||
@@ -28,8 +30,8 @@
|
||||
|
||||
$("#disqus-import-button").hide();
|
||||
|
||||
globalOKShow("Imported " + resp.numImported + " comments!");
|
||||
global.globalOKShow("Imported " + resp.numImported + " comments!");
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Opens the installation view.
|
||||
global.installationOpen = function() {
|
||||
var data = global.dashboard.$data;
|
||||
|
||||
var html = '' +
|
||||
'<div id="commento"></div>\n' +
|
||||
'<script src="' + window.commentoCdn + '/js/commento.js"><\/script>\n' +
|
||||
'';
|
||||
var html = "" +
|
||||
"<div id=\"commento\"></div>\n" +
|
||||
"<script src=\"" + global.cdn + "/js/commento.js\"><\/script>\n" +
|
||||
"";
|
||||
|
||||
$("#code-div").text(html);
|
||||
|
||||
$('pre code').each(function(i, block) {
|
||||
$("pre code").each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
|
||||
@@ -19,4 +20,4 @@
|
||||
$("#installation-view").show();
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Opens the moderatiosn settings window.
|
||||
global.moderationOpen = function() {
|
||||
@@ -20,16 +23,16 @@
|
||||
|
||||
var idx = -1;
|
||||
for (var i = 0; i < data.domains[data.cd].moderators.length; i++) {
|
||||
if (data.domains[data.cd].moderators[i].email == email) {
|
||||
if (data.domains[data.cd].moderators[i].email === email) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx == -1) {
|
||||
if (idx === -1) {
|
||||
data.domains[data.cd].moderators.push({"email": email, "timeAgo": "just now"});
|
||||
global.buttonDisable("#new-mod-button");
|
||||
global.post(global.commentoOrigin + "/api/domain/moderator/new", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/moderator/new", json, function(resp) {
|
||||
global.buttonEnable("#new-mod-button");
|
||||
|
||||
if (!resp.success) {
|
||||
@@ -41,8 +44,7 @@
|
||||
$("#new-mod").val("");
|
||||
$("#new-mod").focus();
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
global.globalErrorShow("Already a moderator.");
|
||||
}
|
||||
}
|
||||
@@ -60,23 +62,23 @@
|
||||
|
||||
var idx = -1;
|
||||
for (var i = 0; i < data.domains[data.cd].moderators.length; i++) {
|
||||
if (data.domains[data.cd].moderators[i].email == email) {
|
||||
if (data.domains[data.cd].moderators[i].email === email) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx != -1) {
|
||||
if (idx !== -1) {
|
||||
data.domains[data.cd].moderators.splice(idx, 1);
|
||||
global.post(global.commentoOrigin + "/api/domain/moderator/delete", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/moderator/delete", json, function(resp) {
|
||||
if (!resp.success) {
|
||||
global.globalErrorShow(resp.message);
|
||||
return
|
||||
}
|
||||
|
||||
globalOKShow("Removed!");
|
||||
global.globalOKShow("Removed!");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Sets the vue.js toggle to select and deselect panes visually.
|
||||
function settingSelectCSS(id) {
|
||||
@@ -6,12 +9,7 @@
|
||||
var settings = data.settings;
|
||||
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i].id == id) {
|
||||
settings[i].selected = true;
|
||||
}
|
||||
else {
|
||||
settings[i].selected = false;
|
||||
}
|
||||
settings[i].selected = settings[i].id === id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +26,9 @@
|
||||
$(".original").addClass("current");
|
||||
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (id == settings[i].id)
|
||||
if (id === settings[i].id) {
|
||||
settings[i].open();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,8 +38,9 @@
|
||||
var data = global.dashboard.$data;
|
||||
var settings = data.settings;
|
||||
|
||||
for (var i = 0; i < settings.length; i++)
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
settings[i].selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
global.numberify = function(x) {
|
||||
if (x == 0)
|
||||
if (x === 0) {
|
||||
return {"zeros": "000", "num": "", "units": ""}
|
||||
}
|
||||
|
||||
if (x < 10)
|
||||
if (x < 10) {
|
||||
return {"zeros": "00", "num": x, "units": ""}
|
||||
}
|
||||
|
||||
if (x < 100)
|
||||
if (x < 100) {
|
||||
return {"zeros": "0", "num": x, "units": ""}
|
||||
}
|
||||
|
||||
if (x < 1000)
|
||||
if (x < 1000) {
|
||||
return {"zeros": "", "num": x, "units": ""}
|
||||
}
|
||||
|
||||
var res;
|
||||
|
||||
if (x < 1000000) {
|
||||
res = numberify((x/1000).toFixed(0))
|
||||
res = global.numberify((x/1000).toFixed(0))
|
||||
res.units = "K"
|
||||
}
|
||||
else if (x < 1000000000) {
|
||||
res = numberify((x/1000000).toFixed(0))
|
||||
} else if (x < 1000000000) {
|
||||
res = global.numberify((x/1000000).toFixed(0))
|
||||
res.units = "M"
|
||||
}
|
||||
else if (x < 1000000000000) {
|
||||
res = numberify((x/1000000000).toFixed(0))
|
||||
} else if (x < 1000000000000) {
|
||||
res = global.numberify((x/1000000000).toFixed(0))
|
||||
res.units = "B"
|
||||
}
|
||||
|
||||
if (res.num*10 % 10 == 0)
|
||||
if (res.num*10 % 10 === 0) {
|
||||
res.num = Math.ceil(res.num);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -43,11 +49,11 @@
|
||||
}
|
||||
|
||||
$(".view").hide();
|
||||
post(global.commentoOrigin + "/api/domain/statistics", json, function(resp) {
|
||||
global.post(global.origin + "/api/domain/statistics", json, function(resp) {
|
||||
$("#statistics-view").show();
|
||||
|
||||
if (!resp.success) {
|
||||
globalErrorShow(resp.message);
|
||||
global.globalErrorShow(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,12 +80,12 @@
|
||||
|
||||
var labels = new Array();
|
||||
for (var i = 0; i < views.length; i++) {
|
||||
if ((views.length-i) % 7 == 0) {
|
||||
if ((views.length-i) % 7 === 0) {
|
||||
var x = (views.length-i)/7;
|
||||
labels.push(x + " week" + (x > 1 ? "s" : "") + " ago");
|
||||
}
|
||||
else
|
||||
} else {
|
||||
labels.push("");
|
||||
}
|
||||
}
|
||||
|
||||
new Chartist.Line("#views-graph", {
|
||||
@@ -92,9 +98,13 @@
|
||||
series: [comments],
|
||||
}, options);
|
||||
|
||||
data.domains[data.cd].viewsLast30Days = numberify(views.reduce(function(a, b) { return a + b; }, 0));
|
||||
data.domains[data.cd].commentsLast30Days = numberify(comments.reduce(function(a, b) { return a + b; }, 0));
|
||||
data.domains[data.cd].viewsLast30Days = global.numberify(views.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0));
|
||||
data.domains[data.cd].commentsLast30Days = global.numberify(comments.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0));
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Sets a vue.js field. Short for "vue set".
|
||||
function vs(field, value) {
|
||||
@@ -20,45 +23,45 @@
|
||||
var settings = [
|
||||
{
|
||||
"id": "installation",
|
||||
"text": "Installation",
|
||||
"text": "Installation Guide",
|
||||
"meaning": "Install Commento with HTML",
|
||||
"selected": false,
|
||||
"open": installationOpen,
|
||||
"open": global.installationOpen,
|
||||
},
|
||||
{
|
||||
"id": "general",
|
||||
"text": "General Settings",
|
||||
"meaning": "Names, domains and the rest",
|
||||
"text": "General",
|
||||
"meaning": "Email settings, data export",
|
||||
"selected": false,
|
||||
"open": generalOpen,
|
||||
"open": global.generalOpen,
|
||||
},
|
||||
{
|
||||
"id": "moderation",
|
||||
"text": "Moderation Settings",
|
||||
"meaning": "Approve and delete comments",
|
||||
"meaning": "Manage moderators, spam filtering",
|
||||
"selected": false,
|
||||
"open": moderationOpen,
|
||||
"open": global.moderationOpen,
|
||||
},
|
||||
{
|
||||
"id": "statistics",
|
||||
"text": "Statistics",
|
||||
"meaning": "Usage and comment statistics",
|
||||
"text": "Analytics",
|
||||
"meaning": "Anonymous statistics and graphs",
|
||||
"selected": false,
|
||||
"open": statisticsOpen,
|
||||
"open": global.statisticsOpen,
|
||||
},
|
||||
{
|
||||
"id": "import",
|
||||
"text": "Import Comments",
|
||||
"meaning": "Import from a different service",
|
||||
"selected": false,
|
||||
"open": importOpen,
|
||||
"open": global.importOpen,
|
||||
},
|
||||
{
|
||||
"id": "danger",
|
||||
"text": "Danger Zone",
|
||||
"meaning": "Delete or freeze domain",
|
||||
"selected": false,
|
||||
"open": dangerOpen,
|
||||
"open": global.dangerOpen,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -82,8 +85,9 @@
|
||||
data: reactiveData,
|
||||
});
|
||||
|
||||
if (callback !== undefined)
|
||||
if (callback !== undefined) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Registers a given ID for a fade out after 5 seconds.
|
||||
global.registerHide = function(id) {
|
||||
var el = $(id);
|
||||
|
||||
setTimeout(function() {
|
||||
$(id).fadeOut("fast");
|
||||
}, 5000);
|
||||
@@ -28,4 +29,4 @@
|
||||
global.showGlobalMessage("#global-ok", text);
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Talks to the API and sends an reset email.
|
||||
global.sendResetHex = function() {
|
||||
var all_ok = global.unfilledMark(["#email"], function(el) {
|
||||
var allOk = global.unfilledMark(["#email"], function(el) {
|
||||
el.css("border-bottom", "1px solid red");
|
||||
});
|
||||
|
||||
if (!all_ok) {
|
||||
if (!allOk) {
|
||||
global.textSet("#err", "Please make sure all fields are filled.");
|
||||
return;
|
||||
}
|
||||
@@ -16,7 +19,7 @@
|
||||
};
|
||||
|
||||
global.buttonDisable("#reset-button");
|
||||
global.post(global.commentoOrigin + "/api/owner/send-reset-hex", json, function(resp) {
|
||||
global.post(global.origin + "/api/owner/send-reset-hex", json, function(resp) {
|
||||
global.buttonEnable("#reset-button");
|
||||
|
||||
global.textSet("#err", "");
|
||||
@@ -30,4 +33,4 @@
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,7 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Performs a JSON POST request to the given url with the given data and
|
||||
// calls the callback function with the JSON response.
|
||||
@@ -28,4 +31,4 @@
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
4
frontend/js/jquery.js
vendored
4
frontend/js/jquery.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,13 +1,15 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
(document);
|
||||
|
||||
// Shows messages produced from email confirmation attempts.
|
||||
function displayConfirmedEmail() {
|
||||
var confirmed = global.paramGet("confirmed");
|
||||
|
||||
if (confirmed == "true") {
|
||||
if (confirmed === "true") {
|
||||
$("#msg").html("Successfully confirmed! Login to continue.")
|
||||
}
|
||||
else if (confirmed == "false") {
|
||||
} else if (confirmed === "false") {
|
||||
$("#err").html("That link has expired.")
|
||||
}
|
||||
}
|
||||
@@ -15,18 +17,18 @@
|
||||
|
||||
// Shows messages produced from password reset attempts.
|
||||
function displayChangedPassword() {
|
||||
var changed = paramGet("changed");
|
||||
var changed = global.paramGet("changed");
|
||||
|
||||
if (changed == "true") {
|
||||
if (changed === "true") {
|
||||
$("#msg").html("Password changed successfully! Login to continue.")
|
||||
}
|
||||
}
|
||||
|
||||
// Shows messages produced from completed signups.
|
||||
function displaySignedUp() {
|
||||
var signedUp = paramGet("signedUp");
|
||||
var signedUp = global.paramGet("signedUp");
|
||||
|
||||
if (signedUp == "true") {
|
||||
if (signedUp === "true") {
|
||||
$("#msg").html("Registration successful! Login to continue.")
|
||||
}
|
||||
}
|
||||
@@ -42,11 +44,11 @@
|
||||
|
||||
// Logs the user in and redirects to the dashboard.
|
||||
global.login = function() {
|
||||
var all_ok = global.unfilledMark(["#email", "#password"], function(el) {
|
||||
var allOk = global.unfilledMark(["#email", "#password"], function(el) {
|
||||
el.css("border-bottom", "1px solid red");
|
||||
});
|
||||
|
||||
if (!all_ok) {
|
||||
if (!allOk) {
|
||||
global.textSet("#err", "Please make sure all fields are filled");
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +59,7 @@
|
||||
};
|
||||
|
||||
global.buttonDisable("#login-button");
|
||||
global.post(global.commentoOrigin + "/api/owner/login", json, function(resp) {
|
||||
global.post(global.origin + "/api/owner/login", json, function(resp) {
|
||||
global.buttonEnable("#login-button");
|
||||
|
||||
if (!resp.success) {
|
||||
@@ -66,8 +68,8 @@
|
||||
}
|
||||
|
||||
global.cookieSet("commentoOwnerToken", resp.ownerToken);
|
||||
document.location = global.commentoOrigin + "/dashboard";
|
||||
document.location = global.origin + "/dashboard";
|
||||
});
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
global.logout = function() {
|
||||
global.cookieDelete("commentoOwnerToken");
|
||||
document.location = global.commentoOrigin + "/login";
|
||||
document.location = global.origin + "/login";
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
global.resetPassword = function() {
|
||||
var all_ok = global.unfilledMark(["#password", "#password2"], function(el) {
|
||||
var allOk = global.unfilledMark(["#password", "#password2"], function(el) {
|
||||
el.css("border-bottom", "1px solid red");
|
||||
});
|
||||
|
||||
if (!all_ok) {
|
||||
if (!allOk) {
|
||||
global.textSet("#err", "Please make sure all fields are filled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($("#password").val() != $("#password2").val()) {
|
||||
if ($("#password").val() !== $("#password2").val()) {
|
||||
global.textSet("#err", "The two passwords do not match.");
|
||||
return;
|
||||
}
|
||||
|
||||
var json = {
|
||||
"resetHex": paramGet("hex"),
|
||||
"resetHex": global.paramGet("hex"),
|
||||
"password": $("#password").val(),
|
||||
};
|
||||
|
||||
global.buttonDisable("#reset-button");
|
||||
global.post(global.commentoOrigin + "/api/owner/reset-password", json, function(resp) {
|
||||
global.post(global.origin + "/api/owner/reset-password", json, function(resp) {
|
||||
global.buttonEnable("#reset-button");
|
||||
|
||||
global.textSet("#err", "");
|
||||
@@ -30,8 +31,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
document.location = global.commentoOrigin + "/login?changed=true";
|
||||
document.location = global.origin + "/login?changed=true";
|
||||
});
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
// Get self details.
|
||||
global.selfGet = function(callback) {
|
||||
@@ -7,14 +8,14 @@
|
||||
};
|
||||
|
||||
if (json.ownerToken === undefined) {
|
||||
document.location = global.commentoOrigin + "/login";
|
||||
document.location = global.origin + "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
global.post(global.commentoOrigin + "/api/owner/self", json, function(resp) {
|
||||
global.post(global.origin + "/api/owner/self", json, function(resp) {
|
||||
if (!resp.success || !resp.loggedIn) {
|
||||
global.cookieDelete("commentoOwnerToken");
|
||||
document.location = global.commentoOrigin + "/login";
|
||||
document.location = global.origin + "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,4 +24,4 @@
|
||||
});
|
||||
};
|
||||
|
||||
}(window, document));
|
||||
}(window.commento, document));
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
(function (global, document) {
|
||||
"use strict"
|
||||
|
||||
// Signs up the user and redirects to either the login page or the email
|
||||
// confirmation, depending on whether or not SMTP is configured in the
|
||||
// backend.
|
||||
global.signup = function() {
|
||||
if ($("#password").val() != $("#password2").val()) {
|
||||
if ($("#password").val() !== $("#password2").val()) {
|
||||
global.textSet("#err", "The two passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
var all_ok = unfilledMark(["#email", "#name", "#password", "#password2"], function(el) {
|
||||
var allOk = global.unfilledMark(["#email", "#name", "#password", "#password2"], function(el) {
|
||||
el.css("border-bottom", "1px solid red");
|
||||
});
|
||||
|
||||
if (!all_ok) {
|
||||
if (!allOk) {
|
||||
global.textSet("#err", "Please make sure all fields are filled");
|
||||
return;
|
||||
}
|
||||
@@ -25,7 +26,7 @@
|
||||
};
|
||||
|
||||
global.buttonDisable("#signup-button");
|
||||
post(global.commentoOrigin + "/api/owner/new", json, function(resp) {
|
||||
global.post(global.origin + "/api/owner/new", json, function(resp) {
|
||||
global.buttonEnable("#signup-button")
|
||||
|
||||
if (!resp.success) {
|
||||
@@ -33,11 +34,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.confirmEmail)
|
||||
document.location = global.commentoOrigin + "/confirm-email";
|
||||
else
|
||||
document.location = global.commentoOrigin + "/login?signedUp=true";
|
||||
if (resp.confirmEmail) {
|
||||
document.location = global.origin + "/confirm-email";
|
||||
} else {
|
||||
document.location = global.origin + "/login?signedUp=true";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
// Gets a GET parameter in the current URL.
|
||||
global.paramGet = function(param) {
|
||||
var pageURL = decodeURIComponent(window.location.search.substring(1));
|
||||
var urlVariables = pageURL.split('&');
|
||||
var urlVariables = pageURL.split("&");
|
||||
|
||||
for (var i = 0; i < urlVariables.length; i++) {
|
||||
var paramURL = urlVariables[i].split('=');
|
||||
if (paramURL[0] === param)
|
||||
var paramURL = urlVariables[i].split("=");
|
||||
if (paramURL[0] === param) {
|
||||
return paramURL[1] === undefined ? true : paramURL[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -42,16 +44,16 @@
|
||||
// Given an array of input IDs, this function calls a callback function with
|
||||
// the first unfilled ID.
|
||||
global.unfilledMark = function(fields, callback) {
|
||||
var all_ok = true;
|
||||
var allOk = true;
|
||||
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var el = $(fields[i]);
|
||||
if (el.val() == "") {
|
||||
if (el.val() === "") {
|
||||
callback(el);
|
||||
}
|
||||
}
|
||||
|
||||
return all_ok;
|
||||
return allOk;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +61,9 @@
|
||||
global.cookieGet = function(name) {
|
||||
var c = "; " + document.cookie;
|
||||
var x = c.split("; " + name + "=");
|
||||
if (x.length == 2)
|
||||
if (x.length === 2) {
|
||||
return x.pop().split(";").shift();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -71,13 +74,18 @@
|
||||
date.setTime(date.getTime() + (365*24*60*60*1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
|
||||
document.cookie = name + "=" + value + expires + "; path=/";
|
||||
var cookieString = name + "=" + value + expires + "; path=/";
|
||||
if (/^https:\/\//i.test(origin)) {
|
||||
cookieString += "; secure";
|
||||
}
|
||||
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
|
||||
|
||||
// Deletes a cookie.
|
||||
global.cookieDelete = function(name) {
|
||||
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
}
|
||||
|
||||
|
||||
@@ -86,29 +94,35 @@
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 31536000);
|
||||
|
||||
if (interval > 1)
|
||||
if (interval > 1) {
|
||||
return interval + " years ago";
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
if (interval > 1)
|
||||
if (interval > 1) {
|
||||
return interval + " months ago";
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval > 1)
|
||||
if (interval > 1) {
|
||||
return interval + " days ago";
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval > 1)
|
||||
if (interval > 1) {
|
||||
return interval + " hours ago";
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1)
|
||||
if (interval > 1) {
|
||||
return interval + " minutes ago";
|
||||
}
|
||||
|
||||
if (seconds > 5)
|
||||
if (seconds > 5) {
|
||||
return Math.floor(seconds) + " seconds ago";
|
||||
else
|
||||
} else {
|
||||
return "just now";
|
||||
}
|
||||
}
|
||||
|
||||
} (window, document));
|
||||
} (window.commento, document));
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0">
|
||||
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||
<script src="[[[.CdnPrefix]]]/js/login.js"></script>
|
||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||
<title>Commento: Login</title>
|
||||
@@ -16,9 +17,9 @@
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.loggedInRedirect();
|
||||
window.prefillEmail();
|
||||
window.displayMessages();
|
||||
window.commento.loggedInRedirect();
|
||||
window.commento.prefillEmail();
|
||||
window.commento.displayMessages();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
<div class="err" id="err"></div>
|
||||
<div class="msg" id="msg"></div>
|
||||
|
||||
<button id="button" class="button" onclick="window.login()">Login</button>
|
||||
<button id="button" class="button" onclick="window.commento.login()">Login</button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
</head>
|
||||
|
||||
<script>
|
||||
window.onload = window.logout;
|
||||
window.onload = window.commento.logout;
|
||||
</script>
|
||||
</html>
|
||||
|
||||
33
frontend/package.json
Normal file
33
frontend/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "commento",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@gitlab.com:commento/commento.git",
|
||||
"author": "Adhityaa <c.adhityaa@gmail.com> Anton Linevych anton@linevich.net",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"chartist": "0.11.0",
|
||||
"fixmyjs": "2.0.0",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-clean-css": "3.9.4",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-eslint": "5.0.0",
|
||||
"gulp-html-minifier": "0.1.8",
|
||||
"gulp-rename": "1.3.0",
|
||||
"gulp-sass": "4.0.1",
|
||||
"gulp-sourcemaps": "2.6.4",
|
||||
"gulp-uglify": "3.0.0",
|
||||
"highlightjs": "9.10.0",
|
||||
"html-minifier": "3.5.7",
|
||||
"jquery": "3.2.1",
|
||||
"natives": "^1.1.6",
|
||||
"normalize-scss": "7.0.1",
|
||||
"sass": "1.5.1",
|
||||
"uglify-js": "3.4.1",
|
||||
"vue": "2.5.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "^5.10.0"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0">
|
||||
<script src="[[[.CdnPrefix]]]/js/jquery.js"></script>
|
||||
<script src="[[[.CdnPrefix]]]/js/reset.js"></script>
|
||||
<link rel="icon" href="[[[.CdnPrefix]]]/images/120x120.png">
|
||||
<link rel="stylesheet" href="[[[.CdnPrefix]]]/css/auth.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Source+Sans+Pro:200,300,400,700" rel="stylesheet">
|
||||
<title>Commento: Reset your Password</title>
|
||||
@@ -32,7 +33,7 @@
|
||||
|
||||
<div class="err" id="err"></div>
|
||||
<div class="msg" id="msg"></div>
|
||||
<button id="reset-button" class="button" onclick="resetPassword()">Reset Password</button>
|
||||
<button id="reset-button" class="button" onclick="window.commento.resetPassword()">Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user