├── .eslintignore ├── src ├── utils │ ├── index.ts │ ├── email.ts │ └── name.ts ├── types │ ├── index.ts │ ├── index.d.ts │ ├── user.ts │ ├── site.ts │ └── comment.ts ├── api │ ├── reactions │ │ ├── index.ts │ │ ├── model.ts │ │ ├── helper.ts │ │ └── controller.ts │ ├── users │ │ ├── index.ts │ │ ├── model.ts │ │ └── controller.ts │ ├── sites │ │ ├── index.ts │ │ ├── model.ts │ │ └── controller.ts │ ├── comments │ │ ├── index.ts │ │ ├── helper.ts │ │ ├── model.ts │ │ └── controller.ts │ └── index.ts ├── services │ ├── mongoose │ │ └── index.ts │ ├── metadata │ │ └── index.ts │ ├── express │ │ └── index.ts │ └── mailer │ │ ├── index.ts │ │ └── template.ts ├── migrations │ └── index.ts └── app.ts ├── .babelrc ├── index.ts ├── .prettierrc ├── Makefile ├── docker-compose.dev.yml ├── .gitignore ├── Dockerfile ├── .env.example ├── .env.docker ├── ecosystem.config.js ├── docker-compose.yml ├── .eslintrc.js ├── .github └── FUNDING.yml ├── README.md ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | .eslintrc.js 3 | .prettierrc.js -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './email'; 2 | export * from './name'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comment'; 2 | export * from './user'; 3 | export * from './site'; 4 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | namespace Express { 2 | export interface Request { 3 | user: any; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/email.ts: -------------------------------------------------------------------------------- 1 | export const cleanEmail = (string: string) => { 2 | return String(string).trim(); 3 | }; 4 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv-safe'; 2 | config({ allowEmptyValues: true }); 3 | 4 | import './src/app'; 5 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export type TUser = { 2 | name: string; 3 | id: string; 4 | email: string; 5 | role: number; 6 | isVerified: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/site.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose'; 2 | 3 | export type TSite = { 4 | userId: Schema.Types.ObjectId; 5 | domain: string; 6 | verified: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "printWidth": 90, 5 | "semi": true, 6 | "trailingComma": "none", 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/name.ts: -------------------------------------------------------------------------------- 1 | export const cleanName = (input: string) => { 2 | const regex = /[\p{L}\s\d\._]/gu; 3 | 4 | const cleaned = input.trim().match(regex)?.join('') || ''; 5 | 6 | return cleaned; 7 | }; 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t zoomment-server . 3 | 4 | up: 5 | docker-compose up -d 6 | 7 | 8 | down: 9 | docker-compose down 10 | 11 | dev: 12 | docker-compose -f docker-compose.dev.yml up 13 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | restart: always 7 | volumes: 8 | - './mongo-data:/data/db' 9 | ports: 10 | - 27017:27017 11 | -------------------------------------------------------------------------------- /src/api/reactions/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { list, add } from './controller'; 3 | 4 | const router = Router(); 5 | 6 | router.post('/', add); 7 | router.get('/', list); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | npm_debug.log 3 | logs 4 | /coverage 5 | /.idea 6 | /lib/ 7 | .nyc_output 8 | .env 9 | dist 10 | .idea 11 | jsconfig.json 12 | .vscode 13 | yarn.lock 14 | .DS_Store 15 | *package-lock.json 16 | 17 | mongo-data 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ["package.json", "package-lock.json*", "./"] 6 | 7 | RUN npm install --production 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | CMD ["node", "./dist/index.js"] 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost:27017/zoomment 2 | PORT=9547 3 | JWT_SECRET= 4 | 5 | API_URL=http://localhost:9547/api 6 | DASHBOARD_URL=http://localhost:3000 7 | 8 | BRAND_NAME="Zoomment.com" 9 | ADMIN_EMAIL_ADDR= 10 | 11 | BOT_EMAIL_HOST= 12 | BOT_EMAIL_PORT= 13 | BOT_EMAIL_ADDR= 14 | BOT_EMAIL_PASS= -------------------------------------------------------------------------------- /.env.docker: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://mongo:27017/zoomment 2 | PORT=9547 3 | JWT_SECRET=docker-secret 4 | 5 | API_URL=http://localhost:9547/api 6 | DASHBOARD_URL=http://localhost:3000 7 | 8 | BRAND_NAME="Zoomment.com" 9 | ADMIN_EMAIL_ADDR= 10 | 11 | BOT_EMAIL_HOST= 12 | BOT_EMAIL_PORT= 13 | BOT_EMAIL_ADDR= 14 | BOT_EMAIL_PASS= -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'ZOOMMENT', 3 | script: './dist/index.js', 4 | error_file: './logs/err.log', 5 | out_file: './logs/out.log', 6 | max_memory_restart: '4G', 7 | exec_mode: 'cluster', 8 | autorestart: true, 9 | time: true, 10 | logDateFormat: 'YYYY-MM-DD HH:mm Z', 11 | watch: false 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { auth, profile, remove } from './controller'; 3 | import { access } from '@/services/express'; 4 | 5 | const router = Router(); 6 | 7 | router.post('/auth', auth); 8 | router.get('/profile', access(), profile); 9 | router.delete('/', access(), remove); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/api/sites/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { add, list, remove } from './controller'; 3 | import { access } from '@/services/express'; 4 | 5 | const router = Router(); 6 | 7 | router.post('/', access('admin'), add); 8 | router.get('/', access('admin'), list); 9 | router.delete('/:id', access('admin'), remove); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/services/mongoose/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | mongoose.Promise = Promise; 4 | 5 | mongoose.connection.on('error', err => { 6 | console.error('MongoDB connection error: ' + err); 7 | process.exit(-1); 8 | }); 9 | 10 | mongoose.connection.on('connected', err => { 11 | console.log('MongoDB connected'); 12 | }); 13 | 14 | export default mongoose; 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | restart: always 7 | volumes: 8 | - './mongo-data:/data/db' 9 | 10 | zoomment-server: 11 | image: zoomment-server 12 | restart: always 13 | volumes: 14 | - ./.env.docker:/app/.env:ro 15 | ports: 16 | - 9547:9547 17 | depends_on: 18 | - mongo 19 | -------------------------------------------------------------------------------- /src/api/comments/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { add, list, remove, listBySiteId } from './controller'; 3 | import { access } from '@/services/express'; 4 | 5 | const router = Router(); 6 | 7 | router.get('/', list); 8 | router.post('/', add); 9 | router.delete('/:id', remove); 10 | 11 | router.get('/sites/:siteId', access('admin'), listBySiteId); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import comments from './comments'; 3 | import reactions from './reactions'; 4 | import users from './users'; 5 | import sites from './sites'; 6 | 7 | const router = Router(); 8 | 9 | router.use('/comments', comments); 10 | router.use('/reactions', reactions); 11 | router.use('/users', users); 12 | router.use('/sites', sites); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/api/sites/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { TSite } from '@/types'; 3 | 4 | const schema = new Schema( 5 | { 6 | domain: { 7 | type: String, 8 | required: true 9 | }, 10 | verified: { 11 | type: Boolean, 12 | default: false 13 | }, 14 | userId: { 15 | type: Schema.Types.ObjectId, 16 | required: true 17 | } 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ); 23 | 24 | const model = mongoose.model('Site', schema); 25 | export default model; 26 | -------------------------------------------------------------------------------- /src/api/reactions/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | 3 | const schema = new Schema( 4 | { 5 | fingerprint: { 6 | type: String, 7 | required: true 8 | }, 9 | domain: { 10 | type: String, 11 | required: true 12 | }, 13 | pageId: { 14 | type: String, 15 | required: true 16 | }, 17 | reaction: { 18 | type: String 19 | } 20 | }, 21 | { 22 | timestamps: true 23 | } 24 | ); 25 | 26 | const model = mongoose.model('Reaction', schema); 27 | 28 | export default model; 29 | -------------------------------------------------------------------------------- /src/types/comment.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document } from 'mongoose'; 2 | 3 | export type TComment = Document & { 4 | id: string; 5 | _id: Schema.Types.ObjectId; 6 | // owner field deprecated 7 | owner: { 8 | gravatar: string; 9 | email: string; 10 | name: string; 11 | }; 12 | isVerified: boolean; 13 | domain: string; 14 | gravatar: string; 15 | author: string; 16 | email: string; 17 | parentId: Schema.Types.ObjectId; 18 | pageUrl: string; 19 | pageId: string; 20 | body: string; 21 | secret: string; 22 | createdAt: Date; 23 | }; 24 | -------------------------------------------------------------------------------- /src/services/metadata/index.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import axios from 'axios'; 3 | 4 | export async function fetchSiteToken(url: string) { 5 | try { 6 | const response = await axios.get(url, { responseType: 'text' }); 7 | const $ = cheerio.load(response.data); 8 | 9 | let token: string | undefined; 10 | $('meta[name^="zoomment"]').each((index, element) => { 11 | token = $(element).attr('content'); 12 | }); 13 | 14 | return token; 15 | } catch (error: any) { 16 | console.error('Error fetching or parsing the HTML:', error.message); 17 | return undefined; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/api/users/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { TUser } from '@/types'; 3 | 4 | const schema = new Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: false 9 | }, 10 | email: { 11 | type: String, 12 | required: true 13 | }, 14 | role: { 15 | type: Number, 16 | required: true, 17 | default: 1 18 | }, 19 | isVerified: { 20 | type: Boolean, 21 | default: false 22 | } 23 | }, 24 | { 25 | timestamps: true 26 | } 27 | ); 28 | 29 | const model = mongoose.model('User', schema); 30 | export default model; 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: ['plugin:prettier/recommended', 'prettier', 'eslint:recommended'], 4 | plugins: ['@typescript-eslint'], 5 | parserOptions: { 6 | ecmaVersion: 2022, 7 | sourceType: 'module', 8 | project: 'tsconfig.json' 9 | }, 10 | env: { 11 | es6: true, 12 | node: true 13 | }, 14 | rules: { 15 | 'no-var': 'error', 16 | semi: 'error', 17 | indent: ['error', 2, { SwitchCase: 1 }], 18 | 'no-multi-spaces': 'error', 19 | 'space-in-parens': 'error', 20 | 'no-multiple-empty-lines': 'error', 21 | 'prefer-const': 'error' 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zoomment 4 | patreon: zoomment 5 | open_collective: zoomment 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/api/reactions/helper.ts: -------------------------------------------------------------------------------- 1 | import Reaction from './model'; 2 | 3 | export const getPageData = async ({ 4 | fingerprint, 5 | pageId 6 | }: { 7 | fingerprint: string; 8 | pageId: string; 9 | }) => { 10 | const userReactionPromise = Reaction.findOne({ 11 | fingerprint, 12 | pageId 13 | }).select('reaction -_id'); 14 | 15 | const aggregationPromise = Reaction.aggregate() 16 | .match({ 17 | pageId 18 | }) 19 | .group({ 20 | _id: '$reaction', 21 | count: { $count: {} } 22 | }); 23 | 24 | const [aggregation, userReaction] = await Promise.all([ 25 | aggregationPromise, 26 | userReactionPromise 27 | ]); 28 | 29 | return { aggregation, userReaction }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/migrations/index.ts: -------------------------------------------------------------------------------- 1 | import Comment from '@/api/comments/model'; 2 | import Reactions from '@/api/reactions/model'; 3 | 4 | export const migrate = async () => { 5 | const comments = await Comment.find({}); 6 | 7 | await comments.reduce((promise, comment) => { 8 | comment.email = comment.owner.email; 9 | comment.author = comment.owner.name; 10 | comment.gravatar = comment.owner.gravatar; 11 | 12 | const url = new URL('https://' + comment.pageId); 13 | comment.domain = url.hostname; 14 | return promise.then(() => comment.save()); 15 | }, Promise.resolve({})); 16 | 17 | const reactions = await Reactions.find({}); 18 | 19 | await reactions.reduce((promise, reaction) => { 20 | const url = new URL('https://' + reaction.pageId); 21 | reaction.domain = url.hostname; 22 | return promise.then(() => reaction.save()); 23 | }, Promise.resolve({})); 24 | }; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [Node.js](https://nodejs.org/en/) 4 | - [MongoDB](https://docs.mongodb.com/manual/administration/install-community/) 5 | 6 | Clone the repository and install dependencies: 7 | 8 | ``` 9 | $ git clone https://github.com/zoomment/zoomment-server.git 10 | $ cd zoomment-server 11 | $ npm install 12 | ``` 13 | 14 | Copy and edit the `env` file: 15 | 16 | ``` 17 | $ cp .env.example .env 18 | $ vim .env 19 | ``` 20 | 21 | Run development: 22 | 23 | ``` 24 | $ npm run dev 25 | ``` 26 | 27 | Run production: 28 | 29 | ``` 30 | $ npm start 31 | ``` 32 | 33 | Stop production: 34 | 35 | ``` 36 | $ npm run kill 37 | ``` 38 | 39 | ## Docker 40 | 41 | modify `.env.docker` to run with docker. 42 | 43 | ### run mongo with docker 44 | 45 | ``` 46 | make dev 47 | ``` 48 | 49 | ### build the docker image 50 | 51 | ``` 52 | make build 53 | ``` 54 | 55 | ### start/stop system 56 | 57 | To start 58 | 59 | ``` 60 | make up 61 | ``` 62 | 63 | To stop 64 | 65 | ``` 66 | make down 67 | ``` 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tigran Simonyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/api/sites/controller.ts: -------------------------------------------------------------------------------- 1 | import { fetchSiteToken } from '@/services/metadata'; 2 | import { asyncRoute } from '@/services/express'; 3 | import Site from './model'; 4 | 5 | export const add = asyncRoute(async (req, res) => { 6 | const websiteUrl = new URL(req.body.url); 7 | const domain = websiteUrl.hostname; 8 | const token = await fetchSiteToken(websiteUrl.href); 9 | 10 | if (token !== req.user.id) { 11 | res.status(404).json({ message: 'Meta tag not found' }); 12 | return; 13 | } 14 | 15 | const exists = await Site.findOne({ domain }); 16 | 17 | if (exists) { 18 | res.status(401).json({ message: 'Website already exists' }); 19 | return; 20 | } 21 | 22 | const site = await Site.create({ 23 | verified: true, 24 | userId: req.user.id, 25 | domain: websiteUrl.hostname 26 | }); 27 | 28 | res.json(site); 29 | }); 30 | 31 | export const list = asyncRoute(async (req, res) => { 32 | const sites = await Site.find({ userId: req.user.id }).sort({ _id: 'desc' }); 33 | 34 | res.json(sites); 35 | }); 36 | 37 | export const remove = asyncRoute(async (req, res) => { 38 | const { deletedCount } = await Site.deleteOne({ 39 | userId: req.user.id, 40 | _id: req.params.id 41 | }); 42 | 43 | if (deletedCount > 0) { 44 | res.status(200).json({ _id: req.params.id }); 45 | } else { 46 | res.status(404).json({ message: 'Site not found' }); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/api/users/controller.ts: -------------------------------------------------------------------------------- 1 | import User from './model'; 2 | import Sites from '../sites/model'; 3 | import jwt from 'jsonwebtoken'; 4 | import { asyncRoute } from '@/services/express'; 5 | import { sendMagicLink } from '@/services/mailer'; 6 | import { cleanEmail } from '@/utils'; 7 | 8 | export const auth = asyncRoute(async (req, res) => { 9 | // TODO add validation 10 | const email = cleanEmail(req.body.email); 11 | 12 | let user = await User.findOne({ email }); 13 | 14 | if (!user) { 15 | user = await User.create({ email }); 16 | } 17 | 18 | const token = jwt.sign( 19 | { id: user.id, email: user.email, name: user.name }, 20 | process.env.JWT_SECRET as string, 21 | { expiresIn: '1y', algorithm: 'HS256' } 22 | ); 23 | 24 | await sendMagicLink(user.email, token); 25 | 26 | res.json({}); 27 | }); 28 | 29 | export const profile = asyncRoute(async (req, res) => { 30 | const user = req.user; 31 | 32 | res.json({ 33 | name: user.name, 34 | email: user.email, 35 | id: user.id 36 | }); 37 | }); 38 | 39 | export const remove = asyncRoute(async (req, res) => { 40 | const userId = req.user.id; 41 | const { deletedCount } = await User.deleteOne({ _id: userId }); 42 | 43 | await Sites.deleteMany({ userId }); 44 | 45 | if (deletedCount > 0) { 46 | res.status(200).json({ _id: userId }); 47 | } else { 48 | res.status(404).json({ message: 'Account not found' }); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import mongoose from './services/mongoose'; 2 | import { auth } from './services/express'; 3 | import cookieParser from 'cookie-parser'; 4 | import bodyParser from 'body-parser'; 5 | import express, { ErrorRequestHandler } from 'express'; 6 | import http from 'http'; 7 | import cors from 'cors'; 8 | import api from './api'; 9 | import { migrate } from './migrations'; 10 | 11 | const app = express(); 12 | 13 | mongoose.connect(process.env.MONGODB_URI || ''); 14 | 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); 17 | app.use(cookieParser()); 18 | app.use(cors()); 19 | app.use(auth()); 20 | 21 | app.use('/api', api); 22 | 23 | // eslint-disable-next-line no-unused-vars 24 | app.use(function (err, req, res, next) { 25 | console.log(err); 26 | const errorMessage = err.message || 'Something is wrong!'; 27 | const errorStatue = err.status || 400; 28 | res.status(errorStatue).send({ message: errorMessage }); 29 | }); 30 | 31 | const server = http.createServer(app); 32 | setImmediate(() => { 33 | server.listen(process.env.PORT, () => { 34 | console.log('Express server listening on http://localhost:%s/', process.env.PORT); 35 | }); 36 | }); 37 | 38 | if (process.env.MIGRATION) { 39 | migrate() 40 | .then(() => { 41 | throw new Error('Migration complied'); 42 | }) 43 | .catch(e => { 44 | throw new Error(e); 45 | }); 46 | } 47 | 48 | export default app; 49 | -------------------------------------------------------------------------------- /src/api/comments/helper.ts: -------------------------------------------------------------------------------- 1 | import { TComment, TUser } from '@/types'; 2 | import { cleanEmail, cleanName } from '@/utils'; 3 | import crypto from 'crypto'; 4 | 5 | export const createCommentData = (obj: any, user?: TUser) => { 6 | const url = new URL(obj.pageUrl); 7 | const email = cleanEmail(obj.email || obj?.owner.email || ''); 8 | const author = cleanName(obj.author || obj?.owner.name || ''); 9 | const parentId = obj.parentId; 10 | const domain = url.hostname; 11 | const gravatar = crypto.createHash('md5').update(email).digest('hex'); 12 | 13 | const isVerified = user && user.email === email; 14 | 15 | const data = { 16 | body: obj.body, 17 | // owner field deprecated 18 | owner: { name: author, email, gravatar }, 19 | parentId, 20 | gravatar, 21 | author, 22 | email, 23 | domain, 24 | isVerified, 25 | pageUrl: url.href, 26 | pageId: obj.pageId, 27 | secret: crypto.randomBytes(20).toString('hex') 28 | }; 29 | 30 | return data; 31 | }; 32 | 33 | export const getCommentPublicData = (comment: TComment, user?: TUser) => { 34 | const obj = comment.toObject(); 35 | const isOwn = user && user.email === comment.email; 36 | 37 | return { 38 | _id: obj._id, 39 | owner: { 40 | name: obj.author, 41 | gravatar: obj.gravatar 42 | }, 43 | isOwn, 44 | body: obj.body, 45 | author: obj.author, 46 | gravatar: obj.gravatar, 47 | parentId: obj.parentId, 48 | createdAt: obj.createdAt, 49 | isVerified: obj.isVerified 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/api/comments/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { TComment } from '@/types'; 3 | 4 | const schema = new Schema( 5 | { 6 | // owner field deprecated 7 | owner: { 8 | ip: { 9 | type: String, 10 | required: false 11 | }, 12 | gravatar: { 13 | type: String, 14 | required: false 15 | }, 16 | email: { 17 | type: String, 18 | required: true 19 | }, 20 | name: { 21 | type: String, 22 | required: true 23 | } 24 | }, 25 | parentId: { 26 | type: String, 27 | required: false, 28 | ref: 'Comment' 29 | }, 30 | isVerified: { 31 | type: Boolean, 32 | default: false 33 | }, 34 | gravatar: { 35 | type: String, 36 | required: false 37 | }, 38 | domain: { 39 | type: String, 40 | required: true 41 | }, 42 | author: { 43 | type: String, 44 | required: true 45 | }, 46 | email: { 47 | type: String, 48 | required: true 49 | }, 50 | pageUrl: { 51 | type: String, 52 | required: true 53 | }, 54 | pageId: { 55 | type: String, 56 | required: true 57 | }, 58 | body: { 59 | type: String, 60 | required: true 61 | }, 62 | secret: { 63 | type: String, 64 | required: true 65 | } 66 | }, 67 | { 68 | timestamps: true 69 | } 70 | ); 71 | 72 | const model = mongoose.model('Comment', schema); 73 | export default model; 74 | -------------------------------------------------------------------------------- /src/services/express/index.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import User from '@/api/users/model'; 4 | 5 | export const asyncRoute = 6 | (fn: RequestHandler): RequestHandler => 7 | (req, res, next) => { 8 | Promise.resolve(fn(req, res, next)).catch(next); 9 | }; 10 | 11 | export const auth = (): RequestHandler => { 12 | return async (req, res, next) => { 13 | const headerToken = req.headers.token; 14 | 15 | if (!headerToken || Array.isArray(headerToken)) { 16 | next(); 17 | return; 18 | } 19 | 20 | try { 21 | const decoded = jwt.verify(headerToken, process.env.JWT_SECRET as string) as { 22 | id: string; 23 | }; 24 | 25 | const user = await User.findById(decoded.id); 26 | 27 | if (user && !user.isVerified) { 28 | user.isVerified = true; 29 | await user.save(); 30 | } 31 | 32 | req.user = user; 33 | next(); 34 | } catch (e) { 35 | next(); 36 | } 37 | }; 38 | }; 39 | 40 | export const access = (level?: 'admin' | 'superAdmin'): RequestHandler => { 41 | return async (req, res, next) => { 42 | if (!req.user) { 43 | res.status(403).json({ message: 'Forbidden' }); 44 | return; 45 | } 46 | 47 | if (!level) { 48 | next(); 49 | } else if (level && req.user.role === 2) { 50 | next(); 51 | } else if (level === 'admin' && req.user.role === 1) { 52 | next(); 53 | } else { 54 | res.status(403).json({ message: 'Forbidden' }); 55 | } 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/reactions/controller.ts: -------------------------------------------------------------------------------- 1 | import Reaction from './model'; 2 | import { getPageData } from './helper'; 3 | import { asyncRoute } from '@/services/express'; 4 | 5 | export const add = asyncRoute(async (req, res) => { 6 | // TODO add validation 7 | const fingerprint: string = req.headers.fingerprint as string; 8 | const pageId = req.body.pageId || req.query.pageId; 9 | const reaction = req.body.reaction.slice(0, 20); // Just in case, limit characters by 20 10 | 11 | // TODO remove after migration 12 | const url = new URL('https://' + pageId); 13 | 14 | if (!fingerprint) { 15 | res.status(500).send('Fingerprint required for reacting.'); 16 | return; 17 | } 18 | 19 | const searchCondition = { pageId, fingerprint, domain: url.hostname }; 20 | const recordInDB = await Reaction.findOne(searchCondition); 21 | 22 | if (!recordInDB) { 23 | await Reaction.create({ ...searchCondition, reaction }); 24 | } else { 25 | if (recordInDB.reaction === reaction) { 26 | await Reaction.deleteOne({ _id: recordInDB.id }); 27 | } else { 28 | recordInDB.reaction = reaction; 29 | await recordInDB.save(); 30 | } 31 | } 32 | 33 | const reactions = await getPageData(searchCondition); 34 | 35 | res.status(200).json(reactions); 36 | }); 37 | 38 | export const list = asyncRoute(async (req, res) => { 39 | // TODO add validation 40 | const fingerprint: string = req.headers.fingerprint as string; 41 | const pageId = req.body.pageId || req.query.pageId; 42 | 43 | const reactions = await getPageData({ pageId, fingerprint }); 44 | 45 | res.status(200).json(reactions); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zoomment-server", 3 | "version": "1.0.0", 4 | "description": "Open Source Self-Hosted Comment System", 5 | "main": "./index.ts", 6 | "scripts": { 7 | "dev": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node -r tsconfig-paths/register ./index.ts'", 8 | "start": "npm run build && pm2 start ./dist/ecosystem.config.js", 9 | "start:only": "pm2 start ./dist/ecosystem.config.js", 10 | "restart": "npm run build && pm2 restart ZOOMMENT", 11 | "kill": "pm2 delete ZOOMMENT", 12 | "build": "tsc && tsc-alias", 13 | "lint": "eslint src/**/*.ts --fix", 14 | "prettier": "npx prettier ./ --check", 15 | "prettier:fix": "npm run prettier -- --write" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged" 20 | } 21 | }, 22 | "lint-staged": { 23 | "*.ts": [ 24 | "eslint --fix", 25 | "prettier --write" 26 | ], 27 | "*.js": [ 28 | "eslint --fix", 29 | "prettier --write" 30 | ] 31 | }, 32 | "author": "Tigran Simonyan", 33 | "license": "ISC", 34 | "dependencies": { 35 | "axios": "^1.6.7", 36 | "body-parser": "^1.19.0", 37 | "cheerio": "^1.0.0-rc.12", 38 | "cookie-parser": "^1.4.5", 39 | "cors": "^2.8.5", 40 | "dayjs": "^1.11.10", 41 | "dotenv-safe": "^8.2.0", 42 | "express": "^4.17.1", 43 | "jsonwebtoken": "^9.0.2", 44 | "mongoose": "^8.8.0", 45 | "nodemailer": "^6.4.15" 46 | }, 47 | "devDependencies": { 48 | "@types/body-parser": "^1.19.5", 49 | "@types/cookie-parser": "^1.4.7", 50 | "@types/cors": "^2.8.17", 51 | "@types/dotenv-safe": "^8.1.6", 52 | "@types/jsonwebtoken": "^9.0.6", 53 | "@types/nodemailer": "^6.4.14", 54 | "@typescript-eslint/eslint-plugin": "^7.1.0", 55 | "@typescript-eslint/parser": "^7.1.0", 56 | "eslint": "^8.57.0", 57 | "eslint-config-prettier": "^9.1.0", 58 | "eslint-plugin-prettier": "^5.1.3", 59 | "husky": "^9.0.11", 60 | "nodemon": "^3.1.9", 61 | "pm2": "^5.4.3", 62 | "prettier": "^3.2.5", 63 | "ts-node": "^10.9.2", 64 | "tsc-alias": "^1.8.8", 65 | "tsconfig-paths": "^4.2.0", 66 | "typescript": "^5.3.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/services/mailer/index.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | const dayjs = require('dayjs'); 3 | import { TComment } from '@/types'; 4 | import { generateTemplate } from './template'; 5 | 6 | const transporter = nodemailer.createTransport({ 7 | port: parseInt(`${process.env.BOT_EMAIL_PORT}`) || 465, 8 | host: process.env.BOT_EMAIL_HOST || 'smtp.gmail.com', 9 | secure: true, 10 | auth: { 11 | user: process.env.BOT_EMAIL_ADDR, 12 | pass: process.env.BOT_EMAIL_PASS 13 | } 14 | }); 15 | 16 | export async function sendCommentNotification(userEmail: string, comment: TComment) { 17 | const date = dayjs(comment.createdAt).format('DD MMM YYYY - HH:mm'); 18 | const template = generateTemplate({ 19 | introduction: ` 20 |

You have a new comment!

21 |
22 |
User: ${comment.author}
23 |
Date: ${date}
24 |
Page: ${comment.pageUrl}
25 |
Comment: ${comment.body}
26 |
27 | `, 28 | buttonUrl: `${process.env.DASHBOARD_URL}/auth`, 29 | buttonText: 'Sign in to manage comments', 30 | epilogue: '' 31 | }); 32 | 33 | await transporter.sendMail({ 34 | from: `"${process.env.BRAND_NAME}" <${process.env.BOT_EMAIL_ADDR}>`, 35 | to: userEmail, 36 | subject: 'You have a new comment!', 37 | html: template 38 | }); 39 | } 40 | 41 | export async function sendMagicLink(userEmail: string, authToken: string) { 42 | const template = generateTemplate({ 43 | introduction: `Click the link below to sign in to your ${process.env.BRAND_NAME} dashboard.`, 44 | buttonUrl: `${process.env.DASHBOARD_URL}/dashboard?zoommentToken=${authToken}`, 45 | buttonText: `Sign in to ${process.env.BRAND_NAME}`, 46 | epilogue: 'If you did not make this request, you can safely ignore this email.' 47 | }); 48 | 49 | return await transporter.sendMail({ 50 | from: `"${process.env.BRAND_NAME}" <${process.env.BOT_EMAIL_ADDR}>`, 51 | to: userEmail, 52 | subject: `Sign in to ${process.env.BRAND_NAME}`, 53 | html: template 54 | }); 55 | } 56 | 57 | export async function sendEmailVerificationLink({ 58 | userEmail, 59 | authToken, 60 | pageUrl 61 | }: { 62 | userEmail: string; 63 | authToken: string; 64 | pageUrl: string; 65 | }) { 66 | const template = generateTemplate({ 67 | introduction: `Please confirm your email address to be able to manage your comment.`, 68 | buttonUrl: `${pageUrl}?zoommentToken=${authToken}`, 69 | buttonText: `Confirm`, 70 | epilogue: 'If you did not make this request, you can safely ignore this email.' 71 | }); 72 | 73 | await transporter.sendMail({ 74 | from: `"${process.env.BRAND_NAME}" <${process.env.BOT_EMAIL_ADDR}>`, 75 | to: userEmail, 76 | subject: 'You have added comment!', 77 | html: template 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/api/comments/controller.ts: -------------------------------------------------------------------------------- 1 | import Comment from './model'; 2 | import Site from '@/api/sites/model'; 3 | import User from '@/api/users/model'; 4 | import * as mailer from '@/services/mailer'; 5 | import { asyncRoute } from '@/services/express'; 6 | import { cleanEmail } from '@/utils'; 7 | import jwt from 'jsonwebtoken'; 8 | import { getCommentPublicData, createCommentData } from './helper'; 9 | 10 | export const add = asyncRoute(async (req, res) => { 11 | //TODO add validation 12 | const data = createCommentData(req.body, req.user); 13 | 14 | const comment = await Comment.create(data); 15 | 16 | res.json(comment); 17 | 18 | // Send confirmation email to guest 19 | if (!req.user) { 20 | let user = await User.findOne({ email: data.email }); 21 | 22 | if (!user) { 23 | user = await User.create({ email: data.email, name: data.author }); 24 | } 25 | 26 | const token = jwt.sign( 27 | { id: user.id, email: data.email, name: data.author }, 28 | process.env.JWT_SECRET as string, 29 | { expiresIn: '1y', algorithm: 'HS256' } 30 | ); 31 | 32 | mailer.sendEmailVerificationLink({ 33 | userEmail: data.email, 34 | authToken: token, 35 | pageUrl: data.pageUrl 36 | }); 37 | } 38 | 39 | // Send an email notification to the site owner about a new comment 40 | const site = await Site.findOne({ domain: data.domain }); 41 | 42 | if (!site) return; 43 | 44 | const siteOwner = await User.findById(site.userId); 45 | 46 | if (siteOwner && siteOwner.email !== data.email) { 47 | mailer.sendCommentNotification(siteOwner.email, comment); 48 | } 49 | }); 50 | 51 | export const list = asyncRoute(async (req, res) => { 52 | const query: any = { 53 | parentId: null 54 | }; 55 | 56 | if (req.query.pageId) { 57 | query.pageId = req.query.pageId; 58 | } else if (req.query.domain) { 59 | query.domain = req.query.domain; 60 | } else { 61 | res.status(400).json({ message: 'Bad request' }); 62 | return; 63 | } 64 | 65 | const comments = await Comment.find(query).sort({ createdAt: 'asc' }); 66 | 67 | const commentsWithReplies = await Promise.all( 68 | comments.map(comment => 69 | Comment.find({ parentId: comment.id }) 70 | .sort({ createdAt: 'asc' }) 71 | .then(replies => { 72 | return { 73 | ...getCommentPublicData(comment, req.user), 74 | replies: replies.map(reply => getCommentPublicData(reply, req.user)) 75 | }; 76 | }) 77 | ) 78 | ); 79 | 80 | res.json(commentsWithReplies); 81 | }); 82 | 83 | export const remove = asyncRoute(async (req, res) => { 84 | // TODO add separate route for websites admin 85 | const query: any = { 86 | _id: req.params.id 87 | }; 88 | 89 | if (req.query.secret) { 90 | query.secret = req.query.secret; 91 | } else if (req.user) { 92 | query.email = cleanEmail(req.user.email); 93 | } else { 94 | res.status(403).json({ message: 'Forbidden' }); 95 | return; 96 | } 97 | 98 | const { deletedCount } = await Comment.deleteOne(query); 99 | 100 | if (deletedCount > 0) { 101 | res.status(200).json({ _id: req.params.id }); 102 | } else { 103 | res.status(404).json({ message: 'Comment not found' }); 104 | } 105 | }); 106 | 107 | export const listBySiteId = asyncRoute(async (req, res) => { 108 | const siteId = req.params.siteId; 109 | const site = await Site.findById(siteId); 110 | 111 | if (!site || String(site.userId) !== String(req.user.id)) { 112 | res.status(404).json({ message: 'Site not found' }); 113 | return; 114 | } 115 | 116 | const comments = await Comment.find({ domain: site.domain }).sort({ 117 | createdAt: 'desc' 118 | }); 119 | 120 | res.status(200).json(comments); 121 | }); 122 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | zoomment.com@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/services/mailer/template.ts: -------------------------------------------------------------------------------- 1 | type Props = { 2 | introduction: string; 3 | buttonText: string; 4 | buttonUrl: string; 5 | epilogue: string; 6 | }; 7 | 8 | export const generateTemplate = (props: Props) => { 9 | const footer = `${process.env.BRAND_NAME} | ${process.env.ADMIN_EMAIL_ADDR}`; 10 | 11 | return ` 12 | 13 | 14 | 15 | 16 | 17 | Simple Transactional Email 18 | 302 | 303 | 304 | 305 | 306 | 307 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | `; 377 | }; 378 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./" /* Specify the root folder within your source files. */, 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, 32 | "paths": { 33 | "@/*": ["src/*"] 34 | } /* Specify a set of entries that re-map imports to additional lookup locations. */, 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | "types": [ 38 | "./src/types" 39 | ] /* Specify type package names to be included without being referenced in a source file. */, 40 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 41 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 42 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 43 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 44 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 45 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 46 | // "resolveJsonModule": true, /* Enable importing .json files. */ 47 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 48 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 49 | 50 | /* JavaScript Support */ 51 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 52 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 53 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 54 | 55 | /* Emit */ 56 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 57 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 58 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 59 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 62 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 63 | // "removeComments": true, /* Disable emitting comments. */ 64 | // "noEmit": true, /* Disable emitting files from a compilation. */ 65 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 66 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 67 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 68 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 71 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 72 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 73 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 74 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 75 | "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */, 76 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 77 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 78 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 79 | 80 | /* Interop Constraints */ 81 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 82 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 83 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 84 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 85 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 86 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 87 | 88 | /* Type Checking */ 89 | "strict": true /* Enable all strict type-checking options. */, 90 | // "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 91 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 92 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 93 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 94 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 95 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 96 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 97 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 98 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 99 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 100 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 101 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 102 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 103 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 104 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 105 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 106 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 107 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 108 | 109 | /* Completeness */ 110 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 111 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 112 | } 113 | } 114 | --------------------------------------------------------------------------------