migration from github
This commit is contained in:
parent
d226eedb4b
commit
4feff06a2b
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/env", {
|
||||||
|
useBuiltIns: "usage",
|
||||||
|
corejs: 3,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/typescript",
|
||||||
|
"@babel/react",
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/proposal-class-properties",
|
||||||
|
"@babel/proposal-object-rest-spread",
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
module.exports = {
|
||||||
|
'env': {
|
||||||
|
'browser': true,
|
||||||
|
'es2021': true,
|
||||||
|
'node': true,
|
||||||
|
},
|
||||||
|
'extends': [
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'google',
|
||||||
|
],
|
||||||
|
'parser': '@typescript-eslint/parser',
|
||||||
|
'parserOptions': {
|
||||||
|
'ecmaFeatures': {
|
||||||
|
'jsx': true,
|
||||||
|
},
|
||||||
|
'ecmaVersion': 12,
|
||||||
|
'sourceType': 'module',
|
||||||
|
},
|
||||||
|
'plugins': [
|
||||||
|
'react',
|
||||||
|
'@typescript-eslint',
|
||||||
|
],
|
||||||
|
'settings': {
|
||||||
|
'react': {
|
||||||
|
'version': 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'rules': {
|
||||||
|
'indent': ['error', "tab"],
|
||||||
|
'no-tabs': ['error', {'allowIndentationTabs': true}],
|
||||||
|
'max-len': ['error', {'tabWidth': 4}],
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,115 @@
|
||||||
|
# vim
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# build and dev deprecated
|
||||||
|
build
|
||||||
|
dev
|
||||||
|
# new build dir
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
78
README.md
78
README.md
|
@ -1,3 +1,79 @@
|
||||||
# curiousroamers
|
# curiousroamers
|
||||||
|
curiousroamers' blog
|
||||||
|
|
||||||
Ongoing project. A blog I make for a friend.
|
# Installation
|
||||||
|
|
||||||
|
You need a postgresql databased called curiousroamers setup,
|
||||||
|
You have to specify :
|
||||||
|
- SESSION_SECRET
|
||||||
|
- ACCESS_TOKEN_SECRET
|
||||||
|
- DATABASE_PASSWORD
|
||||||
|
in the .env file
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
|
||||||
|
If you wanna create an admin you gotta create a new UniqueLink row in the
|
||||||
|
uniquelink table, Then go to localhost:3000/admin/signup/:uuid (:uuid being the
|
||||||
|
UniqueLink row's uuid) to create an account.
|
||||||
|
|
||||||
|
The admin page is not very pretty at the moment cause it is not done.
|
||||||
|
|
||||||
|
I have a project of transforming this website into a simple blog CMS.
|
||||||
|
|
||||||
|
# Design curious roamers
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
### Responsiveness
|
||||||
|
* Pages utilisateurs
|
||||||
|
* Pages admin
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
* Home (Notre voyage)
|
||||||
|
* Qui sommes nous ?
|
||||||
|
* Contact
|
||||||
|
* Blog
|
||||||
|
|
||||||
|
### Contact
|
||||||
|
* Coordonees
|
||||||
|
* Formulaire de contact comme wordpress -siteweb
|
||||||
|
|
||||||
|
### Mail
|
||||||
|
* Check port to use (25, 465, 587 or 2525)
|
||||||
|
* Nodemailer for node
|
||||||
|
* Postfix for server
|
||||||
|
* Check MailHog
|
||||||
|
|
||||||
|
### Blog
|
||||||
|
* Article (markdown)
|
||||||
|
* Voir plus (sans description)
|
||||||
|
* Plusieurs tag par article
|
||||||
|
* Formulaire de contact et source du contact
|
||||||
|
|
||||||
|
### Tag
|
||||||
|
* Recherche par tag
|
||||||
|
* tag cliquables
|
||||||
|
|
||||||
|
### Footer
|
||||||
|
* Que sur la homepage
|
||||||
|
* A propos
|
||||||
|
* Nous contacter
|
||||||
|
* Donation
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
* nomdusite.com/admin
|
||||||
|
* connection classique
|
||||||
|
* Ajouter article
|
||||||
|
* Supprimer article
|
||||||
|
* Editer article
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
### Responsiveness
|
||||||
|
### Pages
|
||||||
|
### Contact
|
||||||
|
### Blog
|
||||||
|
### Tag
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"declarationDir": "../dist/types"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../src/client/**/*"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"declarationDir": "../dist/types"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../src/server/**/*"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
const path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
const EsLint = require('eslint-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
main: path.resolve(__dirname, '../src/client/main.tsx'),
|
||||||
|
admin: path.resolve(__dirname, '../src/client/admin.tsx'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
path: path.resolve(__dirname, '../dist/client'),
|
||||||
|
},
|
||||||
|
target: 'web',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js', '.json']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
include: path.join(__dirname, '../src/client'),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new EsLint({
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
context: path.join(__dirname, '../src/client'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.client.common.js');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
devtool: 'eval-cheap-module-source-map',
|
||||||
|
mode: 'development',
|
||||||
|
watch: true
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.client.common.js');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production',
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [new TerserPlugin({
|
||||||
|
parallel: true
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
const path = require('path');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
const EsLint = require('eslint-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: path.resolve(__dirname, '../src/server/main.ts'),
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, '../dist/server'),
|
||||||
|
},
|
||||||
|
target: 'node',
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js', '.json']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
include: path.join(__dirname, '../src/server'),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new EsLint({
|
||||||
|
extensions: ['.ts', '.tsx'],
|
||||||
|
context: path.join(__dirname, '../src/server'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.server.common.js');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
devtool: 'eval-cheap-module-source-map',
|
||||||
|
mode: 'development',
|
||||||
|
watch: true
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.server.common.js');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production',
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [new TerserPlugin({
|
||||||
|
parallel: true
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,87 @@
|
||||||
|
{
|
||||||
|
"name": "curiousroamers",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "curiousroamers' blog",
|
||||||
|
"scripts": {
|
||||||
|
"type-check:server": "tsc --pretty --project config/tsconfig.server.json --noEmit",
|
||||||
|
"type-check:client": "tsc --pretty --project config/tsconfig.client.json --noEmit",
|
||||||
|
"type-check": "concurrently \"npm:type-check:server\" \"npm:type-check:client\"",
|
||||||
|
"type-check:server:watch": "tsc --watch --preserveWatchOutput --pretty --project config/tsconfig.server.dev.json --noEmit",
|
||||||
|
"type-check:client:watch": "tsc --watch --preserveWatchOutput --pretty --project config/tsconfig.client.dev.json --noEmit",
|
||||||
|
"type-check:watch": "concurrently \"npm:type-check:server:watch\" \"npm:type-check:client:watch\"",
|
||||||
|
"build:server:types": "tsc --emitDeclarationOnly --project config/tsconfig.server.json",
|
||||||
|
"build:server:js": "webpack --config config/webpack.server.prod.js",
|
||||||
|
"build:server": "npm run build:server:types && npm run build:server:js",
|
||||||
|
"build:client:types": "tsc --emitDeclarationOnly --project config/tsconfig.client.json",
|
||||||
|
"build:client:js": "webpack --config config/webpack.client.prod.js",
|
||||||
|
"build:client": "npm run build:client:types && npm run build:client:js",
|
||||||
|
"build": "rm -rf dist && npm run build:server && npm run build:client",
|
||||||
|
"dev:server:types": "tsc --emitDeclarationOnly --watch --preserveWatchOutput --pretty --project config/tsconfig.server.json",
|
||||||
|
"dev:server:js": "webpack --config config/webpack.server.dev.js",
|
||||||
|
"dev:server": "concurrently \"npm:dev:server:types\" \"npm:dev:server:js\"",
|
||||||
|
"dev:client:types": "tsc --emitDeclarationOnly --watch --preserveWatchOutput --pretty --project config/tsconfig.client.json",
|
||||||
|
"dev:client:js": "webpack --config config/webpack.client.dev.js",
|
||||||
|
"dev:client": "concurrently \"npm:dev:client:types\" \"npm:dev:client:js\"",
|
||||||
|
"dev:start": "mkdir -p dist/server && touch dist/server/bundle.js && nodemon dist/server/bundle.js",
|
||||||
|
"dev": "rm -rf dist && concurrently \"npm:dev:server\" \"npm:dev:client\" \"npm:dev:start\"",
|
||||||
|
"start": "node dist/server/bundle.js"
|
||||||
|
},
|
||||||
|
"author": "gbrochar",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.3",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||||
|
"@babel/preset-env": "^7.12.1",
|
||||||
|
"@babel/preset-react": "^7.12.1",
|
||||||
|
"@babel/preset-typescript": "^7.12.1",
|
||||||
|
"@babel/runtime-corejs3": "^7.12.5",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
|
"@types/dompurify": "^2.0.4",
|
||||||
|
"@types/express": "^4.17.8",
|
||||||
|
"@types/express-flash": "0.0.2",
|
||||||
|
"@types/express-session": "^1.17.2",
|
||||||
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
|
"@types/marked": "^1.1.0",
|
||||||
|
"@types/method-override": "0.0.31",
|
||||||
|
"@types/node": "^14.14.6",
|
||||||
|
"@types/react": "^16.9.55",
|
||||||
|
"@types/react-dom": "^16.9.9",
|
||||||
|
"@types/sequelize": "^4.28.9",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
|
"babel-loader": "^8.1.0",
|
||||||
|
"concurrently": "^5.3.0",
|
||||||
|
"core-js": "^3.7.0",
|
||||||
|
"eslint": "^7.13.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-plugin-react": "^7.21.5",
|
||||||
|
"eslint-webpack-plugin": "^2.2.0",
|
||||||
|
"nodemon": "^2.0.6",
|
||||||
|
"typescript": "^4.0.5",
|
||||||
|
"webpack": "^5.3.2",
|
||||||
|
"webpack-cli": "^4.1.0",
|
||||||
|
"webpack-merge": "^5.3.0",
|
||||||
|
"webpack-node-externals": "^2.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"argon2": "^0.27.0",
|
||||||
|
"axios": "^0.21.0",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
|
"dompurify": "^2.2.2",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"ejs": "^3.1.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"express-flash": "0.0.2",
|
||||||
|
"express-session": "^1.17.1",
|
||||||
|
"jsdom": "^16.4.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"marked": "^1.2.4",
|
||||||
|
"method-override": "^3.0.0",
|
||||||
|
"pg": "^8.5.1",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"sequelize": "^6.3.5",
|
||||||
|
"slugify": "^1.4.6"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#imagewithdescription {
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#smallimage {
|
||||||
|
height: 200px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner {
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar {
|
||||||
|
max-width: calc(782px - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 16px solid #f3f3f3;
|
||||||
|
border-top: 16px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: auto;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin{
|
||||||
|
0% {transform: rotate(0deg);}
|
||||||
|
100% {transform: rotate(360deg);}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 238 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
[ZoneTransfer]
|
||||||
|
ZoneId=3
|
||||||
|
ReferrerUrl=https://garrotyoan1.wordpress.com/
|
||||||
|
HostUrl=https://garrotyoan1.files.wordpress.com/2020/09/pexels-photo-4885892.jpeg
|
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
[ZoneTransfer]
|
||||||
|
ZoneId=3
|
||||||
|
ReferrerUrl=https://garrotyoan1.wordpress.com/
|
||||||
|
HostUrl=https://garrotyoan1.files.wordpress.com/2020/09/pexels-photo-5425971.jpeg?w=164&h=235
|
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
[ZoneTransfer]
|
||||||
|
ZoneId=3
|
||||||
|
ReferrerUrl=https://garrotyoan1.wordpress.com/
|
||||||
|
HostUrl=https://garrotyoan1.files.wordpress.com/2020/09/pexels-photo-2512258-1.jpeg?w=344
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from './Link';
|
||||||
|
import Button from './Button';
|
||||||
|
import ArticleTable from './ArticleTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function AdminRoot() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h1>Admin index</h1>
|
||||||
|
<Link to="/admin/new" text="Create new article" />
|
||||||
|
<Button to="/api/signout" method="DELETE" text="Signout" />
|
||||||
|
<h2>List of articles</h2>
|
||||||
|
<ArticleTable />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Loader from './Loader';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function ArticleTable() {
|
||||||
|
const [articleTable, setArticleTable] = useState([])
|
||||||
|
const [clickedOnDelete, setClickedOnDelete] = useState(false);
|
||||||
|
const [busy, setBusy] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBusy(true)
|
||||||
|
axios.get('/api/articletable', {
|
||||||
|
withCredentials: true,
|
||||||
|
}).then((articles: any) => {
|
||||||
|
var $articles: any = [];
|
||||||
|
articles.data.forEach((article: any) => {
|
||||||
|
$articles.push(
|
||||||
|
<tr key={article.id}>
|
||||||
|
<td>{article.title}</td>
|
||||||
|
<td>{article.id}</td>
|
||||||
|
<td>{new Date(article.createdAt).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
<a className="btn btn-info" href={'/admin/edit' + article.id}>Edit</a>
|
||||||
|
<button className="btn btn-danger" onClick={() => deleteArticle(article.id)}>Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>);
|
||||||
|
});
|
||||||
|
setArticleTable($articles);
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
|
}, [clickedOnDelete]);
|
||||||
|
|
||||||
|
function deleteArticle(id: string) {
|
||||||
|
axios.delete('/admin/' + id, {
|
||||||
|
withCredentials: true,
|
||||||
|
}).then(() => {
|
||||||
|
setClickedOnDelete(!clickedOnDelete);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{busy ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<table className="table table-dark">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
<th>Creation Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{articleTable}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Link from './Link';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
to: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BannerProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
links: Array<Link>;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Banner(
|
||||||
|
{title, description, links, image}: BannerProps) {
|
||||||
|
let imgUrl;
|
||||||
|
if (image.startsWith('/static/images/')) {
|
||||||
|
imgUrl = image;
|
||||||
|
} else {
|
||||||
|
imgUrl = '/static/images/' + image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
backgroundImage: `url('` + imgUrl + `')`,
|
||||||
|
backgroundPosition: '64% 78%',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
};
|
||||||
|
|
||||||
|
const jumbotronStyle = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorLayer = {
|
||||||
|
backgroundColor: '#1279be70',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div
|
||||||
|
className='container-fluid p-0'
|
||||||
|
style={containerStyle}>
|
||||||
|
<div
|
||||||
|
className='container-fluid p-0'
|
||||||
|
style={colorLayer}>
|
||||||
|
<div
|
||||||
|
className='jumbotron container text-white px-0'
|
||||||
|
id='banner'
|
||||||
|
style={jumbotronStyle}>
|
||||||
|
<h1 className='display-4 font-weight-bold'>{title}</h1>
|
||||||
|
<div className='h5'>
|
||||||
|
{description.split('\n').map((elem, index) =>
|
||||||
|
<p key={index}>
|
||||||
|
{elem}
|
||||||
|
</p>)}
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
{links.map((elem, index) =>
|
||||||
|
<p key={index} className='lead mt-2'>
|
||||||
|
<Link
|
||||||
|
className='btn btn-info btn-lg
|
||||||
|
font-weight-bold'
|
||||||
|
to={elem.to}
|
||||||
|
text={elem.text} />
|
||||||
|
</p>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
to: string;
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Button({to, method, text}: ButtonProps) {
|
||||||
|
async function buttonFetch() {
|
||||||
|
console.log('buttonFetch');
|
||||||
|
console.log(to);
|
||||||
|
console.log(method);
|
||||||
|
axios({
|
||||||
|
method: method,
|
||||||
|
url: to,
|
||||||
|
withCredentials: true,
|
||||||
|
}).then((_res: any) => {
|
||||||
|
window.location.replace('/');
|
||||||
|
}).catch((_err: any) => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={buttonFetch}>{text}</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
image: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface imagesWithDescriptionProps {
|
||||||
|
images: Array<Image>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function ImagesWithDescription(
|
||||||
|
{images}: imagesWithDescriptionProps) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='container w-50 d-flex justify-content-between'>
|
||||||
|
{images.map((elem, index) =>
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
id='imagewithdescription'
|
||||||
|
className='align-top'>
|
||||||
|
<img
|
||||||
|
src={'/static/images/' + elem.image}
|
||||||
|
className='rounded mb-3'
|
||||||
|
id='smallimage'></img>
|
||||||
|
<h3 className='font-weight-bold'>
|
||||||
|
{elem.title}
|
||||||
|
</h3>
|
||||||
|
<p>{elem.description}</p>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface LinkProps {
|
||||||
|
className?: string;
|
||||||
|
to: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Link({className, to, text}: LinkProps) {
|
||||||
|
className = className || 'btn btn-primary';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className={className} href={to}>{text}</a>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Loader() {
|
||||||
|
return (
|
||||||
|
<div className="loader"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
to: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
main: Link;
|
||||||
|
sub: Array<Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Navbar({main, sub}: NavbarProps) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='navbar navbar-expand-lg navbar-light'>
|
||||||
|
<div
|
||||||
|
className='container'
|
||||||
|
id='navbar'>
|
||||||
|
<a className='navbar-brand' href={main.to}>{main.text}</a>
|
||||||
|
<button
|
||||||
|
className='navbar-toggler'
|
||||||
|
type='button'
|
||||||
|
data-toggle='collapse'
|
||||||
|
data-target='#navbar'
|
||||||
|
aria-controls='navbar'>
|
||||||
|
<span className='navbar-toggler-icon'></span>
|
||||||
|
</button>
|
||||||
|
<div className='collapse navbar-collapse' id='navbar'>
|
||||||
|
<ul className='navbar-nav mr-auto mb-0 pb-0'>
|
||||||
|
{sub.map((elem, index) =>
|
||||||
|
<li key={index} className='nav-item'>
|
||||||
|
<a className='nav-link' href={elem.to}>
|
||||||
|
{elem.text}
|
||||||
|
</a>
|
||||||
|
</li>)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Navbar from './Navbar';
|
||||||
|
import Banner from './Banner';
|
||||||
|
import ImagesWithDescription from './ImagesWithDescription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {jsx} The root component
|
||||||
|
*/
|
||||||
|
export default function Root() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Navbar
|
||||||
|
main={{
|
||||||
|
text: 'Curious Roamers',
|
||||||
|
to: '/blog',
|
||||||
|
}}
|
||||||
|
sub={[
|
||||||
|
{
|
||||||
|
text: 'Home',
|
||||||
|
to: '/',
|
||||||
|
}, {
|
||||||
|
text: 'Carnet de voyage',
|
||||||
|
to: '/blog',
|
||||||
|
}, {
|
||||||
|
text: 'Qui sommes nous ?',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
text: 'Contact',
|
||||||
|
to: '/contact',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
title={`Un voyage à travers l'Europe`}
|
||||||
|
description={`
|
||||||
|
On vous raconte nos aventures à travers l'Europe.\n
|
||||||
|
Nous voyageons en stop et sans argent.\n
|
||||||
|
On loge chez l'habitant ou dehors dans des hamacs.`}
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
to: '/blog',
|
||||||
|
text: 'Notre carnet de voyage',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
image={'map.jpeg'}
|
||||||
|
/>
|
||||||
|
<ImagesWithDescription
|
||||||
|
images={[
|
||||||
|
{
|
||||||
|
image: 'urbex.webp',
|
||||||
|
title: 'Explorations',
|
||||||
|
description: `Amateurs d'escalade et de sensations
|
||||||
|
fortes, nous cherchons des lieux abandonnés ou
|
||||||
|
difficiles d'accès.`,
|
||||||
|
}, {
|
||||||
|
image: 'cliff.jpeg',
|
||||||
|
title: 'Découvertes',
|
||||||
|
description: 'De beaux paysages, tu connais.',
|
||||||
|
}, {
|
||||||
|
image: 'eye.webp',
|
||||||
|
title: 'Rencontres',
|
||||||
|
description: `Notre voyage sera avant tout rythmé de
|
||||||
|
nouveaux visages. Sans personne pour nous aider
|
||||||
|
ce voyage aurait peu de sens.`,
|
||||||
|
},
|
||||||
|
]}/>
|
||||||
|
{/* eslint-disable */}
|
||||||
|
{/* <Quote
|
||||||
|
quote={'Not all those who wander are lost.'}
|
||||||
|
author={'J. R. R. Tolkien'}
|
||||||
|
description={`
|
||||||
|
On veut quitter notre confort pendant un moment.
|
||||||
|
Briser nos habitudes et nos ancrages.
|
||||||
|
Faire place à la nouveauté et l'incompréhension.`}/>
|
||||||
|
<Footer
|
||||||
|
elements={[
|
||||||
|
{
|
||||||
|
title: 'À propos',
|
||||||
|
description: `Nous sommes deux jeunes de 23 ans à la
|
||||||
|
recherche d'aventures et de réponses à travers
|
||||||
|
l'Europe`,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
to: '/about',
|
||||||
|
text: 'Plus de détails',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
title: 'Nous contacter',
|
||||||
|
description: `
|
||||||
|
<ul>
|
||||||
|
<li>curious.roamers@gmail.com</li>
|
||||||
|
<li>06 37 11 28 22</li>
|
||||||
|
</ul>`,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
to: '/contant',
|
||||||
|
text: `Autres façons de nous contacter`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
title: 'Donation',
|
||||||
|
description: `Si vous voulez contribuer à financer nos
|
||||||
|
futurs voyages ou simplement nous aider financièrement
|
||||||
|
:)`,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
to: 'paypal.me/ygarrot',
|
||||||
|
text: 'Paypal',
|
||||||
|
}, {
|
||||||
|
to: 'tipeee.com/curious-roamers',
|
||||||
|
text: 'Tipeee',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}/> */}
|
||||||
|
{/* eslint-enable */}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {hydrate, render} from 'react-dom';
|
||||||
|
import AdminRoot from './AdminRoot';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
let renderMethod;
|
||||||
|
if (root && root.innerHTML !== '') {
|
||||||
|
renderMethod = hydrate;
|
||||||
|
} else {
|
||||||
|
renderMethod = render;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMethod(<AdminRoot />, document.getElementById('root'));
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {render, hydrate} from 'react-dom';
|
||||||
|
import Root from './Root';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
let renderMethod;
|
||||||
|
if (root && root.innerHTML !== '') {
|
||||||
|
renderMethod = hydrate;
|
||||||
|
} else {
|
||||||
|
renderMethod = render;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMethod(<Root />, document.getElementById('root'));
|
|
@ -0,0 +1,59 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import {RequestHandler} from 'express';
|
||||||
|
|
||||||
|
import * as t from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {json} user - the data to store in the token
|
||||||
|
* @return {jwt} - an Access Token
|
||||||
|
* This function generates an access token
|
||||||
|
*/
|
||||||
|
export function generateAccessToken(user: t.User) {
|
||||||
|
return jwt.sign(user,
|
||||||
|
process.env.ACCESS_TOKEN_SECRET!);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkNoAuth: RequestHandler = (req, res, next) => {
|
||||||
|
if (req.cookies.accessToken == undefined) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
return res.redirect('/admin');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authorize: RequestHandler = async (req, res, next) => {
|
||||||
|
const token = req.cookies.accessToken;
|
||||||
|
if (token == undefined) {
|
||||||
|
req.flash('info', 'You must be signed in to access that resource');
|
||||||
|
return res.status(401).redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!,
|
||||||
|
(err: any, _payload: any) => {
|
||||||
|
if (err) {
|
||||||
|
res.cookie('accessToken', '', {expires: new Date()});
|
||||||
|
req.flash('error', `Error in token verification,
|
||||||
|
you've been automatically signed out`);
|
||||||
|
return res.status(401).redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authorizeEmailVerification: RequestHandler = (req, res, next) => {
|
||||||
|
const token = req.cookies.accessToken;
|
||||||
|
if (token == undefined) {
|
||||||
|
req.flash('error', `Please sign in then click the email
|
||||||
|
verification link again`);
|
||||||
|
return res.status(401).redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!,
|
||||||
|
(err: any, _user: any) => {
|
||||||
|
if (err) {
|
||||||
|
res.cookie('accessToken', '', {expires: new Date()});
|
||||||
|
req.flash('error', `Error in token verification,
|
||||||
|
you've been automatically signed out`);
|
||||||
|
return res.status(401).redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {Sequelize} from 'sequelize';
|
||||||
|
|
||||||
|
import Article from './models/article';
|
||||||
|
import Tag from './models/tag';
|
||||||
|
import User from './models/user';
|
||||||
|
import UniqueLink from './models/uniquelink';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to database
|
||||||
|
* @param {Sequelize} sequelize the sequelize object
|
||||||
|
*/
|
||||||
|
export async function dbConnect(sequelize: Sequelize) {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Successfully connected to the database ' +
|
||||||
|
sequelize.config.database);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Connection to database failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect and init database
|
||||||
|
* @param {Sequelize} sequelize the sequelize object
|
||||||
|
*/
|
||||||
|
export async function dbConnectAndInit(sequelize: Sequelize) {
|
||||||
|
await Article.hasMany(Tag, {
|
||||||
|
foreignKey: 'articleId',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
});
|
||||||
|
await dbConnect(sequelize);
|
||||||
|
await User.sync();
|
||||||
|
await UniqueLink.sync();
|
||||||
|
await sequelize.sync();
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
import {Sequelize} from 'sequelize';
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(
|
||||||
|
'curiousroamers',
|
||||||
|
'gbrochar',
|
||||||
|
process.env.DATABASE_PASSWORD!,
|
||||||
|
{
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default sequelize;
|
|
@ -0,0 +1,47 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import flash from 'express-flash';
|
||||||
|
import session from 'express-session';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import methodOverride from 'method-override';
|
||||||
|
|
||||||
|
import sequelize from './dbobject';
|
||||||
|
import {dbConnectAndInit} from './db';
|
||||||
|
|
||||||
|
import adminRouter from './routes/admin';
|
||||||
|
import blogRouter from './routes/blog';
|
||||||
|
import apiRouter from './routes/api';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
dbConnectAndInit(sequelize);
|
||||||
|
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.urlencoded({extended: false}));
|
||||||
|
app.use(methodOverride('_method'));
|
||||||
|
app.use('/js', express.static(path.join(__dirname, '../../dist/client')));
|
||||||
|
app.use('/static', express.static(path.join(__dirname, '../../public')));
|
||||||
|
app.use(session({
|
||||||
|
cookie: {
|
||||||
|
maxAge: 60000,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false, // set this to true in production
|
||||||
|
},
|
||||||
|
secret: process.env.SESSION_SECRET!,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
}));
|
||||||
|
app.use(flash());
|
||||||
|
|
||||||
|
app.get('/', (_req, res) => {
|
||||||
|
res.render('index');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/admin', adminRouter);
|
||||||
|
app.use('/blog', blogRouter);
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
app.listen(3000);
|
|
@ -0,0 +1,57 @@
|
||||||
|
import Sequelize, {DataTypes, Model} from 'sequelize';
|
||||||
|
import sequelize from '../dbobject';
|
||||||
|
|
||||||
|
import marked from 'marked';
|
||||||
|
import slugify from 'slugify';
|
||||||
|
import createDomPurify from 'dompurify';
|
||||||
|
const {JSDOM} = require('jsdom');
|
||||||
|
const dompurify = createDomPurify(new JSDOM().window);
|
||||||
|
|
||||||
|
export interface articleI extends Model {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
markdown: string;
|
||||||
|
slug: string;
|
||||||
|
sanitizedHtml: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Article = sequelize.define<articleI>('article', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
slug: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
sanitizedHtml: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Article.addHook('beforeValidate', (article: articleI, options: any) => {
|
||||||
|
options.fields.push('slug');
|
||||||
|
options.fields.push('sanitizedHtml');
|
||||||
|
if (article.title) {
|
||||||
|
article.slug = slugify(article.title, {lower: true, strict: true});
|
||||||
|
}
|
||||||
|
if (article.markdown) {
|
||||||
|
article.sanitizedHtml = dompurify.sanitize(marked(article.markdown));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Article;
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Sequelize, {DataTypes, Model} from 'sequelize';
|
||||||
|
import sequelize from '../dbobject';
|
||||||
|
|
||||||
|
export interface tagI extends Model {
|
||||||
|
id: string;
|
||||||
|
articleId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag = sequelize.define<tagI>('tag', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Tag;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Sequelize, {DataTypes, Model} from 'sequelize';
|
||||||
|
import sequelize from '../dbobject';
|
||||||
|
|
||||||
|
export interface uniqueLinkI extends Model {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniqueLink = sequelize.define<uniqueLinkI>('uniquelink', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UniqueLink;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import Sequelize, {DataTypes, Model} from 'sequelize';
|
||||||
|
import sequelize from '../dbobject';
|
||||||
|
|
||||||
|
export interface userI extends Model {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
verifiedemail: boolean;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const User = sequelize.define<userI>('user', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
verifiedemail: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default User;
|
|
@ -0,0 +1,260 @@
|
||||||
|
import express, {RequestHandler} from 'express';
|
||||||
|
import sequelize from './../dbobject';
|
||||||
|
|
||||||
|
import Article, {articleI} from './../models/article';
|
||||||
|
import Tag from './../models/tag';
|
||||||
|
import User from './../models/user';
|
||||||
|
import UniqueLink from './../models/uniquelink';
|
||||||
|
|
||||||
|
import argon2 from 'argon2';
|
||||||
|
import {authorize,
|
||||||
|
authorizeEmailVerification,
|
||||||
|
checkNoAuth,
|
||||||
|
generateAccessToken} from './../auth';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
const router = express.Router();
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
article: articleI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
/* Homepage route */
|
||||||
|
|
||||||
|
router.get('/', authorize, async (_req, res) => {
|
||||||
|
const articles = await Article.findAll({
|
||||||
|
attributes: [
|
||||||
|
'title',
|
||||||
|
'id',
|
||||||
|
'createdAt',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
res.render('admin/index', {articles});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Login routes */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} type Type of unique link for error message
|
||||||
|
* @param {number} expiration Duration of expiration of the link in minute
|
||||||
|
* @return {RequestHandler} middleware
|
||||||
|
*/
|
||||||
|
function verifyUniqueLink(type: string, expiration: number): RequestHandler {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const linkCheck = await UniqueLink.findOne({
|
||||||
|
where: {id: req.params.uuid},
|
||||||
|
});
|
||||||
|
if (linkCheck == null) {
|
||||||
|
req.flash('error', 'Page does not exists');
|
||||||
|
return res.status(404).redirect('/');
|
||||||
|
}
|
||||||
|
if ((Date.now() - linkCheck.createdAt.getTime()) / (1000 * 60) >
|
||||||
|
expiration) {
|
||||||
|
if (type == 'signup') {
|
||||||
|
req.flash('error', `The ressource has expired, please contact
|
||||||
|
an admin to create a new signup link`);
|
||||||
|
} else {
|
||||||
|
req.flash('error', `The ressource has expired, please create a
|
||||||
|
new ` + type + ' link');
|
||||||
|
}
|
||||||
|
await linkCheck.destroy();
|
||||||
|
return res.status(410).redirect('/');
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/verifyemail/:id/:uuid', verifyUniqueLink('email verification',
|
||||||
|
60*24*30), authorizeEmailVerification, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: {id: req.params.id},
|
||||||
|
rejectOnEmpty: true,
|
||||||
|
});
|
||||||
|
user.verifiedemail = true;
|
||||||
|
await user.save();
|
||||||
|
await UniqueLink.destroy({where: {id: req.params.uuid}});
|
||||||
|
res.status(204).redirect('/admin');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
req.flash('error', `Server error, please try again, if that
|
||||||
|
persists contact your web administrator`);
|
||||||
|
res.status(500).redirect('/admin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/changepassword/:id/:uuid', verifyUniqueLink('password reset',
|
||||||
|
30), checkNoAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: {id: req.params.id},
|
||||||
|
rejectOnEmpty: true,
|
||||||
|
});
|
||||||
|
const hash = await argon2.hash(req.body.password);
|
||||||
|
user.password = hash;
|
||||||
|
await user.save();
|
||||||
|
await UniqueLink.destroy({where: {id: req.params.uuid}});
|
||||||
|
res.status(204).redirect('/admin/signin');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
req.flash('error', `Server error, please try again, if that
|
||||||
|
persists contact your web administrator`);
|
||||||
|
res.status(500).redirect('/admin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/signup/:uuid', verifyUniqueLink('signup', 30), checkNoAuth,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (await User.findOne({where: {name: req.body.name}}) != null) {
|
||||||
|
req.flash('error', 'Username already in use');
|
||||||
|
return res.status(409).redirect('/admin/signup/' +
|
||||||
|
req.params.uuid);
|
||||||
|
}
|
||||||
|
if (await User.findOne({where: {email: req.body.email}}) != null) {
|
||||||
|
req.flash('error', 'Email already in use');
|
||||||
|
return res.status(409).redirect('/admin/signup/' +
|
||||||
|
req.params.uuid);
|
||||||
|
}
|
||||||
|
const hash = await argon2.hash(req.body.password);
|
||||||
|
const user = User.build({
|
||||||
|
name: req.body.name,
|
||||||
|
email: req.body.email,
|
||||||
|
verifiedemail: false,
|
||||||
|
password: hash,
|
||||||
|
});
|
||||||
|
await user.save();
|
||||||
|
await UniqueLink.destroy({where: {id: req.params.uuid}});
|
||||||
|
res.status(201).redirect('/admin/signin');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
req.flash('error', `Server error, please try again, if that
|
||||||
|
persists contact your web administrator`);
|
||||||
|
res.status(500).redirect('/admin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/signin', checkNoAuth, async (req, res) => {
|
||||||
|
let user: any = await User.findOne({where: {name: req.body.name}});
|
||||||
|
const email = await User.findOne({where: {email: req.body.name}});
|
||||||
|
if (!user && !email) {
|
||||||
|
req.flash('error', 'Name or email does not match');
|
||||||
|
return res.redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
user = email;
|
||||||
|
}
|
||||||
|
if (await argon2.verify(user.password, req.body.password)) {
|
||||||
|
const tokenData = {
|
||||||
|
sub: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
};
|
||||||
|
const accessToken = generateAccessToken(tokenData);
|
||||||
|
const farFuture = new Date(new Date().getTime() +
|
||||||
|
(1000 * 60 * 60 * 24 * 365 * 100));
|
||||||
|
res.cookie('accessToken', accessToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
expires: farFuture,
|
||||||
|
});
|
||||||
|
res.redirect('/admin');
|
||||||
|
} else {
|
||||||
|
req.flash('error', 'Password does not match');
|
||||||
|
return res.redirect('/admin/signin');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/signout', async (_req, res) => {
|
||||||
|
res.cookie('accessToken', '', {expires: new Date()});
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin', checkNoAuth, (_req, res) => {
|
||||||
|
res.render('admin/signin');
|
||||||
|
});
|
||||||
|
router.get('/signup/:uuid', checkNoAuth, (req, res) => {
|
||||||
|
res.render('admin/signup', {uuid: req.params.uuid});
|
||||||
|
});
|
||||||
|
router.get('/changepassword/:id/:uuid', checkNoAuth, (_req, res) => {
|
||||||
|
res.render('admin/changepassword');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Article routes */
|
||||||
|
|
||||||
|
router.get('/new', authorize, (_req, res) => {
|
||||||
|
res.render('admin/new', {article: Article.build(), tags: []});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/edit/:id', authorize, async (req, res) => {
|
||||||
|
const article = await Article.findByPk(req.params.id,
|
||||||
|
{rejectOnEmpty: true});
|
||||||
|
const tags = await Tag.findAll({
|
||||||
|
where: {articleId: article.id},
|
||||||
|
attributes: [
|
||||||
|
'name',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
res.render('admin/edit', {article, tags});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', authorize, async (req, _res, next) => {
|
||||||
|
req.article = Article.build();
|
||||||
|
next();
|
||||||
|
}, saveArticleAndRedirect('new'));
|
||||||
|
|
||||||
|
router.put('/:id', authorize, async (req, _res, next) => {
|
||||||
|
req.article = await Article.findByPk(req.params.id, {rejectOnEmpty: true});
|
||||||
|
next();
|
||||||
|
}, saveArticleAndRedirect('edit'));
|
||||||
|
|
||||||
|
router.delete('/:id', authorize, async (req, res) => {
|
||||||
|
await Article.destroy({where: {id: req.params.id}, cascade: true});
|
||||||
|
res.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path Path of redirection
|
||||||
|
* @return {RequestHandler} middleware
|
||||||
|
*/
|
||||||
|
function saveArticleAndRedirect(path: string): RequestHandler {
|
||||||
|
return async (req, res) => {
|
||||||
|
let article: any = req.article;
|
||||||
|
article.title = req.body.title;
|
||||||
|
article.description = req.body.description;
|
||||||
|
article.markdown = req.body.markdown;
|
||||||
|
try {
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
let tagCount = 0;
|
||||||
|
while (req.body.articletags[tagCount] != '') {
|
||||||
|
tagCount++;
|
||||||
|
}
|
||||||
|
Tag.destroy({where: {articleId: article.id},
|
||||||
|
transaction: t});
|
||||||
|
article = await article.save();
|
||||||
|
for (let i = 0; i < tagCount; i++) {
|
||||||
|
await article.createTag({
|
||||||
|
articleId: article.id,
|
||||||
|
main: (i == 0) ? true : false,
|
||||||
|
name: req.body.articletags[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.redirect(`/blog/post/${article.slug}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
req.flash('error', 'The operation has failed, please try again');
|
||||||
|
res.render(`admin/${path}`, {article: article});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,105 @@
|
||||||
|
import express, {RequestHandler} from 'express';
|
||||||
|
import sequelize from './../dbobject';
|
||||||
|
|
||||||
|
import Article, {articleI} from './../models/article';
|
||||||
|
import Tag from './../models/tag';
|
||||||
|
|
||||||
|
import {authorize} from './../auth';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
const router = express.Router();
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
article: articleI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
router.delete('/signout', async (_req, res) => {
|
||||||
|
res.cookie('accessToken', '', {expires: new Date()});
|
||||||
|
res.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/articletable', authorize, async (_req, res) => {
|
||||||
|
const articles = await Article.findAll({
|
||||||
|
attributes: [
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'createdAt',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
res.send(articles);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/new', authorize, (_req, res) => {
|
||||||
|
res.render('admin/new', {article: Article.build(), tags: []});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/edit/:id', authorize, async (req, res) => {
|
||||||
|
const article = await Article.findByPk(req.params.id,
|
||||||
|
{rejectOnEmpty: true});
|
||||||
|
const tags = await Tag.findAll({
|
||||||
|
where: {articleId: article.id},
|
||||||
|
attributes: [
|
||||||
|
'name',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
res.render('admin/edit', {article, tags});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', authorize, async (req, _res, next) => {
|
||||||
|
req.article = Article.build();
|
||||||
|
next();
|
||||||
|
}, saveArticleAndRedirect('new'));
|
||||||
|
|
||||||
|
router.put('/:id', authorize, async (req, _res, next) => {
|
||||||
|
req.article = await Article.findByPk(req.params.id, {rejectOnEmpty: true});
|
||||||
|
next();
|
||||||
|
}, saveArticleAndRedirect('edit'));
|
||||||
|
|
||||||
|
router.delete('/:id', authorize, async (req, _res) => {
|
||||||
|
await Article.destroy({where: {id: req.params.id}, cascade: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path Path of redirection
|
||||||
|
* @return {RequestHandler} middleware
|
||||||
|
*/
|
||||||
|
function saveArticleAndRedirect(path: string): RequestHandler {
|
||||||
|
return async (req, res) => {
|
||||||
|
let article: any = req.article;
|
||||||
|
article.title = req.body.title;
|
||||||
|
article.description = req.body.description;
|
||||||
|
article.markdown = req.body.markdown;
|
||||||
|
try {
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
let tagCount = 0;
|
||||||
|
while (req.body.articletags[tagCount] != '') {
|
||||||
|
tagCount++;
|
||||||
|
}
|
||||||
|
Tag.destroy({where: {articleId: article.id},
|
||||||
|
transaction: t});
|
||||||
|
article = await article.save();
|
||||||
|
for (let i = 0; i < tagCount; i++) {
|
||||||
|
await article.createTag({
|
||||||
|
articleId: article.id,
|
||||||
|
main: (i == 0) ? true : false,
|
||||||
|
name: req.body.articletags[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.redirect(`/blog/post/${article.slug}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
req.flash('error', 'The operation has failed, please try again');
|
||||||
|
res.render(`admin/${path}`, {article: article});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
const router = express.Router()
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,19 @@
|
||||||
|
export interface Dict {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
sub: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"declaration": true,
|
||||||
|
"jsx" : "preserve",
|
||||||
|
"module": "commonjs",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"config",
|
||||||
|
"views",
|
||||||
|
"build",
|
||||||
|
"dev"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<% if (locals.messages.error) { %>
|
||||||
|
<% messages.error.forEach(message => { %>
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show mb-0" role="alert">
|
||||||
|
<%= message %>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if (locals.messages.warning) { %>
|
||||||
|
<% messages.warning.forEach(message => { %>
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show mb-0" role="alert">
|
||||||
|
<%= message %>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if (locals.messages.success) { %>
|
||||||
|
<% messages.success.forEach(message => { %>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show mb-0" role="alert">
|
||||||
|
<%= message %>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if (locals.messages.info) { %>
|
||||||
|
<% messages.info.forEach(message => { %>
|
||||||
|
<div class="alert alert-info alert-dismissible fade show mb-0" role="alert">
|
||||||
|
<%= message %>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<style>
|
||||||
|
.subtag {
|
||||||
|
display: inline-block;
|
||||||
|
width: 9.636363%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input required value="<%= article.title %>" type="text" name="title" id="title" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (tags[0] != undefined) { %>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tag">Tag</label>
|
||||||
|
<input required value="<%= tags[0].name %>" type="text" name="articletags" id="articletags" class="form-control">
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tag">Tag</label>
|
||||||
|
<input required value="" type="text" name="articletags" id="articletags" class="form-control">
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<div class="form-group">
|
||||||
|
<div>
|
||||||
|
<label for="subtags">Subtags</label>
|
||||||
|
</div>
|
||||||
|
<% let i = 1; %>
|
||||||
|
<% for (i; i < tags.length; i++) { %>
|
||||||
|
<input class="subtag" value="<%= tags[i].name %>" type="text" name="articletags" id="articletags" class="form-control">
|
||||||
|
<% } %>
|
||||||
|
<% for (i; i < 11; i++) { %>
|
||||||
|
<input class="subtag" value="" type="text" name="articletags" id="articletags" class="form-control">
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="markdown">Markdown</label>
|
||||||
|
<textarea required name="markdown" id="markdown" class="form-control"><%= article.markdown %></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="btn btn-secondary">Cancel</a>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
$('#home').submit(function () {
|
||||||
|
window.location = this.action
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form id="home" action="/" method="GET">
|
||||||
|
<ul class="nav masthead-nav">
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
<li class="mydot"><a>.</a></li>
|
||||||
|
<li><button>home</button></li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_flash') %>
|
||||||
|
<div class="container inner">
|
||||||
|
<div class="navbar fixed-top">
|
||||||
|
<%- include('_menu') %>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<h1 class="mb-4">Edit article</h1>
|
||||||
|
<form action="/admin/<%= article.id %>?_method=PUT" method="POST">
|
||||||
|
<%- include('_form_fields') %>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_flash') %>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src='/js/admin.bundle.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_flash') %>
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar fixed-top">
|
||||||
|
<%- include('_menu') %>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<h1 class="mb-4">New article</h1>
|
||||||
|
<form action="/admin" method="POST">
|
||||||
|
<%- include('_form_fields') %>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_flash') %>
|
||||||
|
<h1>signin</h1>
|
||||||
|
<form action="/admin/signin" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="name">Name or email</label>
|
||||||
|
<input required type="text" id="name" name="name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input required type="text" id="password" name="password">
|
||||||
|
</div>
|
||||||
|
<button type="submit">OK</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('../_flash') %>
|
||||||
|
<h1>signin</h1>
|
||||||
|
<form action="/admin/signup/<%= uuid %>" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input required type="text" id="name" name="name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input required type="text" id="email" name="email">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input required type="text" id="password" name="password">
|
||||||
|
</div>
|
||||||
|
<button type="submit">OK</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- include('_flash') %>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="js/main.bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue