@@ -0,0 +1,2 @@ | |||
node_modules | |||
config.js |
@@ -0,0 +1,48 @@ | |||
const mongoose = require("mongoose"); | |||
const slugify = require("slugify"); | |||
const marked = require("marked"); | |||
const createDomPurify = require("dompurify"); | |||
const { JSDOM } = require("jsdom"); | |||
const dompurify = createDomPurify(new JSDOM().window); | |||
const articleSchema = new mongoose.Schema({ | |||
title: { | |||
type: String, | |||
required: true, | |||
}, | |||
description: { | |||
type: String, | |||
required: true, | |||
}, | |||
markdown: { | |||
type: String, | |||
required: true, | |||
}, | |||
createdAt: { | |||
type: Date, | |||
default: Date.now, | |||
}, | |||
slug: { | |||
type: String, | |||
required: true, | |||
unique: true, | |||
}, | |||
sanitizedHtml: { | |||
type: String, | |||
required: true, | |||
}, | |||
}); | |||
articleSchema.pre("validate", function (next) { | |||
if (this.title) { | |||
this.slug = slugify(this.title, { lower: true, strict: true }); | |||
} | |||
if (this.markdown) { | |||
this.sanitizedHtml = dompurify.sanitize(marked(this.markdown)); | |||
} | |||
next(); | |||
}); | |||
module.exports = mongoose.model("Article", articleSchema); |
@@ -0,0 +1,25 @@ | |||
{ | |||
"name": "blog", | |||
"version": "1.0.0", | |||
"description": "", | |||
"main": "index.js", | |||
"scripts": { | |||
"devStart": "nodemon server.js" | |||
}, | |||
"author": "RinRi-D", | |||
"license": "ISC", | |||
"dependencies": { | |||
"dompurify": "^2.0.11", | |||
"ejs": "^3.1.3", | |||
"express": "^4.17.1", | |||
"highlight.js": "^10.1.1", | |||
"jsdom": "^16.2.2", | |||
"marked": "^1.1.0", | |||
"method-override": "^3.0.0", | |||
"mongoose": "^5.9.19", | |||
"slugify": "^1.4.0" | |||
}, | |||
"devDependencies": { | |||
"nodemon": "^2.0.4" | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
const express = require("express"); | |||
const router = express.Router(); | |||
const Article = require("../models/article"); | |||
const article = require("../models/article"); | |||
const { findByIdAndDelete } = require("../models/article"); | |||
const config = require("../config"); | |||
pass = config.users.pass; | |||
router.get("/new", (req, res) => { | |||
res.render("articles/new", { article: new Article() }); | |||
}); | |||
router.get("/:slug", async (req, res) => { | |||
const article = await Article.findOne({ slug: req.params.slug }); | |||
if (article == null) res.redirect("/"); | |||
res.render("articles/show", { article: article }); | |||
}); | |||
router.post("/", async (req, res) => { | |||
let article = new Article({ | |||
title: req.body.title, | |||
description: req.body.description, | |||
markdown: req.body.markdown, | |||
}); | |||
if (req.body.password == pass) { | |||
try { | |||
article = await article.save(); | |||
res.redirect("/articles/" + article.slug); | |||
} catch (e) { | |||
console.log(e); | |||
res.render("articles/new", { article: article }); | |||
} | |||
} | |||
else { | |||
res.render("articles/new", { article: article }); | |||
} | |||
}); | |||
router.delete("/:id", async (req, res) => { | |||
await Article.findByIdAndDelete(req.params.id); | |||
res.redirect("/"); | |||
}); | |||
module.exports = router; |
@@ -0,0 +1,36 @@ | |||
const express = require("express"); | |||
const mongoose = require("mongoose"); | |||
const Article = require("./models/article"); | |||
const articleRouter = require("./routes/articles"); | |||
const methodOverride = require("method-override"); | |||
const app = express(); | |||
const config = require("./config"); | |||
db = config.database; | |||
mongoose.connect( | |||
db.url, | |||
{ | |||
useNewUrlParser: true, | |||
useUnifiedTopology: true, | |||
useCreateIndex: true, | |||
} | |||
); | |||
app.set("view engine", "ejs"); | |||
app.use(express.urlencoded({ extended: false })); | |||
app.use("/blog/articles", articleRouter); | |||
app.use(methodOverride("_method")); | |||
app.get("/blog", async (req, res) => { | |||
const articles = await Article.find().sort({ createdAt: "desc" }); | |||
res.render("articles/index", { articles: articles }); | |||
}); | |||
app.get("/", (req, res) => { | |||
res.render("apps"); | |||
}); | |||
app.use("/", express.static(__dirname + "/public")); | |||
app.listen(5000); |
@@ -0,0 +1,67 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<link rel="stylesheet" href="https://rinri-d.xyz/css/bootstrap.min.css"> | |||
<link rel="stylesheet" href="https://rinri-d.xyz/css/style.css"> | |||
<link rel="shortcut icon" href="https://rinri-d.xyz/images/logo.ico" type="image/x-icon"> | |||
<title>RinRi - Apps</title> | |||
</head> | |||
<body> | |||
<nav class="navbar navbar-expand-lg navbar-dark"> | |||
<a class="navbar-brand" href="https://rinri-d.xyz" style="color: var(--color1);"> | |||
RinRi-D | |||
</a> | |||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" | |||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> | |||
<span class="navbar-toggler-icon"></span> | |||
</button> | |||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | |||
<div class="navbar-nav"> | |||
<a class="nav-item nav-link" href="https://rinri-d.xyz">Home</a> | |||
<a class="nav-item nav-link" href="https://blog.rinri-d.xyz">Blog</a> | |||
<a class="nav-item nav-link active" href="https://apps.rinri-d.xyz">Apps</a> | |||
<a class="nav-item nav-link" href="https://git.rinri-d.xyz">Code</a> | |||
<a class="nav-item nav-link" href="https://rinri-d.xyz/rec.html">Recommendations</a> | |||
</div> | |||
</div> | |||
</nav> | |||
<div class="container"> | |||
<h1>Node.js apps are hosted on this subdomain.</h1> | |||
<p> | |||
<h2>Apps:</h2> | |||
<ul> | |||
<li><a href="https://blog.rinri-d.xyz">Blog</a></li> | |||
</ul> | |||
</p> | |||
</div> | |||
<footer class="footer"> | |||
<div class="container-fluid"> | |||
<div class="row"> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Email: <a | |||
href="mailto:rin@rinri-d.xyz">rin@rinri-d.xyz</a> <a | |||
href="https://cloud.rinri-d.xyz/s/TtX6ny2x7XLSaeK">(PGP)</a> | |||
</div> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Mastodon: <a | |||
href="https://fosstodon.org/@rinri">@rinri@fosstodon.org</a> </div> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Matrix: <a | |||
href="https://matrix.to/#/@rinri-d:matrix.org">@rinri-d:matrix.org</a> </div> | |||
</div> | |||
<div style="text-align: center;">© Copyright 2020 RinRi-D.xyz</div> | |||
</div> | |||
</footer> | |||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" | |||
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" | |||
crossorigin="anonymous"></script> | |||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" | |||
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" | |||
crossorigin="anonymous"></script> | |||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" | |||
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" | |||
crossorigin="anonymous"></script> | |||
</body> | |||
</html> |
@@ -0,0 +1,22 @@ | |||
<div class="form-group"> | |||
<label for="title">Title</label> | |||
<input required value="<%= article.title %>" type="text" name="title" id="title" class="form-control"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="description">Description</label> | |||
<textarea required name="description" id="description" class="form-control"><%= article.description %></textarea> | |||
</div> | |||
<div class="form-group"> | |||
<label for="markdown">Markdown</label> | |||
<textarea required name="markdown" id="markdown" class="form-control"><%= article.markdown %></textarea> | |||
</div> | |||
<div class="form-group"> | |||
<label for="password">Password</label> | |||
<input required type="password" name="password" id="password" class="form-control"> | |||
</div> | |||
<a href="/" class="btn btn-secondary">Cancel</a> | |||
<button type="submit" class="btn btn-primary">Save</button> |
@@ -0,0 +1,17 @@ | |||
<%- include("../head.ejs") %> | |||
<div class="container"> | |||
<h1 class="mb-2">Blog</h1> | |||
<% articles.forEach( article => { %> | |||
<div class="item-info mt-4 col-12"> | |||
<h3><%= article.title %></h3> | |||
<p class="createdAt text-muted"> | |||
<%= article.createdAt.toLocaleString("ru-RU", {dateStyle:"short", timeStyle: "short"}) %> UTC</p> | |||
<p class="description"><%= article.description %></p> | |||
<hr> | |||
<a href="/articles/<%= article.slug %>">Read More</a> | |||
</div> | |||
<% }) %> | |||
</div> | |||
<%- include("../foot.ejs") %> |
@@ -0,0 +1,7 @@ | |||
<%- include("../head.ejs") %> | |||
<div class="container"> | |||
<form action="/articles" method="POST"> | |||
<%- include("_form.ejs") %> | |||
</form> | |||
</div> | |||
<%- include("../foot.ejs") %> |
@@ -0,0 +1,14 @@ | |||
<%- include("../head.ejs") %> | |||
<div class="container"> | |||
<h1 class="mb-1"><%= article.title %></h1> | |||
<div class="text-muted mb-3"> | |||
<%= article.createdAt.toLocaleString("ru-RU", {dateStyle:"short", timeStyle: "short"}) %> UTC | |||
</div> | |||
<div id="out"> | |||
<%- article.sanitizedHtml %> | |||
</div> | |||
<script data-isso="https://comments.rinri-d.xyz/" src="https://comments.rinri-d.xyz/js/embed.min.js"></script> | |||
<section id="isso-thread"></section> | |||
</div> | |||
<%- include("../foot.ejs") %> |
@@ -0,0 +1,28 @@ | |||
<footer class="footer"> | |||
<div class="container-fluid"> | |||
<div class="row"> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Email: <a href="mailto:rin@rinri-d.xyz">rin@rinri-d.xyz</a> <a | |||
href="https://cloud.rinri-d.xyz/s/TtX6ny2x7XLSaeK">(PGP)</a> | |||
</div> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Mastodon: <a | |||
href="https://fosstodon.org/@rinri">@rinri@fosstodon.org</a> </div> | |||
<div class="col-lg-4 col-md-6 col-sm-12 mb-4">Matrix: <a | |||
href="https://matrix.to/#/@rinri-d:matrix.org">@rinri-d:matrix.org</a> </div> | |||
</div> | |||
<div style="text-align: center;">© Copyright 2020 RinRi-D.xyz</div> | |||
</div> | |||
</footer> | |||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" | |||
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" | |||
crossorigin="anonymous"></script> | |||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" | |||
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" | |||
crossorigin="anonymous"></script> | |||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" | |||
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" | |||
crossorigin="anonymous"></script> | |||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.0/highlight.min.js"></script> | |||
<script>hljs.initHighlightingOnLoad();</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,33 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<link rel="stylesheet" href="https://rinri-d.xyz/css/bootstrap.min.css"> | |||
<link rel="stylesheet" href="https://rinri-d.xyz/css/style.css"> | |||
<link rel="stylesheet" | |||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.0/styles/atom-one-dark.min.css"> | |||
<link rel="shortcut icon" href="https://rinri-d.xyz/images/logo.ico" type="image/x-icon"> | |||
<title>RinRi - Blog</title> | |||
</head> | |||
<body> | |||
<nav class="navbar navbar-expand-lg navbar-dark"> | |||
<a class="navbar-brand" href="https://rinri-d.xyz" style="color: var(--color1);"> | |||
RinRi-D | |||
</a> | |||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" | |||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> | |||
<span class="navbar-toggler-icon"></span> | |||
</button> | |||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup"> | |||
<div class="navbar-nav"> | |||
<a class="nav-item nav-link" href="https://rinri-d.xyz">Home</a> | |||
<a class="nav-item nav-link active" href="https://blog.rinri-d.xyz">Blog</a> | |||
<a class="nav-item nav-link" href="https://apps.rinri-d.xyz">Apps</a> | |||
<a class="nav-item nav-link" href="https://git.rinri-d.xyz">Code</a> | |||
<a class="nav-item nav-link" href="https://rinri-d.xyz/rec.html">Recommendations</a> | |||
</div> | |||
</div> | |||
</nav> |