remove the client side rendering version of the app
This commit is contained in:
parent
4bbbe3c746
commit
fef33dc2d4
112 changed files with 0 additions and 9014 deletions
|
@ -1,4 +0,0 @@
|
||||||
[alias]
|
|
||||||
loco = "run --"
|
|
||||||
loco-tool = "run --bin tool --"
|
|
||||||
playground = "run --example playground"
|
|
|
@ -1,6 +0,0 @@
|
||||||
POSTGRES_DB=loco_app
|
|
||||||
POSTGRES_USER=loco
|
|
||||||
POSTGRES_PASSWORD=loco
|
|
||||||
DATABASE_URL=postgres://loco:loco@db:5432/loco_app
|
|
||||||
REDIS_URL=redis://redis:6379
|
|
||||||
MAILER_HOST=mailer
|
|
|
@ -1,8 +0,0 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/rust:1
|
|
||||||
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -y install --no-install-recommends postgresql-client \
|
|
||||||
&& cargo install sea-orm-cli cargo-insta \
|
|
||||||
&& chown -R vscode /usr/local/cargo
|
|
||||||
|
|
||||||
COPY .env /.env
|
|
|
@ -1,49 +0,0 @@
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
command: sleep infinity
|
|
||||||
networks:
|
|
||||||
- db
|
|
||||||
- redis
|
|
||||||
- mailer
|
|
||||||
volumes:
|
|
||||||
- ../..:/workspaces:cached
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
db:
|
|
||||||
image: postgres:15.3-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
networks:
|
|
||||||
- db
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
redis:
|
|
||||||
image: redis:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
networks:
|
|
||||||
- redis
|
|
||||||
mailer:
|
|
||||||
image: mailtutan/mailtutan:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- 1080:1080
|
|
||||||
- 1025:1025
|
|
||||||
networks:
|
|
||||||
- mailer
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
|
|
||||||
|
|
||||||
networks:
|
|
||||||
db:
|
|
||||||
redis:
|
|
||||||
mailer:
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Loco",
|
|
||||||
"dockerComposeFile": "compose.yaml",
|
|
||||||
"service": "app",
|
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
|
||||||
"forwardPorts": [
|
|
||||||
5150
|
|
||||||
]
|
|
||||||
}
|
|
102
nixin_farm/.github/workflows/ci.yaml
vendored
102
nixin_farm/.github/workflows/ci.yaml
vendored
|
@ -1,102 +0,0 @@
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUST_TOOLCHAIN: stable
|
|
||||||
TOOLCHAIN_PROFILE: minimal
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rustfmt:
|
|
||||||
name: Check Style
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
|
||||||
components: rustfmt
|
|
||||||
- name: Run cargo fmt
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
|
|
||||||
clippy:
|
|
||||||
name: Run Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
|
||||||
- name: Setup Rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Run cargo clippy
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Run Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis
|
|
||||||
options: >-
|
|
||||||
--health-cmd "redis-cli ping"
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
postgres:
|
|
||||||
image: postgres
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: postgres_test
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
# Set health checks to wait until postgres has started
|
|
||||||
options: --health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
|
||||||
- name: Setup Rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Run cargo test
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all-features --all
|
|
||||||
env:
|
|
||||||
REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}}
|
|
||||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres_test
|
|
||||||
|
|
19
nixin_farm/.gitignore
vendored
19
nixin_farm/.gitignore
vendored
|
@ -1,19 +0,0 @@
|
||||||
**/config/local.yaml
|
|
||||||
**/config/*.local.yaml
|
|
||||||
**/config/production.yaml
|
|
||||||
|
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# include cargo lock
|
|
||||||
!Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
*.sqlite
|
|
|
@ -1,2 +0,0 @@
|
||||||
max_width = 100
|
|
||||||
use_small_heuristics = "Default"
|
|
5552
nixin_farm/Cargo.lock
generated
5552
nixin_farm/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,55 +0,0 @@
|
||||||
[workspace]
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "nixin_farm"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
default-run = "nixin_farm-cli"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
|
|
||||||
loco-rs = { version = "0.10.1" }
|
|
||||||
migration = { path = "migration" }
|
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1"
|
|
||||||
tokio = { version = "1.33.0", default-features = false }
|
|
||||||
async-trait = "0.1.74"
|
|
||||||
tracing = "0.1.40"
|
|
||||||
chrono = "0.4"
|
|
||||||
validator = { version = "0.18" }
|
|
||||||
sea-orm = { version = "1.0.0", features = [
|
|
||||||
"sqlx-sqlite",
|
|
||||||
"sqlx-postgres",
|
|
||||||
"runtime-tokio-rustls",
|
|
||||||
"macros",
|
|
||||||
] }
|
|
||||||
|
|
||||||
axum = "0.7.5"
|
|
||||||
include_dir = "0.7"
|
|
||||||
uuid = { version = "1.6.0", features = ["v4"] }
|
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
|
|
||||||
|
|
||||||
# view engine i18n
|
|
||||||
fluent-templates = { version = "0.8.0", features = ["tera"] }
|
|
||||||
unic-langid = "0.9.4"
|
|
||||||
# /view engine
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "nixin_farm-cli"
|
|
||||||
path = "src/bin/main.rs"
|
|
||||||
required-features = []
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "tool"
|
|
||||||
path = "src/bin/tool.rs"
|
|
||||||
required-features = []
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serial_test = "3.1.1"
|
|
||||||
rstest = "0.21.0"
|
|
||||||
loco-rs = { version = "0.10.1", features = ["testing"] }
|
|
||||||
insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] }
|
|
|
@ -1,58 +0,0 @@
|
||||||
# Welcome to Loco :train:
|
|
||||||
|
|
||||||
[Loco](https://loco.rs) is a web and API framework running on Rust.
|
|
||||||
|
|
||||||
This is the **SaaS starter** which includes a `User` model and authentication based on JWT.
|
|
||||||
It also include configuration sections that help you pick either a frontend or a server-side template set up for your fullstack server.
|
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo loco start
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ cargo loco start
|
|
||||||
Finished dev [unoptimized + debuginfo] target(s) in 21.63s
|
|
||||||
Running `target/debug/myapp start`
|
|
||||||
|
|
||||||
:
|
|
||||||
:
|
|
||||||
:
|
|
||||||
|
|
||||||
controller/app_routes.rs:203: [Middleware] Adding log trace id
|
|
||||||
|
|
||||||
▄ ▀
|
|
||||||
▀ ▄
|
|
||||||
▄ ▀ ▄ ▄ ▄▀
|
|
||||||
▄ ▀▄▄
|
|
||||||
▄ ▀ ▀ ▀▄▀█▄
|
|
||||||
▀█▄
|
|
||||||
▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█
|
|
||||||
██████ █████ ███ █████ ███ █████ ███ ▀█
|
|
||||||
██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄
|
|
||||||
██████ █████ ███ █████ █████ ███ ████▄
|
|
||||||
██████ █████ ███ █████ ▄▄▄ █████ ███ █████
|
|
||||||
██████ █████ ███ ████ ███ █████ ███ ████▀
|
|
||||||
▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀
|
|
||||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
||||||
https://loco.rs
|
|
||||||
|
|
||||||
environment: development
|
|
||||||
database: automigrate
|
|
||||||
logger: debug
|
|
||||||
compilation: debug
|
|
||||||
modes: server
|
|
||||||
|
|
||||||
listening on http://localhost:5150
|
|
||||||
```
|
|
||||||
|
|
||||||
## Full Stack Serving
|
|
||||||
|
|
||||||
You can check your [configuration](config/development.yaml) to pick either frontend setup or server-side rendered template, and activate the relevant configuration sections.
|
|
||||||
|
|
||||||
|
|
||||||
## Getting help
|
|
||||||
|
|
||||||
Check out [a quick tour](https://loco.rs/docs/getting-started/tour/) or [the complete guide](https://loco.rs/docs/getting-started/guide/).
|
|
|
@ -1,4 +0,0 @@
|
||||||
hello-world = Hallo Welt!
|
|
||||||
greeting = Hallochen { $name }!
|
|
||||||
.placeholder = Hallo Freund!
|
|
||||||
about = Uber
|
|
|
@ -1,10 +0,0 @@
|
||||||
hello-world = Hello World!
|
|
||||||
greeting = Hello { $name }!
|
|
||||||
.placeholder = Hello Friend!
|
|
||||||
about = About
|
|
||||||
simple = simple text
|
|
||||||
reference = simple text with a reference: { -something }
|
|
||||||
parameter = text with a { $param }
|
|
||||||
parameter2 = text one { $param } second { $multi-word-param }
|
|
||||||
email = text with an EMAIL("example@example.org")
|
|
||||||
fallback = this should fall back
|
|
|
@ -1 +0,0 @@
|
||||||
-something = foo
|
|
|
@ -1,3 +0,0 @@
|
||||||
<html><body>
|
|
||||||
not found :-(
|
|
||||||
</body></html>
|
|
Binary file not shown.
Before Width: | Height: | Size: 298 KiB |
|
@ -1,26 +0,0 @@
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>{% block title %}{% endblock title %}</title>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"></script>
|
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
|
|
||||||
{% block head %}
|
|
||||||
|
|
||||||
{% endblock head %}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="prose p-10">
|
|
||||||
<div id="content">
|
|
||||||
{% block content %}
|
|
||||||
{% endblock content %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
|
|
||||||
{% endblock js %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<html><body>
|
|
||||||
<img src="/static/image.png" width="200"/>
|
|
||||||
<br/>
|
|
||||||
find this tera template at <code>assets/views/home/hello.html</code>:
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
{{ t(key="hello-world", lang="en-US") }},
|
|
||||||
<br/>
|
|
||||||
{{ t(key="hello-world", lang="de-DE") }}
|
|
||||||
|
|
||||||
</body></html>
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
Create server
|
|
||||||
{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="mb-10">
|
|
||||||
<form hx-post="/servers" hx-ext="submitjson">
|
|
||||||
<h1>Create new server</h1>
|
|
||||||
<div class="mb-5">
|
|
||||||
<div>
|
|
||||||
<label>name</label>
|
|
||||||
<br />
|
|
||||||
<input id="name" name="name" type="text" value=""/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>domain</label>
|
|
||||||
<br />
|
|
||||||
<input id="domain" name="domain" type="text" value=""/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class=" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white" type="submit">Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
<script>
|
|
||||||
htmx.defineExtension('submitjson', {
|
|
||||||
onEvent: function (name, evt) {
|
|
||||||
if (name === "htmx:configRequest") {
|
|
||||||
evt.detail.headers['Content-Type'] = "application/json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
encodeParameters: function (xhr, parameters, elt) {
|
|
||||||
const json = {};
|
|
||||||
for (const [key, value] of Object.entries(parameters)) {
|
|
||||||
const inputType = elt.querySelector(`[name=${key}]`).type;
|
|
||||||
if (inputType === 'number') {
|
|
||||||
json[key] = parseFloat(value);
|
|
||||||
} else if (inputType === 'checkbox') {
|
|
||||||
json[key] = elt.querySelector(`[name=${key}]`).checked;
|
|
||||||
} else {
|
|
||||||
json[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(json);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock js %}
|
|
|
@ -1,73 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
Edit server: {{ item.id }}
|
|
||||||
{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Edit server: {{ item.id }}</h1>
|
|
||||||
<div class="mb-10">
|
|
||||||
<form hx-post="/servers/{{ item.id }}" hx-ext="submitjson" hx-target="#success-message">
|
|
||||||
<div class="mb-5">
|
|
||||||
<div>
|
|
||||||
<label>name</label>
|
|
||||||
<br />
|
|
||||||
<input id="name" name="name" type="text" value="{{item.name}}"></input>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>domain</label>
|
|
||||||
<br />
|
|
||||||
<input id="domain" name="domain" type="text" value="{{item.domain}}"></input>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<button class=" text-xs py-3 px-6 rounded-lg bg-gray-900 text-white" type="submit">Submit</button>
|
|
||||||
<button class="text-xs py-3 px-6 rounded-lg bg-red-600 text-white"
|
|
||||||
onclick="confirmDelete(event)">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="success-message" class="mt-4"></div>
|
|
||||||
<br />
|
|
||||||
<a href="/servers">Back to server</a>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
<script>
|
|
||||||
htmx.defineExtension('submitjson', {
|
|
||||||
onEvent: function (name, evt) {
|
|
||||||
if (name === "htmx:configRequest") {
|
|
||||||
evt.detail.headers['Content-Type'] = "application/json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
encodeParameters: function (xhr, parameters, elt) {
|
|
||||||
const json = {};
|
|
||||||
for (const [key, value] of Object.entries(parameters)) {
|
|
||||||
const inputType = elt.querySelector(`[name=${key}]`).type;
|
|
||||||
if (inputType === 'number') {
|
|
||||||
json[key] = parseFloat(value);
|
|
||||||
} else if (inputType === 'checkbox') {
|
|
||||||
json[key] = elt.querySelector(`[name=${key}]`).checked;
|
|
||||||
} else {
|
|
||||||
json[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(json);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
function confirmDelete(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (confirm("Are you sure you want to delete this item?")) {
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.open("DELETE", "/servers/{{ item.id }}", true);
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
|
||||||
window.location.href = "/servers";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock js %}
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
List of server
|
|
||||||
{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>server</h1>
|
|
||||||
<div class="mb-10">
|
|
||||||
{% for item in items %}
|
|
||||||
<div class="mb-5">
|
|
||||||
<div>
|
|
||||||
<label><b>{{"name" | capitalize }}:</b> {{item.name}}</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label><b>{{"domain" | capitalize }}:</b> {{item.domain}}</label>
|
|
||||||
</div>
|
|
||||||
<a href="/servers/{{ item.id }}/edit">Edit</a>
|
|
||||||
<a href="/servers/{{ item.id }}">View</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<a href="/servers/new">New server</a>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,19 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
View server: {{ item.id }}
|
|
||||||
{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>View server: {{ item.id }}</h1>
|
|
||||||
<div class="mb-10">
|
|
||||||
<div>
|
|
||||||
<label><b>{{"name" | capitalize }}:</b> {{item.name}}</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label><b>{{"domain" | capitalize }}:</b> {{item.domain}}</label>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<a href="/servers">Back to servers</a>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
|
@ -1,138 +0,0 @@
|
||||||
# Loco configuration file documentation
|
|
||||||
|
|
||||||
# Application logging configuration
|
|
||||||
logger:
|
|
||||||
# Enable or disable logging.
|
|
||||||
enable: true
|
|
||||||
# Enable pretty backtrace (sets RUST_BACKTRACE=1)
|
|
||||||
pretty_backtrace: true
|
|
||||||
# Log level, options: trace, debug, info, warn or error.
|
|
||||||
level: debug
|
|
||||||
# Define the logging format. options: compact, pretty or json
|
|
||||||
format: compact
|
|
||||||
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
|
|
||||||
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
|
|
||||||
# override_filter: trace
|
|
||||||
|
|
||||||
# Web server configuration
|
|
||||||
server:
|
|
||||||
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
|
|
||||||
port: 5150
|
|
||||||
# The UI hostname or IP address that mailers will point to.
|
|
||||||
host: http://localhost
|
|
||||||
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
|
|
||||||
middlewares:
|
|
||||||
# #############################################
|
|
||||||
# Full stack SaaS asset serving
|
|
||||||
# #############################################
|
|
||||||
#
|
|
||||||
# You can choose from:
|
|
||||||
#
|
|
||||||
# (1) Server-side rendered templates (like erb in Ruby, ejs in nodejs) based on the Tera templating engine.
|
|
||||||
# (2) Client-side frontend set up
|
|
||||||
#
|
|
||||||
# You can also choose both.
|
|
||||||
#
|
|
||||||
# Uncomment the relevant sections below:
|
|
||||||
|
|
||||||
#
|
|
||||||
# (1) Server-side static assets config
|
|
||||||
# ====================================
|
|
||||||
#
|
|
||||||
# for use with the view_engine in initializers/view_engine.rs
|
|
||||||
# (server-block-start)
|
|
||||||
# static:
|
|
||||||
# enable: true
|
|
||||||
# must_exist: true
|
|
||||||
# precompressed: false
|
|
||||||
# folder:
|
|
||||||
# uri: "/static"
|
|
||||||
# path: "assets/static"
|
|
||||||
# fallback: "assets/static/404.html"
|
|
||||||
# (server-block-end)
|
|
||||||
|
|
||||||
#
|
|
||||||
# (2) Client side app static config
|
|
||||||
# =================================
|
|
||||||
#
|
|
||||||
# Note that you need to go in `frontend` and run your frontend build first,
|
|
||||||
# e.g.: $ npm install & npm build
|
|
||||||
#
|
|
||||||
static:
|
|
||||||
enable: true
|
|
||||||
must_exist: true
|
|
||||||
precompressed: false
|
|
||||||
folder:
|
|
||||||
uri: "/"
|
|
||||||
path: "frontend/dist"
|
|
||||||
fallback: "frontend/dist/index.html"
|
|
||||||
#
|
|
||||||
|
|
||||||
# Worker Configuration
|
|
||||||
workers:
|
|
||||||
# specifies the worker mode. Options:
|
|
||||||
# - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
|
|
||||||
# - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
|
|
||||||
# - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
|
|
||||||
mode: BackgroundAsync
|
|
||||||
|
|
||||||
# Mailer Configuration.
|
|
||||||
mailer:
|
|
||||||
# SMTP mailer configuration.
|
|
||||||
smtp:
|
|
||||||
# Enable/Disable smtp mailer.
|
|
||||||
enable: true
|
|
||||||
# SMTP server host. e.x localhost, smtp.gmail.com
|
|
||||||
host: {{ get_env(name="MAILER_HOST", default="localhost") }}
|
|
||||||
# SMTP server port
|
|
||||||
port: 1025
|
|
||||||
# Use secure connection (SSL/TLS).
|
|
||||||
secure: false
|
|
||||||
# auth:
|
|
||||||
# user:
|
|
||||||
# password:
|
|
||||||
|
|
||||||
# Initializers Configuration
|
|
||||||
# initializers:
|
|
||||||
# oauth2:
|
|
||||||
# authorization_code: # Authorization code grant type
|
|
||||||
# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
|
|
||||||
# ... other fields
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
database:
|
|
||||||
# Database connection URI
|
|
||||||
uri: {{ get_env(name="DATABASE_URL", default="sqlite://nixin_farm_development.sqlite?mode=rwc") }}
|
|
||||||
# When enabled, the sql query will be logged.
|
|
||||||
enable_logging: false
|
|
||||||
# Set the timeout duration when acquiring a connection.
|
|
||||||
connect_timeout: {{ get_env(name="DB_CONNECT_TIMEOUT", default="500") }}
|
|
||||||
# Set the idle duration before closing a connection.
|
|
||||||
idle_timeout: 500
|
|
||||||
# Minimum number of connections for a pool.
|
|
||||||
min_connections: 1
|
|
||||||
# Maximum number of connections for a pool.
|
|
||||||
max_connections: 1
|
|
||||||
# Run migration up when application loaded
|
|
||||||
auto_migrate: true
|
|
||||||
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_truncate: false
|
|
||||||
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_recreate: false
|
|
||||||
|
|
||||||
# Queue Configuration
|
|
||||||
queue:
|
|
||||||
kind: Redis
|
|
||||||
# Redis connection URI
|
|
||||||
uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }}
|
|
||||||
# Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_flush: false
|
|
||||||
|
|
||||||
# Authentication Configuration
|
|
||||||
auth:
|
|
||||||
# JWT authentication
|
|
||||||
jwt:
|
|
||||||
# Secret key for token generation and verification
|
|
||||||
secret: H86EQ4JaalMucUtxGnQP
|
|
||||||
# Token expiration time in seconds
|
|
||||||
expiration: 604800 # 7 days
|
|
|
@ -1,101 +0,0 @@
|
||||||
# Loco configuration file documentation
|
|
||||||
|
|
||||||
# Application logging configuration
|
|
||||||
logger:
|
|
||||||
# Enable or disable logging.
|
|
||||||
enable: false
|
|
||||||
# Log level, options: trace, debug, info, warn or error.
|
|
||||||
level: debug
|
|
||||||
# Define the logging format. options: compact, pretty or json
|
|
||||||
format: compact
|
|
||||||
# By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
|
|
||||||
# Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
|
|
||||||
# override_filter: trace
|
|
||||||
|
|
||||||
# Web server configuration
|
|
||||||
server:
|
|
||||||
# Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
|
|
||||||
port: 5150
|
|
||||||
# The UI hostname or IP address that mailers will point to.
|
|
||||||
host: http://localhost
|
|
||||||
# Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
|
|
||||||
middlewares:
|
|
||||||
static:
|
|
||||||
enable: true
|
|
||||||
precompressed: false
|
|
||||||
must_exist: false
|
|
||||||
folder:
|
|
||||||
uri: "/"
|
|
||||||
path: "frontend/dist"
|
|
||||||
fallback: "frontend/dist/index.html"
|
|
||||||
|
|
||||||
# Worker Configuration
|
|
||||||
workers:
|
|
||||||
# specifies the worker mode. Options:
|
|
||||||
# - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
|
|
||||||
# - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
|
|
||||||
# - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
|
|
||||||
mode: ForegroundBlocking
|
|
||||||
|
|
||||||
# Mailer Configuration.
|
|
||||||
mailer:
|
|
||||||
# SMTP mailer configuration.
|
|
||||||
smtp:
|
|
||||||
# Enable/Disable smtp mailer.
|
|
||||||
enable: true
|
|
||||||
# SMTP server host. e.x localhost, smtp.gmail.com
|
|
||||||
host: localhost
|
|
||||||
# SMTP server port
|
|
||||||
port: 1025
|
|
||||||
# Use secure connection (SSL/TLS).
|
|
||||||
secure: false
|
|
||||||
# auth:
|
|
||||||
# user:
|
|
||||||
# password:
|
|
||||||
stub: true
|
|
||||||
|
|
||||||
# Initializers Configuration
|
|
||||||
# initializers:
|
|
||||||
# oauth2:
|
|
||||||
# authorization_code: # Authorization code grant type
|
|
||||||
# - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
|
|
||||||
# ... other fields
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
database:
|
|
||||||
# Database connection URI
|
|
||||||
uri: {{get_env(name="DATABASE_URL", default="sqlite://nixin_farm_test.sqlite?mode=rwc")}}
|
|
||||||
# When enabled, the sql query will be logged.
|
|
||||||
enable_logging: false
|
|
||||||
# Set the timeout duration when acquiring a connection.
|
|
||||||
connect_timeout: {{ get_env(name="DB_CONNECT_TIMEOUT", default="500") }}
|
|
||||||
# Set the idle duration before closing a connection.
|
|
||||||
idle_timeout: 500
|
|
||||||
# Minimum number of connections for a pool.
|
|
||||||
min_connections: 1
|
|
||||||
# Maximum number of connections for a pool.
|
|
||||||
max_connections: 1
|
|
||||||
# Run migration up when application loaded
|
|
||||||
auto_migrate: true
|
|
||||||
# Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_truncate: true
|
|
||||||
# Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_recreate: false
|
|
||||||
|
|
||||||
# Queue Configuration
|
|
||||||
queue:
|
|
||||||
kind: Redis
|
|
||||||
# Redis connection URI
|
|
||||||
uri: {{get_env(name="REDIS_URL", default="redis://127.0.0.1")}}
|
|
||||||
# Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
|
|
||||||
dangerously_flush: false
|
|
||||||
|
|
||||||
# Authentication Configuration
|
|
||||||
auth:
|
|
||||||
# JWT authentication
|
|
||||||
jwt:
|
|
||||||
# Secret key for token generation and verification
|
|
||||||
secret: 6GBXpgynnJ82bdkkjBa6
|
|
||||||
# Token expiration time in seconds
|
|
||||||
expiration: 604800 # 7 days
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
#[allow(unused_imports)]
|
|
||||||
use loco_rs::{cli::playground, prelude::*};
|
|
||||||
use nixin_farm::app::App;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> loco_rs::Result<()> {
|
|
||||||
let _ctx = playground::<App>().await?;
|
|
||||||
|
|
||||||
// let active_model: articles::ActiveModel = ActiveModel {
|
|
||||||
// title: Set(Some("how to build apps in 3 steps".to_string())),
|
|
||||||
// content: Set(Some("use Loco: https://loco.rs".to_string())),
|
|
||||||
// ..Default::default()
|
|
||||||
// };
|
|
||||||
// active_model.insert(&ctx.db).await.unwrap();
|
|
||||||
|
|
||||||
// let res = articles::Entity::find().all(&ctx.db).await.unwrap();
|
|
||||||
// println!("{:?}", res);
|
|
||||||
println!("welcome to playground. edit me at `examples/playground.rs`");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
31
nixin_farm/frontend/.gitignore
vendored
31
nixin_farm/frontend/.gitignore
vendored
|
@ -1,31 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist-ssr
|
|
||||||
dist/
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
# Common local dotenv files popularised by Create React App & Next.js
|
|
||||||
# https://rsbuild.dev/guide/advanced/env-vars#env-file
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.production.local
|
|
||||||
.env.test.local
|
|
|
@ -1,42 +0,0 @@
|
||||||
# SaaS Frontend
|
|
||||||
|
|
||||||
## Batteries included
|
|
||||||
|
|
||||||
- [TypeScript](https://www.typescriptlang.org/): A typed superset of JavaScript
|
|
||||||
- [Rsbuild](https://rsbuild.dev/): A Rust-based web build tool
|
|
||||||
- [Biome](https://biomejs.dev/): A Rust-based formatter and sensible linter for the web
|
|
||||||
- [React](https://reactjs.org/): A JavaScript library for building user interfaces
|
|
||||||
|
|
||||||
If you don't like React for some reason, Rsbuild makes it easy to replace it with something else!
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
To get started with the development of the SaaS frontend, follow these steps:
|
|
||||||
|
|
||||||
### 1. Install Packages
|
|
||||||
|
|
||||||
Use the following command to install the required packages using pnpm:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Run in Development Mode
|
|
||||||
|
|
||||||
Once the packages are installed, run your frontend application in development mode with the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start the development frontend server serving via vit
|
|
||||||
|
|
||||||
### 3. Build The application
|
|
||||||
|
|
||||||
To build your application run the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
After the build `dist` folder is ready to served by loco. run loco `cargo loco start` and the frontend application will served via Loco
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"json": {
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "rsbuild dev --open",
|
|
||||||
"build": "rsbuild build",
|
|
||||||
"lint": "biome check src/",
|
|
||||||
"preview": "rsbuild preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18",
|
|
||||||
"react-dom": "^18"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^1",
|
|
||||||
"@rsbuild/core": "^1",
|
|
||||||
"@rsbuild/plugin-react": "^1",
|
|
||||||
"@types/react": "^18",
|
|
||||||
"@types/react-dom": "^18",
|
|
||||||
"typescript": "^5"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { defineConfig } from "@rsbuild/core";
|
|
||||||
import { pluginReact } from "@rsbuild/plugin-react";
|
|
||||||
|
|
||||||
// https://rsbuild.dev/guide/basic/configure-rsbuild
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [pluginReact()],
|
|
||||||
html: {
|
|
||||||
favicon: "src/assets/favicon.ico",
|
|
||||||
title: "Loco SaaS Starter",
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
target: "http://127.0.0.1:5150",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,105 +0,0 @@
|
||||||
export const LocoSplash = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<header className="navbar fixed-top">
|
|
||||||
<div className="container">
|
|
||||||
<a href="https://loco.rs?ref=starter">Loco</a>
|
|
||||||
<ul className="navbar-nav ">
|
|
||||||
<li className="">
|
|
||||||
<a
|
|
||||||
className=""
|
|
||||||
href="https://github.com/loco-rs/loco?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
className="feather feather-github"
|
|
||||||
>
|
|
||||||
<title>Loco GitHub repo</title>
|
|
||||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="">
|
|
||||||
<a
|
|
||||||
className=""
|
|
||||||
href="https://github.com/loco-rs/loco/stargazers?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
className="feather feather-star"
|
|
||||||
>
|
|
||||||
<title>Loco GitHub stars</title>
|
|
||||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="logo">
|
|
||||||
<h1>Loco: SaaS application</h1>
|
|
||||||
<img src="https://loco.rs/icon.svg" className="logo" alt="Loco logo" />
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://loco.rs?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Our Documentation
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/loco-rs/loco?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/loco-rs/loco/issues?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Found a bug?
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/loco-rs/loco/discussions?ref=starter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Needs help?
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
1
nixin_farm/frontend/src/env.d.ts
vendored
1
nixin_farm/frontend/src/env.d.ts
vendored
|
@ -1 +0,0 @@
|
||||||
/// <reference types="@rsbuild/core/types" />
|
|
|
@ -1,100 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Arimo", -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
background: #212529;
|
|
||||||
color: #dee2e6;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-webkit-tap-highlight-color: rgba(29, 45, 53, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #dee2e6;
|
|
||||||
text-decoration: none
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1320px;
|
|
||||||
padding-right: var(--bs-gutter-x, 24px);
|
|
||||||
padding-left: var(--bs-gutter-x, 24px);
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
padding-top: .5rem;
|
|
||||||
padding-bottom: .5rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.navbar-nav li {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed-top {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding-top: 6rem !important
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
border-bottom: 1px solid #2a2f34;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer ul {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer ul li {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 5px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer ul li:not(:last-child) {
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
padding-right: 5px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import { LocoSplash } from "./LocoSplash";
|
|
||||||
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
throw new Error("No root element found");
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.createRoot(root).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<LocoSplash />
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"lib": ["DOM", "ES2020"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"useDefineForClassFields": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "migration"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "migration"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
|
||||||
loco-rs = { version = "0.10.1" }
|
|
||||||
|
|
||||||
[dependencies.sea-orm-migration]
|
|
||||||
version = "1.0.0"
|
|
||||||
features = [
|
|
||||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
|
||||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
|
||||||
# e.g.
|
|
||||||
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
#![allow(elided_lifetimes_in_paths)]
|
|
||||||
#![allow(clippy::wildcard_imports)]
|
|
||||||
pub use sea_orm_migration::prelude::*;
|
|
||||||
|
|
||||||
mod m20220101_000001_users;
|
|
||||||
mod m20231103_114510_notes;
|
|
||||||
|
|
||||||
mod m20241016_180911_servers;
|
|
||||||
pub struct Migrator;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigratorTrait for Migrator {
|
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
|
||||||
vec![
|
|
||||||
Box::new(m20220101_000001_users::Migration),
|
|
||||||
Box::new(m20231103_114510_notes::Migration),
|
|
||||||
Box::new(m20241016_180911_servers::Migration),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
use loco_rs::schema::table_auto_tz;
|
|
||||||
use sea_orm_migration::{prelude::*, schema::*};
|
|
||||||
|
|
||||||
#[derive(DeriveMigrationName)]
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
let table = table_auto_tz(Users::Table)
|
|
||||||
.col(pk_auto(Users::Id))
|
|
||||||
.col(uuid(Users::Pid))
|
|
||||||
.col(string_uniq(Users::Email))
|
|
||||||
.col(string(Users::Password))
|
|
||||||
.col(string(Users::ApiKey).unique_key())
|
|
||||||
.col(string(Users::Name))
|
|
||||||
.col(string_null(Users::ResetToken))
|
|
||||||
.col(timestamp_with_time_zone_null(Users::ResetSentAt))
|
|
||||||
.col(string_null(Users::EmailVerificationToken))
|
|
||||||
.col(timestamp_with_time_zone_null(
|
|
||||||
Users::EmailVerificationSentAt,
|
|
||||||
))
|
|
||||||
.col(timestamp_with_time_zone_null(Users::EmailVerifiedAt))
|
|
||||||
.to_owned();
|
|
||||||
manager.create_table(table).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.drop_table(Table::drop().table(Users::Table).to_owned())
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Iden)]
|
|
||||||
pub enum Users {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Pid,
|
|
||||||
Email,
|
|
||||||
Name,
|
|
||||||
Password,
|
|
||||||
ApiKey,
|
|
||||||
ResetToken,
|
|
||||||
ResetSentAt,
|
|
||||||
EmailVerificationToken,
|
|
||||||
EmailVerificationSentAt,
|
|
||||||
EmailVerifiedAt,
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
use loco_rs::schema::table_auto_tz;
|
|
||||||
use sea_orm_migration::{prelude::*, schema::*};
|
|
||||||
|
|
||||||
#[derive(DeriveMigrationName)]
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
table_auto_tz(Notes::Table)
|
|
||||||
.col(pk_auto(Notes::Id))
|
|
||||||
.col(string_null(Notes::Title))
|
|
||||||
.col(string_null(Notes::Content))
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.drop_table(Table::drop().table(Notes::Table).to_owned())
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
|
||||||
enum Notes {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Title,
|
|
||||||
Content,
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use loco_rs::schema::table_auto_tz;
|
|
||||||
use sea_orm_migration::{prelude::*, schema::*};
|
|
||||||
|
|
||||||
#[derive(DeriveMigrationName)]
|
|
||||||
pub struct Migration;
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MigrationTrait for Migration {
|
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.create_table(
|
|
||||||
table_auto_tz(Servers::Table)
|
|
||||||
.col(pk_auto(Servers::Id))
|
|
||||||
.col(string_null(Servers::Name))
|
|
||||||
.col(string_null(Servers::Domain))
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
|
||||||
manager
|
|
||||||
.drop_table(Table::drop().table(Servers::Table).to_owned())
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
|
||||||
enum Servers {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Name,
|
|
||||||
Domain,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use loco_rs::{
|
|
||||||
app::{AppContext, Hooks, Initializer},
|
|
||||||
bgworker::{BackgroundWorker, Queue},
|
|
||||||
boot::{create_app, BootResult, StartMode},
|
|
||||||
controller::AppRoutes,
|
|
||||||
db::{self, truncate_table},
|
|
||||||
environment::Environment,
|
|
||||||
task::Tasks,
|
|
||||||
Result,
|
|
||||||
};
|
|
||||||
use migration::Migrator;
|
|
||||||
use sea_orm::DatabaseConnection;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
controllers, initializers,
|
|
||||||
models::_entities::{notes, users},
|
|
||||||
tasks,
|
|
||||||
workers::downloader::DownloadWorker,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct App;
|
|
||||||
#[async_trait]
|
|
||||||
impl Hooks for App {
|
|
||||||
fn app_name() -> &'static str {
|
|
||||||
env!("CARGO_CRATE_NAME")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app_version() -> String {
|
|
||||||
format!(
|
|
||||||
"{} ({})",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
option_env!("BUILD_SHA")
|
|
||||||
.or(option_env!("GITHUB_SHA"))
|
|
||||||
.unwrap_or("dev")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn boot(mode: StartMode, environment: &Environment) -> Result<BootResult> {
|
|
||||||
create_app::<Self, Migrator>(mode, environment).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
|
||||||
Ok(vec![Box::new(
|
|
||||||
initializers::view_engine::ViewEngineInitializer,
|
|
||||||
)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn routes(_ctx: &AppContext) -> AppRoutes {
|
|
||||||
AppRoutes::with_default_routes()
|
|
||||||
.add_route(controllers::server::routes())
|
|
||||||
.add_route(controllers::notes::routes())
|
|
||||||
.add_route(controllers::auth::routes())
|
|
||||||
.add_route(controllers::user::routes())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
|
||||||
queue.register(DownloadWorker::build(ctx)).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_tasks(tasks: &mut Tasks) {
|
|
||||||
tasks.register(tasks::seed::SeedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn truncate(db: &DatabaseConnection) -> Result<()> {
|
|
||||||
truncate_table(db, users::Entity).await?;
|
|
||||||
truncate_table(db, notes::Entity).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {
|
|
||||||
db::seed::<users::ActiveModel>(db, &base.join("users.yaml").display().to_string()).await?;
|
|
||||||
db::seed::<notes::ActiveModel>(db, &base.join("notes.yaml").display().to_string()).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
use loco_rs::cli;
|
|
||||||
use nixin_farm::app::App;
|
|
||||||
use migration::Migrator;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> loco_rs::Result<()> {
|
|
||||||
cli::main::<App, Migrator>().await
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
use loco_rs::cli;
|
|
||||||
use nixin_farm::app::App;
|
|
||||||
use migration::Migrator;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> loco_rs::Result<()> {
|
|
||||||
cli::main::<App, Migrator>().await
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
use axum::debug_handler;
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
mailers::auth::AuthMailer,
|
|
||||||
models::{
|
|
||||||
_entities::users,
|
|
||||||
users::{LoginParams, RegisterParams},
|
|
||||||
},
|
|
||||||
views::auth::LoginResponse,
|
|
||||||
};
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct VerifyParams {
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ForgotParams {
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct ResetParams {
|
|
||||||
pub token: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register function creates a new user with the given parameters and sends a
|
|
||||||
/// welcome email to the user
|
|
||||||
#[debug_handler]
|
|
||||||
async fn register(
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<RegisterParams>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let res = users::Model::create_with_password(&ctx.db, ¶ms).await;
|
|
||||||
|
|
||||||
let user = match res {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::info!(
|
|
||||||
message = err.to_string(),
|
|
||||||
user_email = ¶ms.email,
|
|
||||||
"could not register user",
|
|
||||||
);
|
|
||||||
return format::json(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = user
|
|
||||||
.into_active_model()
|
|
||||||
.set_email_verification_sent(&ctx.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
AuthMailer::send_welcome(&ctx, &user).await?;
|
|
||||||
|
|
||||||
format::json(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify register user. if the user not verified his email, he can't login to
|
|
||||||
/// the system.
|
|
||||||
#[debug_handler]
|
|
||||||
async fn verify(
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<VerifyParams>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let user = users::Model::find_by_verification_token(&ctx.db, ¶ms.token).await?;
|
|
||||||
|
|
||||||
if user.email_verified_at.is_some() {
|
|
||||||
tracing::info!(pid = user.pid.to_string(), "user already verified");
|
|
||||||
} else {
|
|
||||||
let active_model = user.into_active_model();
|
|
||||||
let user = active_model.verified(&ctx.db).await?;
|
|
||||||
tracing::info!(pid = user.pid.to_string(), "user verified");
|
|
||||||
}
|
|
||||||
|
|
||||||
format::json(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// In case the user forgot his password this endpoints generate a forgot token
|
|
||||||
/// and send email to the user. In case the email not found in our DB, we are
|
|
||||||
/// returning a valid request for for security reasons (not exposing users DB
|
|
||||||
/// list).
|
|
||||||
#[debug_handler]
|
|
||||||
async fn forgot(
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<ForgotParams>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else {
|
|
||||||
// we don't want to expose our users email. if the email is invalid we still
|
|
||||||
// returning success to the caller
|
|
||||||
return format::json(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = user
|
|
||||||
.into_active_model()
|
|
||||||
.set_forgot_password_sent(&ctx.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
AuthMailer::forgot_password(&ctx, &user).await?;
|
|
||||||
|
|
||||||
format::json(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reset user password by the given parameters
|
|
||||||
#[debug_handler]
|
|
||||||
async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -> Result<Response> {
|
|
||||||
let Ok(user) = users::Model::find_by_reset_token(&ctx.db, ¶ms.token).await else {
|
|
||||||
// we don't want to expose our users email. if the email is invalid we still
|
|
||||||
// returning success to the caller
|
|
||||||
tracing::info!("reset token not found");
|
|
||||||
|
|
||||||
return format::json(());
|
|
||||||
};
|
|
||||||
user.into_active_model()
|
|
||||||
.reset_password(&ctx.db, ¶ms.password)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
format::json(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a user login and returns a token
|
|
||||||
#[debug_handler]
|
|
||||||
async fn login(State(ctx): State<AppContext>, Json(params): Json<LoginParams>) -> Result<Response> {
|
|
||||||
let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?;
|
|
||||||
|
|
||||||
let valid = user.verify_password(¶ms.password);
|
|
||||||
|
|
||||||
if !valid {
|
|
||||||
return unauthorized("unauthorized!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let jwt_secret = ctx.config.get_jwt_config()?;
|
|
||||||
|
|
||||||
let token = user
|
|
||||||
.generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
|
|
||||||
.or_else(|_| unauthorized("unauthorized!"))?;
|
|
||||||
|
|
||||||
format::json(LoginResponse::new(&user, &token))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("api/auth")
|
|
||||||
.add("/register", post(register))
|
|
||||||
.add("/verify", post(verify))
|
|
||||||
.add("/login", post(login))
|
|
||||||
.add("/forgot", post(forgot))
|
|
||||||
.add("/reset", post(reset))
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod auth;
|
|
||||||
pub mod notes;
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
pub mod server;
|
|
|
@ -1,75 +0,0 @@
|
||||||
#![allow(clippy::missing_errors_doc)]
|
|
||||||
#![allow(clippy::unnecessary_struct_initialization)]
|
|
||||||
#![allow(clippy::unused_async)]
|
|
||||||
use axum::debug_handler;
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::_entities::notes::{ActiveModel, Entity, Model};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Params {
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Params {
|
|
||||||
fn update(&self, item: &mut ActiveModel) {
|
|
||||||
item.title = Set(self.title.clone());
|
|
||||||
item.content = Set(self.content.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
|
||||||
let item = Entity::find_by_id(id).one(&ctx.db).await?;
|
|
||||||
item.ok_or_else(|| Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn list(State(ctx): State<AppContext>) -> Result<Response> {
|
|
||||||
format::json(Entity::find().all(&ctx.db).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn add(State(ctx): State<AppContext>, Json(params): Json<Params>) -> Result<Response> {
|
|
||||||
let mut item = ActiveModel {
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
params.update(&mut item);
|
|
||||||
let item = item.insert(&ctx.db).await?;
|
|
||||||
format::json(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn update(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<Params>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let item = load_item(&ctx, id).await?;
|
|
||||||
let mut item = item.into_active_model();
|
|
||||||
params.update(&mut item);
|
|
||||||
let item = item.update(&ctx.db).await?;
|
|
||||||
format::json(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
|
||||||
load_item(&ctx, id).await?.delete(&ctx.db).await?;
|
|
||||||
format::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn get_one(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
|
||||||
format::json(load_item(&ctx, id).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("api/notes")
|
|
||||||
.add("/", get(list))
|
|
||||||
.add("/", post(add))
|
|
||||||
.add("/:id", get(get_one))
|
|
||||||
.add("/:id", delete(remove))
|
|
||||||
.add("/:id", post(update))
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
#![allow(clippy::missing_errors_doc)]
|
|
||||||
#![allow(clippy::unnecessary_struct_initialization)]
|
|
||||||
#![allow(clippy::unused_async)]
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sea_orm::{sea_query::Order, QueryOrder};
|
|
||||||
use axum::debug_handler;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
models::_entities::servers::{ActiveModel, Column, Entity, Model},
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Params {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub domain: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Params {
|
|
||||||
fn update(&self, item: &mut ActiveModel) {
|
|
||||||
item.name = Set(self.name.clone());
|
|
||||||
item.domain = Set(self.domain.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
|
|
||||||
let item = Entity::find_by_id(id).one(&ctx.db).await?;
|
|
||||||
item.ok_or_else(|| Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn list(
|
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let item = Entity::find()
|
|
||||||
.order_by(Column::Id, Order::Desc)
|
|
||||||
.all(&ctx.db)
|
|
||||||
.await?;
|
|
||||||
views::server::list(&v, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn new(
|
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
|
||||||
State(_ctx): State<AppContext>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
views::server::create(&v)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn update(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<Params>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let item = load_item(&ctx, id).await?;
|
|
||||||
let mut item = item.into_active_model();
|
|
||||||
params.update(&mut item);
|
|
||||||
let item = item.update(&ctx.db).await?;
|
|
||||||
format::json(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn edit(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let item = load_item(&ctx, id).await?;
|
|
||||||
views::server::edit(&v, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn show(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let item = load_item(&ctx, id).await?;
|
|
||||||
views::server::show(&v, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn add(
|
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
Json(params): Json<Params>,
|
|
||||||
) -> Result<Response> {
|
|
||||||
let mut item = ActiveModel {
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
params.update(&mut item);
|
|
||||||
let item = item.insert(&ctx.db).await?;
|
|
||||||
views::server::show(&v, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn remove(Path(id): Path<i32>, State(ctx): State<AppContext>) -> Result<Response> {
|
|
||||||
load_item(&ctx, id).await?.delete(&ctx.db).await?;
|
|
||||||
format::empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("servers/")
|
|
||||||
.add("/", get(list))
|
|
||||||
.add("/", post(add))
|
|
||||||
.add("new", get(new))
|
|
||||||
.add(":id", get(show))
|
|
||||||
.add(":id/edit", get(edit))
|
|
||||||
.add(":id", post(update))
|
|
||||||
.add(":id", delete(remove))
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
use axum::debug_handler;
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
|
|
||||||
use crate::{models::_entities::users, views::user::CurrentResponse};
|
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
|
|
||||||
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
|
||||||
format::json(CurrentResponse::new(&user))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("api/user")
|
|
||||||
.add("/current", get(current))
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
---
|
|
||||||
- id: 1
|
|
||||||
title: Loco note 1
|
|
||||||
content: Loco note 1 content
|
|
||||||
created_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
updated_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
- id: 2
|
|
||||||
title: Loco note 2
|
|
||||||
content: Loco note 2 content
|
|
||||||
created_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
updated_at: "2023-11-12T12:34:56.789Z"
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
- id: 1
|
|
||||||
pid: 11111111-1111-1111-1111-111111111111
|
|
||||||
email: user1@example.com
|
|
||||||
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
|
|
||||||
api_key: lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758
|
|
||||||
name: user1
|
|
||||||
created_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
updated_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
- id: 2
|
|
||||||
pid: 22222222-2222-2222-2222-222222222222
|
|
||||||
email: user2@example.com
|
|
||||||
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc"
|
|
||||||
api_key: lo-153561ca-fa84-4e1b-813a-c62526d0a77e
|
|
||||||
name: user2
|
|
||||||
created_at: "2023-11-12T12:34:56.789Z"
|
|
||||||
updated_at: "2023-11-12T12:34:56.789Z"
|
|
|
@ -1,2 +0,0 @@
|
||||||
#![allow(clippy::module_name_repetitions)]
|
|
||||||
pub mod view_engine;
|
|
|
@ -1,36 +0,0 @@
|
||||||
use axum::{async_trait, Extension, Router as AxumRouter};
|
|
||||||
use fluent_templates::{ArcLoader, FluentLoader};
|
|
||||||
use loco_rs::{
|
|
||||||
app::{AppContext, Initializer},
|
|
||||||
controller::views::{engines, ViewEngine},
|
|
||||||
Error, Result,
|
|
||||||
};
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
const I18N_DIR: &str = "assets/i18n";
|
|
||||||
const I18N_SHARED: &str = "assets/i18n/shared.ftl";
|
|
||||||
|
|
||||||
pub struct ViewEngineInitializer;
|
|
||||||
#[async_trait]
|
|
||||||
impl Initializer for ViewEngineInitializer {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"view-engine".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
|
|
||||||
let mut tera_engine = engines::TeraView::build()?;
|
|
||||||
if std::path::Path::new(I18N_DIR).exists() {
|
|
||||||
let arc = ArcLoader::builder(&I18N_DIR, unic_langid::langid!("en-US"))
|
|
||||||
.shared_resources(Some(&[I18N_SHARED.into()]))
|
|
||||||
.customize(|bundle| bundle.set_use_isolating(false))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| Error::string(&e.to_string()))?;
|
|
||||||
tera_engine
|
|
||||||
.tera
|
|
||||||
.register_function("t", FluentLoader::new(arc));
|
|
||||||
info!("locales loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
pub mod app;
|
|
||||||
pub mod controllers;
|
|
||||||
pub mod initializers;
|
|
||||||
pub mod mailers;
|
|
||||||
pub mod models;
|
|
||||||
pub mod tasks;
|
|
||||||
pub mod views;
|
|
||||||
pub mod workers;
|
|
|
@ -1,65 +0,0 @@
|
||||||
// auth mailer
|
|
||||||
#![allow(non_upper_case_globals)]
|
|
||||||
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::models::users;
|
|
||||||
|
|
||||||
static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
|
|
||||||
static forgot: Dir<'_> = include_dir!("src/mailers/auth/forgot");
|
|
||||||
// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets
|
|
||||||
// move on for now.
|
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
pub struct AuthMailer {}
|
|
||||||
impl Mailer for AuthMailer {}
|
|
||||||
impl AuthMailer {
|
|
||||||
/// Sending welcome email the the given user
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When email sending is failed
|
|
||||||
pub async fn send_welcome(ctx: &AppContext, user: &users::Model) -> Result<()> {
|
|
||||||
Self::mail_template(
|
|
||||||
ctx,
|
|
||||||
&welcome,
|
|
||||||
mailer::Args {
|
|
||||||
to: user.email.to_string(),
|
|
||||||
locals: json!({
|
|
||||||
"name": user.name,
|
|
||||||
"verifyToken": user.email_verification_token,
|
|
||||||
"domain": ctx.config.server.full_url()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sending forgot password email
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When email sending is failed
|
|
||||||
pub async fn forgot_password(ctx: &AppContext, user: &users::Model) -> Result<()> {
|
|
||||||
Self::mail_template(
|
|
||||||
ctx,
|
|
||||||
&forgot,
|
|
||||||
mailer::Args {
|
|
||||||
to: user.email.to_string(),
|
|
||||||
locals: json!({
|
|
||||||
"name": user.name,
|
|
||||||
"resetToken": user.reset_token,
|
|
||||||
"domain": ctx.config.server.full_url()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
;<html>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
Hey {{name}},
|
|
||||||
Forgot your password? No worries! You can reset it by clicking the link below:
|
|
||||||
<a href="http://{{domain}}/reset#{{resetToken}}">Reset Your Password</a>
|
|
||||||
If you didn't request a password reset, please ignore this email.
|
|
||||||
Best regards,<br>The Loco Team</br>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1 +0,0 @@
|
||||||
Your reset password link
|
|
|
@ -1,3 +0,0 @@
|
||||||
Reset your password with this link:
|
|
||||||
|
|
||||||
http://localhost/reset#{{resetToken}}
|
|
|
@ -1,13 +0,0 @@
|
||||||
;<html>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
Dear {{name}},
|
|
||||||
Welcome to Loco! You can now log in to your account.
|
|
||||||
Before you get started, please verify your account by clicking the link below:
|
|
||||||
<a href="http://{{domain}}/verify#{{verifyToken}}">
|
|
||||||
Verify Your Account
|
|
||||||
</a>
|
|
||||||
<p>Best regards,<br>The Loco Team</p>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1 +0,0 @@
|
||||||
Welcome {{name}}
|
|
|
@ -1,4 +0,0 @@
|
||||||
Welcome {{name}}, you can now log in.
|
|
||||||
Verify your account with the link below:
|
|
||||||
|
|
||||||
http://localhost/verify#{{verifyToken}}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod auth;
|
|
|
@ -1,7 +0,0 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
|
||||||
|
|
||||||
pub mod prelude;
|
|
||||||
|
|
||||||
pub mod notes;
|
|
||||||
pub mod servers;
|
|
||||||
pub mod users;
|
|
|
@ -1,18 +0,0 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
|
||||||
#[sea_orm(table_name = "notes")]
|
|
||||||
pub struct Model {
|
|
||||||
pub created_at: DateTimeWithTimeZone,
|
|
||||||
pub updated_at: DateTimeWithTimeZone,
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
|
||||||
|
|
||||||
pub use super::notes::Entity as Notes;
|
|
||||||
pub use super::servers::Entity as Servers;
|
|
||||||
pub use super::users::Entity as Users;
|
|
|
@ -1,18 +0,0 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
|
||||||
#[sea_orm(table_name = "servers")]
|
|
||||||
pub struct Model {
|
|
||||||
pub created_at: DateTimeWithTimeZone,
|
|
||||||
pub updated_at: DateTimeWithTimeZone,
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub domain: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {}
|
|
|
@ -1,28 +0,0 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
|
||||||
#[sea_orm(table_name = "users")]
|
|
||||||
pub struct Model {
|
|
||||||
pub created_at: DateTimeWithTimeZone,
|
|
||||||
pub updated_at: DateTimeWithTimeZone,
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: i32,
|
|
||||||
pub pid: Uuid,
|
|
||||||
#[sea_orm(unique)]
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
#[sea_orm(unique)]
|
|
||||||
pub api_key: String,
|
|
||||||
pub name: String,
|
|
||||||
pub reset_token: Option<String>,
|
|
||||||
pub reset_sent_at: Option<DateTimeWithTimeZone>,
|
|
||||||
pub email_verification_token: Option<String>,
|
|
||||||
pub email_verification_sent_at: Option<DateTimeWithTimeZone>,
|
|
||||||
pub email_verified_at: Option<DateTimeWithTimeZone>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {}
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod _entities;
|
|
||||||
pub mod notes;
|
|
||||||
pub mod users;
|
|
||||||
pub mod servers;
|
|
|
@ -1,7 +0,0 @@
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
use super::_entities::notes::ActiveModel;
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {
|
|
||||||
// extend activemodel below (keep comment for generators)
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use super::_entities::servers::{ActiveModel, Entity};
|
|
||||||
pub type Servers = Entity;
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {
|
|
||||||
// extend activemodel below (keep comment for generators)
|
|
||||||
}
|
|
|
@ -1,298 +0,0 @@
|
||||||
use async_trait::async_trait;
|
|
||||||
use chrono::offset::Local;
|
|
||||||
use loco_rs::{auth::jwt, hash, prelude::*};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub use super::_entities::users::{self, ActiveModel, Entity, Model};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct LoginParams {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct RegisterParams {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Validate, Deserialize)]
|
|
||||||
pub struct Validator {
|
|
||||||
#[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
|
|
||||||
pub name: String,
|
|
||||||
#[validate(custom(function = "validation::is_valid_email"))]
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validatable for super::_entities::users::ActiveModel {
|
|
||||||
fn validator(&self) -> Box<dyn Validate> {
|
|
||||||
Box::new(Validator {
|
|
||||||
name: self.name.as_ref().to_owned(),
|
|
||||||
email: self.email.as_ref().to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ActiveModelBehavior for super::_entities::users::ActiveModel {
|
|
||||||
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
|
|
||||||
where
|
|
||||||
C: ConnectionTrait,
|
|
||||||
{
|
|
||||||
self.validate()?;
|
|
||||||
if insert {
|
|
||||||
let mut this = self;
|
|
||||||
this.pid = ActiveValue::Set(Uuid::new_v4());
|
|
||||||
this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4()));
|
|
||||||
Ok(this)
|
|
||||||
} else {
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Authenticable for super::_entities::users::Model {
|
|
||||||
async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::ApiKey, api_key)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
|
|
||||||
Self::find_by_pid(db, claims_key).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::_entities::users::Model {
|
|
||||||
/// finds a user by the provided email
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not find user by the given token or DB query error
|
|
||||||
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::Email, email)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// finds a user by the provided verification token
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not find user by the given token or DB query error
|
|
||||||
pub async fn find_by_verification_token(
|
|
||||||
db: &DatabaseConnection,
|
|
||||||
token: &str,
|
|
||||||
) -> ModelResult<Self> {
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::EmailVerificationToken, token)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// finds a user by the provided reset token
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not find user by the given token or DB query error
|
|
||||||
pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::ResetToken, token)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// finds a user by the provided pid
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not find user or DB query error
|
|
||||||
pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
|
|
||||||
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::Pid, parse_uuid)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// finds a user by the provided api key
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not find user by the given token or DB query error
|
|
||||||
pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
|
|
||||||
let user = users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::ApiKey, api_key)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies whether the provided plain password matches the hashed password
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when could not verify password
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify_password(&self, password: &str) -> bool {
|
|
||||||
hash::verify_password(password, &self.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asynchronously creates a user with a password and saves it to the
|
|
||||||
/// database.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When could not save the user into the DB
|
|
||||||
pub async fn create_with_password(
|
|
||||||
db: &DatabaseConnection,
|
|
||||||
params: &RegisterParams,
|
|
||||||
) -> ModelResult<Self> {
|
|
||||||
let txn = db.begin().await?;
|
|
||||||
|
|
||||||
if users::Entity::find()
|
|
||||||
.filter(
|
|
||||||
model::query::condition()
|
|
||||||
.eq(users::Column::Email, ¶ms.email)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.one(&txn)
|
|
||||||
.await?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Err(ModelError::EntityAlreadyExists {});
|
|
||||||
}
|
|
||||||
|
|
||||||
let password_hash =
|
|
||||||
hash::hash_password(¶ms.password).map_err(|e| ModelError::Any(e.into()))?;
|
|
||||||
let user = users::ActiveModel {
|
|
||||||
email: ActiveValue::set(params.email.to_string()),
|
|
||||||
password: ActiveValue::set(password_hash),
|
|
||||||
name: ActiveValue::set(params.name.to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.insert(&txn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
txn.commit().await?;
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a JWT
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when could not convert user claims to jwt token
|
|
||||||
pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult<String> {
|
|
||||||
Ok(jwt::JWT::new(secret).generate_token(expiration, self.pid.to_string(), None)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::_entities::users::ActiveModel {
|
|
||||||
/// Sets the email verification information for the user and
|
|
||||||
/// updates it in the database.
|
|
||||||
///
|
|
||||||
/// This method is used to record the timestamp when the email verification
|
|
||||||
/// was sent and generate a unique verification token for the user.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when has DB query error
|
|
||||||
pub async fn set_email_verification_sent(
|
|
||||||
mut self,
|
|
||||||
db: &DatabaseConnection,
|
|
||||||
) -> ModelResult<Model> {
|
|
||||||
self.email_verification_sent_at = ActiveValue::set(Some(Local::now().into()));
|
|
||||||
self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
|
|
||||||
Ok(self.update(db).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the information for a reset password request,
|
|
||||||
/// generates a unique reset password token, and updates it in the
|
|
||||||
/// database.
|
|
||||||
///
|
|
||||||
/// This method records the timestamp when the reset password token is sent
|
|
||||||
/// and generates a unique token for the user.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when has DB query error
|
|
||||||
pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
|
||||||
self.reset_sent_at = ActiveValue::set(Some(Local::now().into()));
|
|
||||||
self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
|
|
||||||
Ok(self.update(db).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Records the verification time when a user verifies their
|
|
||||||
/// email and updates it in the database.
|
|
||||||
///
|
|
||||||
/// This method sets the timestamp when the user successfully verifies their
|
|
||||||
/// email.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when has DB query error
|
|
||||||
pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
|
|
||||||
self.email_verified_at = ActiveValue::set(Some(Local::now().into()));
|
|
||||||
Ok(self.update(db).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the current user password with a new password and
|
|
||||||
/// updates it in the database.
|
|
||||||
///
|
|
||||||
/// This method hashes the provided password and sets it as the new password
|
|
||||||
/// for the user.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// when has DB query error or could not hashed the given password
|
|
||||||
pub async fn reset_password(
|
|
||||||
mut self,
|
|
||||||
db: &DatabaseConnection,
|
|
||||||
password: &str,
|
|
||||||
) -> ModelResult<Model> {
|
|
||||||
self.password =
|
|
||||||
ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?);
|
|
||||||
self.reset_token = ActiveValue::Set(None);
|
|
||||||
self.reset_sent_at = ActiveValue::Set(None);
|
|
||||||
Ok(self.update(db).await?)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod seed;
|
|
|
@ -1,45 +0,0 @@
|
||||||
//! This task implements data seeding functionality for initializing new
|
|
||||||
//! development/demo environments.
|
|
||||||
//!
|
|
||||||
//! # Example
|
|
||||||
//!
|
|
||||||
//! Run the task with the following command:
|
|
||||||
//! ```sh
|
|
||||||
//! cargo run task
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! To override existing data and reset the data structure, use the following
|
|
||||||
//! command with the `refresh:true` argument:
|
|
||||||
//! ```sh
|
|
||||||
//! cargo run task seed_data refresh:true
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use loco_rs::{db, prelude::*};
|
|
||||||
use migration::Migrator;
|
|
||||||
|
|
||||||
use crate::app::App;
|
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
pub struct SeedData;
|
|
||||||
#[async_trait]
|
|
||||||
impl Task for SeedData {
|
|
||||||
fn task(&self) -> TaskInfo {
|
|
||||||
TaskInfo {
|
|
||||||
name: "seed_data".to_string(),
|
|
||||||
detail: "Task for seeding data".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
|
|
||||||
let refresh = vars
|
|
||||||
.cli_arg("refresh")
|
|
||||||
.is_ok_and(|refresh| refresh == "true");
|
|
||||||
|
|
||||||
if refresh {
|
|
||||||
db::reset::<Migrator>(&app_context.db).await?;
|
|
||||||
}
|
|
||||||
let path = std::path::Path::new("src/fixtures");
|
|
||||||
db::run_app_seed::<App>(&app_context.db, path).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::_entities::users;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub pid: String,
|
|
||||||
pub name: String,
|
|
||||||
pub is_verified: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginResponse {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(user: &users::Model, token: &String) -> Self {
|
|
||||||
Self {
|
|
||||||
token: token.to_string(),
|
|
||||||
pid: user.pid.to_string(),
|
|
||||||
name: user.name.clone(),
|
|
||||||
is_verified: user.email_verified_at.is_some(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod auth;
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
pub mod server;
|
|
|
@ -1,39 +0,0 @@
|
||||||
use loco_rs::prelude::*;
|
|
||||||
|
|
||||||
use crate::models::_entities::servers;
|
|
||||||
|
|
||||||
/// Render a list view of servers.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When there is an issue with rendering the view.
|
|
||||||
pub fn list(v: &impl ViewRenderer, items: &Vec<servers::Model>) -> Result<Response> {
|
|
||||||
format::render().view(v, "server/list.html", data!({"items": items}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a single server view.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When there is an issue with rendering the view.
|
|
||||||
pub fn show(v: &impl ViewRenderer, item: &servers::Model) -> Result<Response> {
|
|
||||||
format::render().view(v, "server/show.html", data!({"item": item}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a server create form.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When there is an issue with rendering the view.
|
|
||||||
pub fn create(v: &impl ViewRenderer) -> Result<Response> {
|
|
||||||
format::render().view(v, "server/create.html", data!({}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a server edit form.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// When there is an issue with rendering the view.
|
|
||||||
pub fn edit(v: &impl ViewRenderer, item: &servers::Model) -> Result<Response> {
|
|
||||||
format::render().view(v, "server/edit.html", data!({"item": item}))
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::models::_entities::users;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct CurrentResponse {
|
|
||||||
pub pid: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CurrentResponse {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(user: &users::Model) -> Self {
|
|
||||||
Self {
|
|
||||||
pid: user.pid.to_string(),
|
|
||||||
name: user.name.clone(),
|
|
||||||
email: user.email.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use loco_rs::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use crate::models::users;
|
|
||||||
|
|
||||||
pub struct DownloadWorker {
|
|
||||||
pub ctx: AppContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Serialize)]
|
|
||||||
pub struct DownloadWorkerArgs {
|
|
||||||
pub user_guid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {
|
|
||||||
fn build(ctx: &AppContext) -> Self {
|
|
||||||
Self { ctx: ctx.clone() }
|
|
||||||
}
|
|
||||||
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {
|
|
||||||
// TODO: Some actual work goes here...
|
|
||||||
println!("================================================");
|
|
||||||
println!("Sending payment report to user {}", args.user_guid);
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(2000)).await;
|
|
||||||
|
|
||||||
let all = users::Entity::find()
|
|
||||||
.all(&self.ctx.db)
|
|
||||||
.await
|
|
||||||
.map_err(Box::from)?;
|
|
||||||
for user in &all {
|
|
||||||
println!("user: {}", user.id);
|
|
||||||
}
|
|
||||||
println!("================================================");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod downloader;
|
|
|
@ -1,4 +0,0 @@
|
||||||
mod models;
|
|
||||||
mod requests;
|
|
||||||
mod tasks;
|
|
||||||
mod workers;
|
|
|
@ -1,3 +0,0 @@
|
||||||
mod users;
|
|
||||||
|
|
||||||
mod servers;
|
|
|
@ -1,31 +0,0 @@
|
||||||
use nixin_farm::app::App;
|
|
||||||
use loco_rs::testing;
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
macro_rules! configure_insta {
|
|
||||||
($($expr:expr),*) => {
|
|
||||||
let mut settings = insta::Settings::clone_current();
|
|
||||||
settings.set_prepend_module_to_snapshot(false);
|
|
||||||
let _guard = settings.bind_to_scope();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_model() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
// query your model, e.g.:
|
|
||||||
//
|
|
||||||
// let item = models::posts::Model::find_by_pid(
|
|
||||||
// &boot.app_context.db,
|
|
||||||
// "11111111-1111-1111-1111-111111111111",
|
|
||||||
// )
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// snapshot the result:
|
|
||||||
// assert_debug_snapshot!(item);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: res
|
|
||||||
---
|
|
||||||
Ok(
|
|
||||||
Model {
|
|
||||||
created_at: DATE,
|
|
||||||
updated_at: DATE,
|
|
||||||
id: ID
|
|
||||||
pid: PID,
|
|
||||||
email: "test@framework.com",
|
|
||||||
password: "PASSWORD",
|
|
||||||
api_key: "lo-PID",
|
|
||||||
name: "framework",
|
|
||||||
reset_token: None,
|
|
||||||
reset_sent_at: None,
|
|
||||||
email_verification_token: None,
|
|
||||||
email_verification_sent_at: None,
|
|
||||||
email_verified_at: None,
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/main.rs
|
|
||||||
expression: non_existing_user_results
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
EntityNotFound,
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: non_existing_user_results
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
EntityNotFound,
|
|
||||||
)
|
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: existing_user
|
|
||||||
---
|
|
||||||
Ok(
|
|
||||||
Model {
|
|
||||||
created_at: 2023-11-12T12:34:56.789+00:00,
|
|
||||||
updated_at: 2023-11-12T12:34:56.789+00:00,
|
|
||||||
id: 1,
|
|
||||||
pid: 11111111-1111-1111-1111-111111111111,
|
|
||||||
email: "user1@example.com",
|
|
||||||
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc",
|
|
||||||
api_key: "lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758",
|
|
||||||
name: "user1",
|
|
||||||
reset_token: None,
|
|
||||||
reset_sent_at: None,
|
|
||||||
email_verification_token: None,
|
|
||||||
email_verification_sent_at: None,
|
|
||||||
email_verified_at: None,
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/main.rs
|
|
||||||
expression: non_existing_user_results
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
EntityNotFound,
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: non_existing_user_results
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
EntityNotFound,
|
|
||||||
)
|
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: existing_user
|
|
||||||
---
|
|
||||||
Ok(
|
|
||||||
Model {
|
|
||||||
created_at: 2023-11-12T12:34:56.789+00:00,
|
|
||||||
updated_at: 2023-11-12T12:34:56.789+00:00,
|
|
||||||
id: 1,
|
|
||||||
pid: 11111111-1111-1111-1111-111111111111,
|
|
||||||
email: "user1@example.com",
|
|
||||||
password: "$argon2id$v=19$m=19456,t=2,p=1$ETQBx4rTgNAZhSaeYZKOZg$eYTdH26CRT6nUJtacLDEboP0li6xUwUF/q5nSlQ8uuc",
|
|
||||||
api_key: "lo-95ec80d7-cb60-4b70-9b4b-9ef74cb88758",
|
|
||||||
name: "user1",
|
|
||||||
reset_token: None,
|
|
||||||
reset_sent_at: None,
|
|
||||||
email_verification_token: None,
|
|
||||||
email_verification_sent_at: None,
|
|
||||||
email_verified_at: None,
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/main.rs
|
|
||||||
expression: res
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
Custom(
|
|
||||||
"{\"email\":[{\"code\":\"invalid email\",\"message\":null}],\"name\":[{\"code\":\"length\",\"message\":\"Name must be at least 2 characters long.\"}]}",
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/models/users.rs
|
|
||||||
expression: new_user
|
|
||||||
---
|
|
||||||
Err(
|
|
||||||
EntityAlreadyExists,
|
|
||||||
)
|
|
|
@ -1,223 +0,0 @@
|
||||||
use insta::assert_debug_snapshot;
|
|
||||||
use loco_rs::{model::ModelError, testing};
|
|
||||||
use nixin_farm::{
|
|
||||||
app::App,
|
|
||||||
models::users::{self, Model, RegisterParams},
|
|
||||||
};
|
|
||||||
use sea_orm::{ActiveModelTrait, ActiveValue, IntoActiveModel};
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
macro_rules! configure_insta {
|
|
||||||
($($expr:expr),*) => {
|
|
||||||
let mut settings = insta::Settings::clone_current();
|
|
||||||
settings.set_prepend_module_to_snapshot(false);
|
|
||||||
settings.set_snapshot_suffix("users");
|
|
||||||
let _guard = settings.bind_to_scope();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_can_validate_model() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
|
|
||||||
let res = users::ActiveModel {
|
|
||||||
name: ActiveValue::set("1".to_string()),
|
|
||||||
email: ActiveValue::set("invalid-email".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.insert(&boot.app_context.db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_debug_snapshot!(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_create_with_password() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
|
|
||||||
let params = RegisterParams {
|
|
||||||
email: "test@framework.com".to_string(),
|
|
||||||
password: "1234".to_string(),
|
|
||||||
name: "framework".to_string(),
|
|
||||||
};
|
|
||||||
let res = Model::create_with_password(&boot.app_context.db, ¶ms).await;
|
|
||||||
|
|
||||||
insta::with_settings!({
|
|
||||||
filters => testing::cleanup_user_model()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn handle_create_with_password_with_duplicate() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let new_user: Result<Model, ModelError> = Model::create_with_password(
|
|
||||||
&boot.app_context.db,
|
|
||||||
&RegisterParams {
|
|
||||||
email: "user1@example.com".to_string(),
|
|
||||||
password: "1234".to_string(),
|
|
||||||
name: "framework".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_debug_snapshot!(new_user);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_find_by_email() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let existing_user = Model::find_by_email(&boot.app_context.db, "user1@example.com").await;
|
|
||||||
let non_existing_user_results =
|
|
||||||
Model::find_by_email(&boot.app_context.db, "un@existing-email.com").await;
|
|
||||||
|
|
||||||
assert_debug_snapshot!(existing_user);
|
|
||||||
assert_debug_snapshot!(non_existing_user_results);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_find_by_pid() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let existing_user =
|
|
||||||
Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await;
|
|
||||||
let non_existing_user_results =
|
|
||||||
Model::find_by_pid(&boot.app_context.db, "23232323-2323-2323-2323-232323232323").await;
|
|
||||||
|
|
||||||
assert_debug_snapshot!(existing_user);
|
|
||||||
assert_debug_snapshot!(non_existing_user_results);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_verification_token() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.email_verification_sent_at.is_none());
|
|
||||||
assert!(user.email_verification_token.is_none());
|
|
||||||
|
|
||||||
assert!(user
|
|
||||||
.into_active_model()
|
|
||||||
.set_email_verification_sent(&boot.app_context.db)
|
|
||||||
.await
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.email_verification_sent_at.is_some());
|
|
||||||
assert!(user.email_verification_token.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_set_forgot_password_sent() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.reset_sent_at.is_none());
|
|
||||||
assert!(user.reset_token.is_none());
|
|
||||||
|
|
||||||
assert!(user
|
|
||||||
.into_active_model()
|
|
||||||
.set_forgot_password_sent(&boot.app_context.db)
|
|
||||||
.await
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.reset_sent_at.is_some());
|
|
||||||
assert!(user.reset_token.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_verified() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.email_verified_at.is_none());
|
|
||||||
|
|
||||||
assert!(user
|
|
||||||
.into_active_model()
|
|
||||||
.verified(&boot.app_context.db)
|
|
||||||
.await
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.email_verified_at.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_reset_password() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
let boot = testing::boot_test::<App>().await.unwrap();
|
|
||||||
testing::seed::<App>(&boot.app_context.db).await.unwrap();
|
|
||||||
|
|
||||||
let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.verify_password("12341234"));
|
|
||||||
|
|
||||||
assert!(user
|
|
||||||
.clone()
|
|
||||||
.into_active_model()
|
|
||||||
.reset_password(&boot.app_context.db, "new-password")
|
|
||||||
.await
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.verify_password("new-password")
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
use insta::{assert_debug_snapshot, with_settings};
|
|
||||||
use loco_rs::testing;
|
|
||||||
use nixin_farm::{app::App, models::users};
|
|
||||||
use rstest::rstest;
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
use super::prepare_data;
|
|
||||||
|
|
||||||
// TODO: see how to dedup / extract this to app-local test utils
|
|
||||||
// not to framework, because that would require a runtime dep on insta
|
|
||||||
macro_rules! configure_insta {
|
|
||||||
($($expr:expr),*) => {
|
|
||||||
let mut settings = insta::Settings::clone_current();
|
|
||||||
settings.set_prepend_module_to_snapshot(false);
|
|
||||||
settings.set_snapshot_suffix("auth_request");
|
|
||||||
let _guard = settings.bind_to_scope();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_register() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
let email = "test@loco.com";
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"name": "loco",
|
|
||||||
"email": email,
|
|
||||||
"password": "12341234"
|
|
||||||
});
|
|
||||||
|
|
||||||
let _response = request.post("/api/auth/register").json(&payload).await;
|
|
||||||
let saved_user = users::Model::find_by_email(&ctx.db, email).await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => testing::cleanup_user_model()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(saved_user);
|
|
||||||
});
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => testing::cleanup_email()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case("login_with_valid_password", "12341234")]
|
|
||||||
#[case("login_with_invalid_password", "invalid-password")]
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
let email = "test@loco.com";
|
|
||||||
let register_payload = serde_json::json!({
|
|
||||||
"name": "loco",
|
|
||||||
"email": email,
|
|
||||||
"password": "12341234"
|
|
||||||
});
|
|
||||||
|
|
||||||
//Creating a new user
|
|
||||||
_ = request
|
|
||||||
.post("/api/auth/register")
|
|
||||||
.json(®ister_payload)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let user = users::Model::find_by_email(&ctx.db, email).await.unwrap();
|
|
||||||
let verify_payload = serde_json::json!({
|
|
||||||
"token": user.email_verification_token,
|
|
||||||
});
|
|
||||||
request.post("/api/auth/verify").json(&verify_payload).await;
|
|
||||||
|
|
||||||
//verify user request
|
|
||||||
let response = request
|
|
||||||
.post("/api/auth/login")
|
|
||||||
.json(&serde_json::json!({
|
|
||||||
"email": email,
|
|
||||||
"password": password
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Make sure email_verified_at is set
|
|
||||||
assert!(users::Model::find_by_email(&ctx.db, email)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.email_verified_at
|
|
||||||
.is_some());
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => testing::cleanup_user_model()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(test_name, (response.status_code(), response.text()));
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_login_without_verify() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, _ctx| async move {
|
|
||||||
let email = "test@loco.com";
|
|
||||||
let password = "12341234";
|
|
||||||
let register_payload = serde_json::json!({
|
|
||||||
"name": "loco",
|
|
||||||
"email": email,
|
|
||||||
"password": password
|
|
||||||
});
|
|
||||||
|
|
||||||
//Creating a new user
|
|
||||||
_ = request
|
|
||||||
.post("/api/auth/register")
|
|
||||||
.json(®ister_payload)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
//verify user request
|
|
||||||
let response = request
|
|
||||||
.post("/api/auth/login")
|
|
||||||
.json(&serde_json::json!({
|
|
||||||
"email": email,
|
|
||||||
"password": password
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => testing::cleanup_user_model()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!((response.status_code(), response.text()));
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_reset_password() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
let login_data = prepare_data::init_user_login(&request, &ctx).await;
|
|
||||||
|
|
||||||
let forgot_payload = serde_json::json!({
|
|
||||||
"email": login_data.user.email,
|
|
||||||
});
|
|
||||||
_ = request.post("/api/auth/forgot").json(&forgot_payload).await;
|
|
||||||
|
|
||||||
let user = users::Model::find_by_email(&ctx.db, &login_data.user.email)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(user.reset_token.is_some());
|
|
||||||
assert!(user.reset_sent_at.is_some());
|
|
||||||
|
|
||||||
let new_password = "new-password";
|
|
||||||
let reset_payload = serde_json::json!({
|
|
||||||
"token": user.reset_token,
|
|
||||||
"password": new_password,
|
|
||||||
});
|
|
||||||
|
|
||||||
let reset_response = request.post("/api/auth/reset").json(&reset_payload).await;
|
|
||||||
|
|
||||||
let user = users::Model::find_by_email(&ctx.db, &user.email)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user.reset_token.is_none());
|
|
||||||
assert!(user.reset_sent_at.is_none());
|
|
||||||
|
|
||||||
assert_debug_snapshot!((reset_response.status_code(), reset_response.text()));
|
|
||||||
|
|
||||||
let response = request
|
|
||||||
.post("/api/auth/login")
|
|
||||||
.json(&serde_json::json!({
|
|
||||||
"email": user.email,
|
|
||||||
"password": new_password
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(response.status_code(), 200);
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => testing::cleanup_email()
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
mod auth;
|
|
||||||
mod notes;
|
|
||||||
mod prepare_data;
|
|
||||||
mod user;
|
|
|
@ -1,123 +0,0 @@
|
||||||
use insta::{assert_debug_snapshot, with_settings};
|
|
||||||
use loco_rs::testing;
|
|
||||||
use nixin_farm::{app::App, models::_entities::notes::Entity};
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
// TODO: see how to dedup / extract this to app-local test utils
|
|
||||||
// not to framework, because that would require a runtime dep on insta
|
|
||||||
macro_rules! configure_insta {
|
|
||||||
($($expr:expr),*) => {
|
|
||||||
let mut settings = insta::Settings::clone_current();
|
|
||||||
settings.set_prepend_module_to_snapshot(false);
|
|
||||||
settings.set_snapshot_suffix("notes_request");
|
|
||||||
let _guard = settings.bind_to_scope();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_get_notes() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
testing::seed::<App>(&ctx.db).await.unwrap();
|
|
||||||
|
|
||||||
let notes = request.get("/api/notes").await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => {
|
|
||||||
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
|
|
||||||
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
|
|
||||||
combined_filters
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(
|
|
||||||
(notes.status_code(), notes.text())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_add_note() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, _ctx| async move {
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"title": "loco",
|
|
||||||
"content": "loco note test",
|
|
||||||
});
|
|
||||||
|
|
||||||
let add_note_request = request.post("/api/notes").json(&payload).await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => {
|
|
||||||
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
|
|
||||||
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
|
|
||||||
combined_filters
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(
|
|
||||||
(add_note_request.status_code(), add_note_request.text())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_get_note() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
testing::seed::<App>(&ctx.db).await.unwrap();
|
|
||||||
|
|
||||||
let add_note_request = request.get("/api/notes/1").await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => {
|
|
||||||
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
|
|
||||||
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
|
|
||||||
combined_filters
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(
|
|
||||||
(add_note_request.status_code(), add_note_request.text())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn can_delete_note() {
|
|
||||||
configure_insta!();
|
|
||||||
|
|
||||||
testing::request::<App, _, _>(|request, ctx| async move {
|
|
||||||
testing::seed::<App>(&ctx.db).await.unwrap();
|
|
||||||
|
|
||||||
let count_before_delete = Entity::find().all(&ctx.db).await.unwrap().len();
|
|
||||||
let delete_note_request = request.delete("/api/notes/1").await;
|
|
||||||
|
|
||||||
with_settings!({
|
|
||||||
filters => {
|
|
||||||
let mut combined_filters = testing::CLEANUP_DATE.to_vec();
|
|
||||||
combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]);
|
|
||||||
combined_filters
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
assert_debug_snapshot!(
|
|
||||||
(delete_note_request.status_code(), delete_note_request.text())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let count_after_delete = Entity::find().all(&ctx.db).await.unwrap().len();
|
|
||||||
assert_eq!(count_after_delete, count_before_delete - 1);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
use axum::http::{HeaderName, HeaderValue};
|
|
||||||
use loco_rs::{app::AppContext, TestServer};
|
|
||||||
use nixin_farm::{models::users, views::auth::LoginResponse};
|
|
||||||
|
|
||||||
const USER_EMAIL: &str = "test@loco.com";
|
|
||||||
const USER_PASSWORD: &str = "1234";
|
|
||||||
|
|
||||||
pub struct LoggedInUser {
|
|
||||||
pub user: users::Model,
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_user_login(request: &TestServer, ctx: &AppContext) -> LoggedInUser {
|
|
||||||
let register_payload = serde_json::json!({
|
|
||||||
"name": "loco",
|
|
||||||
"email": USER_EMAIL,
|
|
||||||
"password": USER_PASSWORD
|
|
||||||
});
|
|
||||||
|
|
||||||
//Creating a new user
|
|
||||||
request
|
|
||||||
.post("/api/auth/register")
|
|
||||||
.json(®ister_payload)
|
|
||||||
.await;
|
|
||||||
let user = users::Model::find_by_email(&ctx.db, USER_EMAIL)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let verify_payload = serde_json::json!({
|
|
||||||
"token": user.email_verification_token,
|
|
||||||
});
|
|
||||||
|
|
||||||
request.post("/api/auth/verify").json(&verify_payload).await;
|
|
||||||
|
|
||||||
let response = request
|
|
||||||
.post("/api/auth/login")
|
|
||||||
.json(&serde_json::json!({
|
|
||||||
"email": USER_EMAIL,
|
|
||||||
"password": USER_PASSWORD
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let login_response: LoginResponse = serde_json::from_str(&response.text()).unwrap();
|
|
||||||
|
|
||||||
LoggedInUser {
|
|
||||||
user: users::Model::find_by_email(&ctx.db, USER_EMAIL)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
token: login_response.token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auth_header(token: &str) -> (HeaderName, HeaderValue) {
|
|
||||||
let auth_header_value = HeaderValue::from_str(&format!("Bearer {}", &token)).unwrap();
|
|
||||||
|
|
||||||
(HeaderName::from_static("authorization"), auth_header_value)
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/requests/notes.rs
|
|
||||||
expression: "(add_note_request.status_code(), add_note_request.text())"
|
|
||||||
---
|
|
||||||
(
|
|
||||||
200,
|
|
||||||
"{\"created_at\":\"DATEZ\",\"updated_at\":\"DATEZ\",\"id\":ID,\"title\":\"loco\",\"content\":\"loco note test\"}",
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/requests/notes.rs
|
|
||||||
expression: "(delete_note_request.status_code(), delete_note_request.text())"
|
|
||||||
---
|
|
||||||
(
|
|
||||||
200,
|
|
||||||
"",
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/requests/user.rs
|
|
||||||
expression: "(response.status_code(), response.text())"
|
|
||||||
---
|
|
||||||
(
|
|
||||||
200,
|
|
||||||
"{\"pid\":\"PID\",\"name\":\"loco\",\"email\":\"test@loco.com\"}",
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
source: tests/requests/notes.rs
|
|
||||||
expression: "(add_note_request.status_code(), add_note_request.text())"
|
|
||||||
---
|
|
||||||
(
|
|
||||||
200,
|
|
||||||
"{\"created_at\":\"DATEZ\",\"updated_at\":\"DATEZ\",\"id\":ID,\"title\":\"Loco note 1\",\"content\":\"Loco note 1 content\"}",
|
|
||||||
)
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue