Compare commits
12 commits
243bc6397b
...
40b9a90bf9
Author | SHA1 | Date | |
---|---|---|---|
|
40b9a90bf9 | ||
|
16efbc7cb5 | ||
|
4e0611d5c2 | ||
|
6b0072df97 | ||
|
fda295b8f8 | ||
|
e19663d4e6 | ||
|
59014b5f9c | ||
|
787d40fb6b | ||
|
accbdb6f7a | ||
|
5c1c0de1b1 | ||
0ab9e12723 | |||
015d36ef91 |
13 changed files with 307 additions and 166 deletions
|
@ -5,7 +5,7 @@ ENV RUSTFLAGS='-C target-feature=-crt-static'
|
|||
WORKDIR /app
|
||||
COPY ./site .
|
||||
RUN apk add --no-cache musl-dev sqlite-dev
|
||||
RUN cargo install --path .
|
||||
RUN cargo install --path . -j $(nproc)
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
|
|
32
README.md
32
README.md
|
@ -2,31 +2,31 @@
|
|||
|
||||
Pure rust. Built with actix, diesel, tera, serde and sqlite3.
|
||||
|
||||
## Build instructions using docker
|
||||
## Run instructions using docker
|
||||
|
||||
1. Clone the repository
|
||||
```bash
|
||||
git clone https://github.com/leonardlorenz/crablog
|
||||
git clone https://github.com/mtrx1337/crablog
|
||||
cd crablog/site
|
||||
```
|
||||
2. Install diesel and create a database
|
||||
```bash
|
||||
cargo install diesel_cli
|
||||
cargo install diesel_cli --no-default-features --features "sqlite"
|
||||
diesel setup --database-url ../content/db.sqlite3
|
||||
diesel migration run --database-url ../content/db.sqlite3
|
||||
```
|
||||
3. Set up your configuration file (see below)
|
||||
4. Build and run the docker container (Will compile from source and thus take a while)
|
||||
4. Pull the image (or build from source) and run the docker container
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Configuration environment file
|
||||
|
||||
All configuration options are defined in .env, to be created in the same directory as this readme.
|
||||
|
||||
An example configuration:
|
||||
All configuration options are defined in crablog.env, an example configuration is provided.
|
||||
When not using Docker you may have to add crablog.env to your startup script or define the variables there.
|
||||
|
||||
`crablog.env`
|
||||
```
|
||||
USERNAME=yourusername
|
||||
EMAIL=me@mydomain.tld
|
||||
|
@ -44,13 +44,17 @@ ROOT_PATH=/path/to/template/directory/and/sqliteDB
|
|||
|
||||
## Routes
|
||||
|
||||
- / site welcome
|
||||
- /blog shows the last 5 posts
|
||||
- /blog/id/<id> shows a single post by id
|
||||
- /blog/all shows all posts
|
||||
- /blog/submit set your submit token and create posts
|
||||
- /blog/edit/<id> edit, delete or hide posts
|
||||
| Route | Description |
|
||||
| ------------ | -------------------------------------------------- |
|
||||
| `/` | shows the last 5 posts |
|
||||
| `/id/<id>` | shows a single post by id |
|
||||
| `/all` | shows all posts |
|
||||
| `/submit` | set your submit token and create posts |
|
||||
| `/edit/<id>` | edit, delete or hide posts |
|
||||
| `/about` | information about this blog, social media accounts |
|
||||
|
||||
**API Routes**
|
||||
|
||||
- /api/blog/posts returns all posts as json
|
||||
| Route | Description |
|
||||
| ---------------- | ------------------------- |
|
||||
| `api/blog/posts` | returns all posts as json |
|
||||
|
|
|
@ -13,9 +13,11 @@
|
|||
|
||||
<body>
|
||||
<h1>Hi, I'm {{ username }}</h1>
|
||||
<p style="text-align: right">
|
||||
<a href="/">Back to the blog</a>
|
||||
</p>
|
||||
<p>
|
||||
I have a <a href="/blog">blog.</a><br>
|
||||
If you have questions or input for me please send me an E-Mail to {{ email }}
|
||||
This is my blog. If you have questions or input for me please send me an E-Mail to {{ email }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
|
@ -13,13 +13,16 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<h1><a href="/blog" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right"><a href="/">Home</a> <a href="/blog">Last 5 posts</a></p>
|
||||
<h1><a href="/" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right">
|
||||
<a href="/about">About</a>
|
||||
<a href="/">Last 5 posts</a>
|
||||
</p>
|
||||
<ul>
|
||||
{% for post in posts %}
|
||||
<article>
|
||||
<div>
|
||||
<a href="/blog/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
<a href="/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h2 class="post-title">{{ post.title }}</h2>
|
||||
|
|
|
@ -13,12 +13,16 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<h1><a href="/blog" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right"><a href="/">Home</a> <a href="/blog">Last 5 Posts</a> <a href="/blog/all">All Posts</a></p>
|
||||
<h1><a href="/" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right">
|
||||
<a href="/">Home</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="/all">All Posts</a>
|
||||
</p>
|
||||
<ul>
|
||||
<article>
|
||||
<div>
|
||||
<a href="/blog/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
<a href="/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h2 class="post-title">{{ post.title }}</h2>
|
||||
|
|
|
@ -13,13 +13,15 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<h1><a href="/blog" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right"><a href="/">Home</a> <a href="/blog/all">All Posts</a></p>
|
||||
<h1><a href="/" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<p style="text-align: right">
|
||||
<a href="/about">About</a>
|
||||
<a href="/all">All Posts</a></p>
|
||||
<ul>
|
||||
{% for post in posts %}
|
||||
<article>
|
||||
<div>
|
||||
<a href="/blog/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
<a href="/id/{{ post.id }}" class="post-link">[link]</a>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h2 class="post-title">{{ post.title }}</h2>
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<link rel="shortcut icon" type="image/jpg" href="/static/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="/blog" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<h1><a href="/" class="post-link" style="text-decoration:none;color:black;">{{ username }}' blog</a></h1>
|
||||
<h2>Edit posts</h2>
|
||||
<ul style="list-style: none;">
|
||||
{% for post in posts %}
|
||||
<li><a href="/blog/edit/{{ post.id }}">{{ post.title }}</a></li>
|
||||
<li><a href="/edit/{{ post.id }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
|
|
13
crablog.env
13
crablog.env
|
@ -1,6 +1,9 @@
|
|||
SUBMIT_TOKEN=mysupersecretpw
|
||||
USERNAME=yourusername
|
||||
EMAIL=me@mydomain.tld
|
||||
BIND_PORT=8000
|
||||
USERNAME=
|
||||
EMAIL=
|
||||
TWITTER_ACCOUNT=
|
||||
GITHUB_ACCOUNT=
|
||||
SUBMIT_TOKEN=Submit!123 # token needed for submitting
|
||||
GITHUB_ACCOUNT=usernam3
|
||||
TWITTER_ACCOUNT=usernam3
|
||||
MASTODON_ACCOUNT=usernam3@mastodon.social
|
||||
REDDIT_ACCOUNT=usernam3
|
||||
DISCORD_ACCOUNT=usernam3
|
|
@ -1,7 +1,8 @@
|
|||
version: "3.8"
|
||||
version: "3.0"
|
||||
services:
|
||||
crablog:
|
||||
build: .
|
||||
image: mtrx1337/crablog
|
||||
ports:
|
||||
- 8000:8000
|
||||
hostname: crablog
|
||||
|
|
179
site/Cargo.lock
generated
179
site/Cargo.lock
generated
|
@ -57,9 +57,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "404df68c297f73b8d36c9c9056404913d25905a8f80127b0e5fe147c9c4b9f02"
|
||||
checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-connect",
|
||||
|
@ -92,7 +92,7 @@ dependencies = [
|
|||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project 1.0.1",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2"
|
||||
dependencies = [
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -233,9 +233,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "3.2.0"
|
||||
version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88344b7a5ef27e5e09e73565379f69273dd3e2d29e82afc381b84d170d0a5631"
|
||||
checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
@ -278,7 +278,7 @@ checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -313,7 +313,7 @@ checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -335,9 +335,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
|||
|
||||
[[package]]
|
||||
name = "awc"
|
||||
version = "2.0.1"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "425980a1e58e5030a3e4b065a3d577c8f0e16142ea9d81f30614eae810c98577"
|
||||
checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
@ -351,7 +351,7 @@ dependencies = [
|
|||
"log",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
|
@ -572,12 +572,12 @@ checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
|
|||
|
||||
[[package]]
|
||||
name = "crablog"
|
||||
version = "0.2.3"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-web",
|
||||
"chrono",
|
||||
"diesel 1.4.5",
|
||||
"diesel 1.4.6",
|
||||
"diesel_codegen",
|
||||
"env_logger",
|
||||
"once_cell",
|
||||
|
@ -616,7 +616,7 @@ checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -636,9 +636,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
|
||||
checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"chrono",
|
||||
|
@ -667,7 +667,7 @@ checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -694,12 +694,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
|
@ -724,14 +718,14 @@ dependencies = [
|
|||
"heck",
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e"
|
||||
checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
|
@ -835,7 +829,7 @@ dependencies = [
|
|||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -912,6 +906,17 @@ dependencies = [
|
|||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.23.0"
|
||||
|
@ -933,9 +938,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "178270263374052c40502e9f607134947de75302c1348d1a0e31db67c1691446"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore",
|
||||
|
@ -1322,9 +1327,9 @@ checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.5.2"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
|
||||
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
|
@ -1408,7 +1413,7 @@ dependencies = [
|
|||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1448,7 +1453,7 @@ checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1459,7 +1464,7 @@ checksum = "81a4ffa594b66bff340084d4081df649a7dc049ac8d7fc458d8e628bfbbb2f86"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1500,9 +1505,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.1",
|
||||
]
|
||||
|
@ -1534,11 +1539,23 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.15",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.0",
|
||||
"rand_core 0.6.2",
|
||||
"rand_hc 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1548,7 +1565,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1557,7 +1584,16 @@ version = "0.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1566,7 +1602,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
dependencies = [
|
||||
"rand_core 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1656,22 +1701,22 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.117"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
|
||||
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.117"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
|
||||
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1687,14 +1732,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
|
||||
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1803,7 +1848,7 @@ dependencies = [
|
|||
"quote 1.0.7",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1819,7 +1864,7 @@ dependencies = [
|
|||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1841,9 +1886,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.48"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
|
||||
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
|
@ -1861,9 +1906,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.5.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1381c83828bedd5ce4e59473110afa5381ffe523406d9ade4b77c9f7be70ff9a"
|
||||
checksum = "b64b021b8d3ab1f59ceae9e6cd1c26c8e7ce0322a9ebfff6c0e22b3b66938935"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
|
@ -1873,7 +1918,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"rand",
|
||||
"rand 0.8.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1907,7 +1952,7 @@ checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1974,7 +2019,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"standback",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2070,7 +2115,7 @@ dependencies = [
|
|||
"idna",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
@ -2218,9 +2263,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"sha1",
|
||||
|
@ -2245,7 +2290,7 @@ dependencies = [
|
|||
"nom",
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2320,7 +2365,7 @@ dependencies = [
|
|||
"log",
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -2342,7 +2387,7 @@ checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
"syn 1.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "crablog"
|
||||
version = "0.2.3"
|
||||
version = "0.3.1"
|
||||
authors = ["Leonard Lorenz <dev@leonardlorenz.de>"]
|
||||
edition = "2018"
|
||||
|
||||
|
@ -8,20 +8,20 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
actix-web = "3.2.0"
|
||||
actix-web = "3.3.2"
|
||||
actix-files = "0.4.0"
|
||||
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_json = "1.0.59"
|
||||
serde_derive = "1.0.117"
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
serde_json = "*"
|
||||
serde_derive = "*"
|
||||
|
||||
diesel = { version = "1.4.5", default-features = false, features = ["sqlite", "chrono"] }
|
||||
diesel = { version = "1.4.6", default-features = false, features = ["sqlite", "chrono"] }
|
||||
diesel_codegen = { version = "0.16.1", default-features = false }
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v5"] }
|
||||
uuid = { version = "0.8.2", features = ["serde", "v5"] }
|
||||
|
||||
tera = "1.5.0"
|
||||
tera = "1.8.0"
|
||||
|
||||
once_cell = "1.5.2"
|
||||
once_cell = "1.7.2"
|
||||
|
||||
env_logger = "0.8.2"
|
||||
env_logger = "0.8.3"
|
||||
|
|
101
site/src/main.rs
101
site/src/main.rs
|
@ -9,50 +9,89 @@ extern crate serde_derive;
|
|||
extern crate tera;
|
||||
|
||||
use actix_files as fs;
|
||||
use actix_web::{App, HttpServer, middleware::Logger};
|
||||
use actix_web::{middleware::Logger, App, HttpServer};
|
||||
use env_logger::Env;
|
||||
use tera::Tera;
|
||||
use std::{env, sync::RwLock, collections::HashMap};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, env, sync::RwLock};
|
||||
use tera::Tera;
|
||||
|
||||
pub static CONFIG_MAP: Lazy<RwLock<HashMap<String, String>>> = Lazy::new(|| {
|
||||
let mut config: HashMap<String, String> = HashMap::new();
|
||||
config.insert(String::from("SUBMIT_TOKEN"), env::var("SUBMIT_TOKEN").expect("SUBMIT_TOKEN variable not set."));
|
||||
config.insert(String::from("ROOT_PATH"), env::var("ROOT_PATH").expect("ROOT_PATH variable not set."));
|
||||
config.insert(String::from("USERNAME"), env::var("USERNAME").expect("USERNAME variable not set."));
|
||||
config.insert(String::from("EMAIL"), env::var("EMAIL").expect("EMAIL variable not set."));
|
||||
config.insert(String::from("BIND_PORT"), env::var("BIND_PORT").expect("BIND_PORT variable not set."));
|
||||
if let Ok(acc) = env::var("GITHUB_ACCOUNT") {
|
||||
config.insert(String::from("GITHUB_ACCOUNT"), acc.clone());
|
||||
|
||||
let required_env_vars = [
|
||||
"SUBMIT_TOKEN",
|
||||
"ROOT_PATH",
|
||||
"USERNAME",
|
||||
"EMAIL",
|
||||
"BIND_PORT",
|
||||
];
|
||||
|
||||
let optional_env_vars = [
|
||||
"GITHUB_ACCOUNT",
|
||||
"TWITTER_ACCOUNT",
|
||||
"MASTODON_ACCOUNT",
|
||||
"DISCORD_ACCOUNT",
|
||||
"REDDIT_ACCOUNT",
|
||||
];
|
||||
|
||||
// Test if variable is set. If not, panic.
|
||||
let mut insert_required_env = |env: &str| {
|
||||
let env_string = String::from(env);
|
||||
config.insert(
|
||||
env_string.clone(), // env var name
|
||||
env::var(env_string).expect(format!("`{}` variable not set.", env).as_str()), // env var content
|
||||
)
|
||||
};
|
||||
|
||||
for var in required_env_vars.iter() {
|
||||
insert_required_env(var);
|
||||
}
|
||||
if let Ok(acc) = env::var("TWITTER_ACCOUNT") {
|
||||
config.insert(String::from("TWITTER_ACCOUNT"), acc.clone());
|
||||
|
||||
// Test if variable is set. If it is insert into config.
|
||||
let mut insert_optional_env = |env: &str| {
|
||||
if let Ok(var_content) = env::var(String::from(env)) {
|
||||
config.insert(String::from(env), var_content.clone());
|
||||
}
|
||||
if let Ok(acc) = env::var("MASTODON_ACCOUNT") {
|
||||
config.insert(String::from("MASTODON_ACCOUNT"), acc.clone());
|
||||
}
|
||||
if let Ok(acc) = env::var("DISCORD_ACCOUNT") {
|
||||
config.insert(String::from("DISCORD_ACCOUNT"), acc.clone());
|
||||
}
|
||||
if let Ok(acc) = env::var("REDDIT_ACCOUNT") {
|
||||
config.insert(String::from("REDDIT_ACCOUNT"), acc.clone());
|
||||
};
|
||||
|
||||
for var in optional_env_vars.iter() {
|
||||
insert_optional_env(var);
|
||||
}
|
||||
|
||||
// Print some info about the current configuration
|
||||
println!("Submit token = `{}`", config.get("SUBMIT_TOKEN").unwrap());
|
||||
println!(
|
||||
"Current working directory = `{}`",
|
||||
env::current_dir().unwrap().to_str().unwrap()
|
||||
);
|
||||
println!("Root path = `{}`", config.get("ROOT_PATH").unwrap());
|
||||
println!(
|
||||
"Template path = `{}/templates/*`",
|
||||
config.get("ROOT_PATH").unwrap()
|
||||
);
|
||||
println!("Launching on 0.0.0.0:{}", config.get("BIND_PORT").unwrap());
|
||||
RwLock::new(config)
|
||||
});
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
HttpServer::new(|| {
|
||||
|
||||
let mut tera = Tera::new(format!("{}{}", CONFIG_MAP.read().unwrap().get("ROOT_PATH").unwrap(), "/templates/*").as_str()).unwrap();
|
||||
let mut tera = Tera::new(
|
||||
format!(
|
||||
"{}{}",
|
||||
CONFIG_MAP.read().unwrap().get("ROOT_PATH").unwrap(),
|
||||
"/templates/*"
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
tera.autoescape_on(vec![".sql"]);
|
||||
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info"));
|
||||
|
||||
App::new()
|
||||
.data(tera)
|
||||
.service(routes::root)
|
||||
.service(routes::about)
|
||||
.service(routes::blog)
|
||||
.service(routes::blog_all)
|
||||
.service(routes::blog_by_id)
|
||||
|
@ -64,10 +103,20 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(api::blog_edit_post)
|
||||
.service(api::blog_hide_post)
|
||||
.service(api::blog_delete_post)
|
||||
.service(fs::Files::new("/static", format!("{}{}", CONFIG_MAP.read().unwrap().get("ROOT_PATH").unwrap(), "/static")))
|
||||
.service(fs::Files::new(
|
||||
"/static",
|
||||
format!(
|
||||
"{}{}",
|
||||
CONFIG_MAP.read().unwrap().get("ROOT_PATH").unwrap(),
|
||||
"/static"
|
||||
),
|
||||
))
|
||||
.wrap(Logger::new("%a %r %t"))
|
||||
})
|
||||
.bind(format!("0.0.0.0:{}", CONFIG_MAP.read().unwrap().get("BIND_PORT").unwrap()))?
|
||||
.bind(format!(
|
||||
"0.0.0.0:{}",
|
||||
CONFIG_MAP.read().unwrap().get("BIND_PORT").unwrap()
|
||||
))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::db;
|
||||
|
||||
use actix_web::{get, http::StatusCode, web, HttpResponse, Error, error};
|
||||
use tera::Context;
|
||||
use super::CONFIG_MAP;
|
||||
use actix_web::{error, get, http::StatusCode, web, Error, HttpResponse};
|
||||
use tera::Context;
|
||||
|
||||
/// tests if the post id is a valid i32 integer bigger than zero
|
||||
/// assert(!(id_valid("2147483648").0))
|
||||
|
@ -33,10 +33,13 @@ pub fn replace_br_tags(x: &str) -> String {
|
|||
x.replace("<br>", "\n")
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn root(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
#[get("/about")]
|
||||
async fn about(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
context.insert("username", CONFIG_MAP.read().unwrap().get("USERNAME").unwrap());
|
||||
context.insert(
|
||||
"username",
|
||||
CONFIG_MAP.read().unwrap().get("USERNAME").unwrap(),
|
||||
);
|
||||
context.insert("email", CONFIG_MAP.read().unwrap().get("EMAIL").unwrap());
|
||||
if let Some(acc) = CONFIG_MAP.read().unwrap().get("GITHUB_ACCOUNT") {
|
||||
context.insert("github_account", acc);
|
||||
|
@ -54,89 +57,113 @@ async fn root(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
|||
context.insert("reddit_account", acc);
|
||||
}
|
||||
|
||||
let result = tmpl.render("index.html", &context)
|
||||
let result = tmpl
|
||||
.render("about.html", &context)
|
||||
.map_err(|e| error::ErrorInternalServerError(format!("Template error\n{}", e)))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
}
|
||||
|
||||
#[get("/blog")]
|
||||
#[get("/")]
|
||||
async fn blog(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
let posts = db::get_last_five_posts();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("posts", &posts);
|
||||
context.insert("username", CONFIG_MAP.read().unwrap().get("USERNAME").unwrap());
|
||||
context.insert(
|
||||
"username",
|
||||
CONFIG_MAP.read().unwrap().get("USERNAME").unwrap(),
|
||||
);
|
||||
|
||||
let result = tmpl.render("blog.html", &context)
|
||||
let result = tmpl
|
||||
.render("blog.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
}
|
||||
|
||||
#[get("/blog/all")]
|
||||
#[get("/all")]
|
||||
async fn blog_all(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
let posts = db::get_all_posts();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("posts", &posts);
|
||||
context.insert("username", CONFIG_MAP.read().unwrap().get("USERNAME").unwrap());
|
||||
context.insert(
|
||||
"username",
|
||||
CONFIG_MAP.read().unwrap().get("USERNAME").unwrap(),
|
||||
);
|
||||
|
||||
let result = tmpl.render("blog-all-posts.html", &context)
|
||||
let result = tmpl
|
||||
.render("blog-all-posts.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
}
|
||||
|
||||
#[get("/blog/id/{post_id}")]
|
||||
async fn blog_by_id(tmpl: web::Data<tera::Tera>, web::Path(post_id): web::Path<std::string::String>) -> Result<HttpResponse, Error> {
|
||||
#[get("/id/{post_id}")]
|
||||
async fn blog_by_id(
|
||||
tmpl: web::Data<tera::Tera>,
|
||||
web::Path(post_id): web::Path<std::string::String>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (valid, id) = id_valid(post_id);
|
||||
if valid {
|
||||
let post = db::get_post_by_id(id as i32);
|
||||
|
||||
if !post.published {
|
||||
return Ok(HttpResponse::new(StatusCode::UNAUTHORIZED))
|
||||
return Ok(HttpResponse::new(StatusCode::UNAUTHORIZED));
|
||||
}
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("post", &post);
|
||||
context.insert("username", CONFIG_MAP.read().unwrap().get("USERNAME").unwrap());
|
||||
context.insert(
|
||||
"username",
|
||||
CONFIG_MAP.read().unwrap().get("USERNAME").unwrap(),
|
||||
);
|
||||
|
||||
let result = tmpl.render("blog-by-id.html", &context)
|
||||
let result = tmpl
|
||||
.render("blog-by-id.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(result));
|
||||
} else {
|
||||
return Ok(HttpResponse::new(StatusCode::NOT_FOUND))
|
||||
return Ok(HttpResponse::new(StatusCode::NOT_FOUND));
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/blog/submit")]
|
||||
#[get("/submit")]
|
||||
async fn blog_submit(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
context.insert("title", "");
|
||||
context.insert("body", "");
|
||||
|
||||
let result = tmpl.render("submit.html", &context)
|
||||
let result = tmpl
|
||||
.render("submit.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(result));
|
||||
}
|
||||
|
||||
#[get("/blog/edit")]
|
||||
#[get("/edit")]
|
||||
async fn blog_edit(tmpl: web::Data<tera::Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
context.insert("posts", &db::get_all_posts());
|
||||
context.insert("username", CONFIG_MAP.read().unwrap().get("USERNAME").unwrap());
|
||||
context.insert(
|
||||
"username",
|
||||
CONFIG_MAP.read().unwrap().get("USERNAME").unwrap(),
|
||||
);
|
||||
|
||||
let result = tmpl.render("edit.html", &context)
|
||||
let result = tmpl
|
||||
.render("edit.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
}
|
||||
|
||||
#[get("/blog/edit/{post_id}")]
|
||||
async fn blog_edit_by_id(tmpl: web::Data<tera::Tera>, web::Path(post_id): web::Path<std::string::String>) -> Result<HttpResponse, Error> {
|
||||
#[get("/edit/{post_id}")]
|
||||
async fn blog_edit_by_id(
|
||||
tmpl: web::Data<tera::Tera>,
|
||||
web::Path(post_id): web::Path<std::string::String>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (valid, id) = id_valid(post_id);
|
||||
if valid {
|
||||
let mut post = db::get_post_by_id(id as i32);
|
||||
|
@ -149,7 +176,8 @@ async fn blog_edit_by_id(tmpl: web::Data<tera::Tera>, web::Path(post_id): web::P
|
|||
context.insert("body", &post.body);
|
||||
context.insert("id", &id);
|
||||
|
||||
let result = tmpl.render("edit-form.html", &context)
|
||||
let result = tmpl
|
||||
.render("edit-form.html", &context)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(result))
|
||||
|
|
Loading…
Reference in a new issue