Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit a996871e authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

Merge branch 'dev' into 'master'

Dev

See merge request !1
parents d30c3361 b68000c1
No related branches found
No related tags found
No related merge requests found
Showing
with 5437 additions and 31 deletions
This diff is collapsed.
...@@ -12,5 +12,13 @@ actix-web = { version = "3", features=["rustls"] } ...@@ -12,5 +12,13 @@ actix-web = { version = "3", features=["rustls"] }
actix-web-middleware-redirect-https = "3.0.1" actix-web-middleware-redirect-https = "3.0.1"
rustls="0.18.1" rustls="0.18.1"
actix-files="0.5" actix-files="0.5"
futures = "0.3.17"
serde = "1"
serde_json="1"
wither="0.9"
magic-crypt="3"
env_logger="0.9" env_logger="0.9"
chrono="0.4"
rand="0.8"
dotenv="0.15" dotenv="0.15"
tokio = { version = "0.2", features = ["full"] }
\ No newline at end of file
...@@ -22,11 +22,17 @@ doc: ...@@ -22,11 +22,17 @@ doc:
bash-api: bash-api:
docker exec -it kuadrado_server bash docker exec -it kuadrado_server bash
build-front: build-website:
npm run --prefix ./website build npm run --prefix ./website build
build-front-debug: build-website-debug:
npm run --prefix ./website build debug npm run --prefix ./website build debug
build-admin:
npm run --prefix ./admin-frontend build
build-admin-debug:
npm run --prefix ./admin-frontend build debug
logs: logs:
docker-compose logs -f docker-compose logs -f
\ No newline at end of file
#!/usr/bin/env node
const { bundle } = require("simple-browser-js-bundler");
const path = require("path");
const dir = process.cwd();
bundle(
`${dir}/src/index.js`,
path.resolve(dir, "../public/views/admin-panel/assets/bundle.js"),
{
minify: !process.argv.includes("debug")
}
);
\ No newline at end of file
This diff is collapsed.
{
"name": "admin-frontend",
"version": "1.0.0",
"description": "An admin panel app for the Mentalo API",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "node ./build.js",
"build-debug": "node ./build.js debug"
},
"author": "kuadrado-software",
"license": "GPL-3.0",
"dependencies": {
"object-to-html-renderer": "^1.1.3"
},
"devDependencies": {
"dotenv": "^10.0.0",
"simple-browser-js-bundler": "^0.1.1"
}
}
\ No newline at end of file
"use strict";
class Article {
constructor(data) {
if (data) {
this.from(data)
} else {
this.title = "";
this.subtitle = "";
this.category = "";
this.details = [];
this.images = [];
this.body = "";
this.locale = "";
}
}
from(data) {
Object.entries(data).forEach(k_v => {
const [key, value] = k_v;
this[key] = value;
});
}
}
module.exports = Article;
\ No newline at end of file
"use strict";
const Article = require("../article");
const { images_url } = require("../constants");
const { fetch_post_article, fetch_article, fetch_update_article } = require("../xhr");
class CreateArticleForm {
constructor(params) {
this.params = params || {};
this.state = {
output: new Article(this.params.data),
article_sent: {},
}
}
reset() {
this.state.output = new Article();
this.state.article_sent = {};
this.refresh();
}
handle_text_input(field, e) {
this.state.output[field] = e.target.value;
}
handle_del_detail(index) {
this.state.output.details.splice(index, 1);
this.refresh_details();
}
handle_add_detail() {
this.state.output.details.push({ label: "", value: "" });
this.refresh_details();
}
handle_del_image(index) {
this.state.output.images.splice(index, 1);
this.refresh_images();
}
handle_add_image() {
this.state.output.images.push("")
this.refresh_images();
}
refresh_details() {
obj2htm.subRender(
this.render_details_inputs(),
document.getElementById("create-article-form-details"),
{ mode: "replace" }
);
}
render_details_inputs() {
return {
tag: "ul",
style_rules: {
gridColumn: "1 / span 2",
display: "flex",
flexDirection: "column",
gap: "10px",
listStyleType: "none",
padding: 0,
},
id: "create-article-form-details",
contents: this.state.output.details.map((detail, i) => {
return {
tag: "li",
style_rules: {
display: "grid",
gridTemplateColumns: "200px auto 60px",
gap: "10px",
},
contents: [
{
tag: "input",
type: "text",
placeholder: "Label",
value: detail.label,
oninput: e => {
this.state.output.details[i].label = e.target.value;
}
},
{
tag: "input",
type: "text",
placeholder: "Value",
value: detail.value,
oninput: e => {
this.state.output.details[i].value = e.target.value;
}
},
{
tag: "button", contents: "DEL",
onclick: this.handle_del_detail.bind(this, i)
}
]
}
}).concat([
{
tag: "li", contents: [{
tag: "button", contents: "ADD DETAIL",
onclick: this.handle_add_detail.bind(this)
}]
}
])
}
}
refresh_images() {
obj2htm.subRender(
this.render_images_inputs(),
document.getElementById("create-article-form-images"),
{ mode: "replace" }
);
}
render_images_inputs() {
return {
tag: "ul",
style_rules: {
gridColumn: "1 / span 2",
display: "flex",
flexDirection: "column",
gap: "10px",
listStyleType: "none",
padding: 0,
},
id: "create-article-form-images",
contents: this.state.output.images.map((img, i) => {
return {
tag: "li",
style_rules: {
display: "flex",
alignItems: "center",
gap: "10px",
},
contents: [
{
tag: "div",
style_rules: {
display: "flex",
flexDirection: "center",
alignItems: "center",
justifyContent: "center",
width: "150px",
height: "150px",
overflow: "hidden",
},
contents: [
{
tag: "img",
style_rules: { minWidth: "100%", minHeight: "100%" },
src: img ? `${images_url}/${img}` : "",
}
],
},
{
tag: "input",
type: "text",
placeholder: "image file name",
value: img,
oninput: e => {
this.state.output.images[i] = e.target.value;
}
},
{
tag: "button", contents: "OK",
onclick: this.refresh_images.bind(this)
},
{
tag: "button", contents: "DEL",
onclick: this.handle_del_image.bind(this, i)
}
]
}
}).concat([
{
tag: "li", contents: [{
tag: "button", contents: "ADD IMAGE",
onclick: this.handle_add_image.bind(this)
}]
}
])
}
}
render_article_sent() {
const article = this.state.article_sent;
return {
tag: "div",
style_rules: {
maxWidth: "800px",
},
contents: [
{ tag: "button", contents: "RESET", onclick: this.reset.bind(this) },
{ tag: "h2", contents: article.title },
{ tag: "h4", contents: article.subtitle },
{ tag: "p", contents: article.body.replace(/\n/g, "<br>") },
{
tag: "ul", contents: article.details.map(det => {
return {
tag: "li",
style_rules: {
display: "flex",
gap: "20px",
justifyContent: "space-between",
},
contents: [
{ tag: "span", contents: det.label },
{ tag: "span", contents: det.value }
]
};
})
},
{
tag: "div", style_rules: { display: "flex", gap: "10px" },
contents: article.images.map(img => {
return {
tag: "img",
style_rules: { height: "100px", width: "auto" },
src: `${images_url}/${img}`
}
})
}
]
}
}
refresh() {
obj2htm.subRender(
this.render(),
document.getElementById("create-article-form"),
{ mode: "replace" }
);
}
render() {
return {
tag: "form",
id: "create-article-form",
style_rules: {
display: "grid",
maxWidth: "800px",
gridTemplateColumns: "1fr 1fr",
gap: "20px",
},
onsubmit: e => {
e.preventDefault();
const __fetch = this.params.data
? fetch_update_article
: fetch_post_article;
__fetch(this.state.output)
.then(res => {
const id = res.insertedId ? res.insertedId.$oid : res._id ? res._id.$oid : undefined;
if (!id) {
alert("error")
} else {
fetch_article(id)
.then(article => {
this.state.article_sent = article;
this.params.on_article_sent && this.params.on_article_sent();
this.refresh();
})
.catch(er => console.log(er));
}
})
.catch(err => console.log(err))
},
contents: this.state.article_sent._id ? [this.render_article_sent()] : [
{
tag: "input", type: "text", placeholder: "category",
value: this.state.output.category,
oninput: this.handle_text_input.bind(this, "category")
},
{
tag: "select", value: this.state.output.locale,
onchange: e => this.state.output.locale = e.target.value,
contents: [{
tag: "option",
value: "",
contents: "-- LOCALE --"
}].concat(["fr", "en", "es"].map(loc => {
return {
tag: "option",
value: loc,
contents: loc,
selected: this.state.output.locale === loc
}
}))
},
{
tag: "input", type: "text",
placeholder: "Article title",
value: this.state.output.title,
oninput: this.handle_text_input.bind(this, "title")
},
{
tag: "input", type: "text",
style_rules: {
gridColumn: "1 / span 2"
},
placeholder: "Article subtitle",
value: this.state.output.subtitle,
oninput: this.handle_text_input.bind(this, "subtitle")
},
{
tag: "textarea",
style_rules: {
gridColumn: "1 / span 2",
height: "300px",
},
value: this.state.output.body,
placeholder: "Article body",
oninput: this.handle_text_input.bind(this, "body")
},
this.render_details_inputs(),
this.render_images_inputs(),
{ tag: "input", type: "submit" }
]
}
}
}
module.exports = CreateArticleForm;
\ No newline at end of file
const CreateArticleForm = require("./create-article-form");
const UpdateArticleForm = require("./update-article-form");
class RootComponent {
constructor() {
this.state = {
selected_tab: ""
};
}
handle_nav_click(e) {
this.state.selected_tab = e.target.tab_name;
obj2htm.renderCycle();
}
render_state() {
switch (this.state.selected_tab) {
case "create":
return new CreateArticleForm().render();
case "update":
return new UpdateArticleForm().render();
default:
return undefined;
}
}
render() {
return {
tag: "main",
contents: [
{ tag: "h1", contents: "Kuadrado admin panel" },
{
tag: "nav",
contents: [
{
tag: "span", contents: "Create article", tab_name: "create",
class: this.state.selected_tab === "create" ? "selected" : "",
onclick: this.handle_nav_click.bind(this),
},
{
tag: "span", contents: "Update article", tab_name: "update",
class: this.state.selected_tab === "update" ? "selected" : "",
onclick: this.handle_nav_click.bind(this),
},
],
},
this.render_state(),
],
};
}
}
module.exports = RootComponent;
\ No newline at end of file
"use strict";
const { fetch_article_by_title, fetch_delete_article } = require("../xhr");
const CreateArticleForm = require("./create-article-form");
class UpdateArticleForm {
constructor() {
this.state = {
search_article_title: "",
search_result: {},
article_to_update: {},
}
}
reset() {
this.state = {
search_article_title: "",
search_result: {},
article_to_update: {},
};
}
handle_search_article() {
fetch_article_by_title(this.state.search_article_title)
.then(res => {
this.state.search_result = res;
this.state.article_to_update = {};
this.refresh_search_result();
this.refresh_update_form();
})
.catch(err => alert(err));
}
handle_select_result() {
this.state.article_to_update = { ...this.state.search_result };
this.refresh_update_form();
}
handle_delete_article() {
fetch_delete_article(this.state.search_result._id.$oid)
.then(res => {
alert(res);
this.reset();
this.refresh();
})
.catch(err => alert(err))
}
render_search() {
return {
tag: "form",
onsubmit: e => {
e.preventDefault();
this.handle_search_article();
},
style_rules: { display: "flex", gap: "10px", width: "100%" },
contents: [
{
tag: "input", type: "text", value: this.state.search_article_title,
style_rules: { flex: 1 },
placeholder: "Search article by title",
oninput: e => this.state.search_article_title = e.target.value,
},
{
tag: "input", type: "submit", value: "SEARCH"
}
]
}
}
refresh_search_result() {
obj2htm.subRender(
this.render_search_result(),
document.getElementById("update-article-form-search-result"),
{ mode: "replace" },
);
}
render_search_result() {
const { search_result } = this.state;
return {
tag: "div",
id: "update-article-form-search-result",
style_rules: {
display: "flex",
gap: "10px",
alignItems: "center"
},
contents: search_result.title ? [
{ tag: "strong", contents: search_result.title },
{
tag: "button", contents: "SELECT",
onclick: this.handle_select_result.bind(this)
},
{
tag: "button", contents: "DELETE",
onclick: this.handle_delete_article.bind(this)
}
] : []
}
}
refresh_update_form() {
obj2htm.subRender(
this.render_update_form(),
document.getElementById("update-article-form-container"),
{ mode: "replace" },
);
}
render_update_form() {
return {
tag: "div",
id: "update-article-form-container",
contents: this.state.article_to_update._id
? [new CreateArticleForm({
data: this.state.article_to_update,
on_article_sent: () => {
this.reset();
this.refresh_search_result();
}
}).render()]
: []
}
}
refresh() {
obj2htm.subRender(this.render(), document.getElementById("update-article-multiform"), { mode: "replace" })
}
render() {
return {
tag: "div",
id: "update-article-multiform",
style_rules: {
display: "flex",
flexDirection: "column",
gap: "20px",
maxWidth: "800px",
},
contents: [
this.render_search(),
this.render_search_result(),
{ tag: "hr", style_rules: { width: "100%" } },
this.render_update_form(),
]
}
}
}
module.exports = UpdateArticleForm;
\ No newline at end of file
module.exports = {
images_url: "/assets/images"
}
\ No newline at end of file
const renderer = require("object-to-html-renderer");
const RootComponent = require("./components/root");
renderer.register("obj2htm");
obj2htm.setRenderCycleRoot(new RootComponent());
obj2htm.renderCycle();
\ No newline at end of file
function get_text_date(iso_str) {
const date = new Date(iso_str);
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} - ${date.getHours()}h${date.getMinutes()}mn`
}
module.exports = {
get_text_date
}
\ No newline at end of file
async function fetch_article(article_id) {
return new Promise((resolve, reject) => {
fetch(`/article/${article_id}`).then(async res => {
if (res.status >= 400 && res.status < 600) {
const text = await res.text();
reject(text);
} else {
resolve(await res.json());
}
}).catch(e => reject(e))
})
}
async function fetch_article_by_title(article_title) {
const form_data = new FormData();
form_data.append("title", article_title);
return new Promise((resolve, reject) => {
fetch(`/article-by-title/`, {
method: "POST",
body: new URLSearchParams(form_data),
}).then(async res => {
if (res.status >= 400 && res.status < 600) {
const text = await res.text();
reject(text);
} else {
resolve(await res.json());
}
}).catch(e => reject(e))
})
}
async function fetch_articles_by_category(category) {
return new Promise((resolve, reject) => {
fetch(`/articles/${category}`).then(async res => {
if (res.status >= 400 && res.status < 600) {
const text = await res.text();
reject(text);
} else {
resolve(await res.json());
}
}).catch(e => reject(e))
})
}
async function fetch_post_article(article_data) {
return new Promise((resolve, reject) => {
fetch("/post-article", {
credentials: 'include',
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(article_data),
})
.then(async res => {
if (res.status >= 400 && res.status < 600) {
const text = await res.text();
reject(text)
} else {
const json = await res.json();
resolve(json);
}
})
.catch(err => reject(err))
})
}
async function fetch_update_article(article_data) {
return new Promise((resolve, reject) => {
fetch(`/update-article/${article_data._id.$oid}`, {
credentials: 'include',
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(article_data),
})
.then(async res => {
if (res.status >= 400 && res.status < 600) {
const text = await res.text();
reject(text)
} else {
const json = await res.json();
resolve(json);
}
})
.catch(err => reject(err))
})
}
async function fetch_delete_article(article_id) {
return new Promise((resolve, reject) => {
fetch(`/delete-article/${article_id}`, {
credentials: 'include',
method: "DELETE"
})
.then(async res => {
const text = await res.text();
if (res.status >= 400 && res.status < 600) {
reject(text)
} else {
resolve(text);
}
})
.catch(err => reject(err))
});
}
module.exports = {
fetch_article,
fetch_article_by_title,
fetch_articles_by_category,
fetch_post_article,
fetch_update_article,
fetch_delete_article,
}
\ No newline at end of file
...@@ -5,6 +5,8 @@ services: ...@@ -5,6 +5,8 @@ services:
context: . context: .
dockerfile: ./dev.Dockerfile dockerfile: ./dev.Dockerfile
container_name: "kuadrado_server" container_name: "kuadrado_server"
depends_on:
- ${DATABASE_NAME}
restart: unless-stopped restart: unless-stopped
ports: ports:
- 80:${SERVER_PORT} - 80:${SERVER_PORT}
...@@ -17,3 +19,17 @@ services: ...@@ -17,3 +19,17 @@ services:
command: cargo run command: cargo run
env_file: env_file:
- ./.env - ./.env
kuadradodb:
build: ./mongo/
container_name: ${DATABASE_NAME}
environment:
- MONGO_INITDB_DATABASE=${DATABASE_NAME}
- MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MONGO_INITDB_NON_ROOT_USERNAME=${DB_USERNAME}
- MONGO_INITDB_NON_ROOT_PASSWORD=${DB_USER_PASSWORD}
volumes:
- ./mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
- /var/${DATABASE_NAME}-volume:/data/db
ports:
- "27017-27019:27017-27019"
...@@ -5,6 +5,8 @@ services: ...@@ -5,6 +5,8 @@ services:
context: . context: .
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
container_name: "kuadrado_server" container_name: "kuadrado_server"
depends_on:
- ${DATABASE_NAME}
restart: unless-stopped restart: unless-stopped
ports: ports:
- 80:${SERVER_PORT} - 80:${SERVER_PORT}
...@@ -14,3 +16,17 @@ services: ...@@ -14,3 +16,17 @@ services:
- /etc/letsencrypt/:${RESOURCES_DIR}/certs:ro - /etc/letsencrypt/:${RESOURCES_DIR}/certs:ro
env_file: env_file:
- ./.env - ./.env
kuadradodb:
build: ./mongo/
container_name: ${DATABASE_NAME}
environment:
- MONGO_INITDB_DATABASE=${DATABASE_NAME}
- MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MONGO_INITDB_NON_ROOT_USERNAME=${DB_USERNAME}
- MONGO_INITDB_NON_ROOT_PASSWORD=${DB_USER_PASSWORD}
volumes:
- ./mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
- /var/${DATABASE_NAME}-volume:/data/db
ports:
- "27017-27019:27017-27019"
from mongo
RUN mkdir /mongoinit && chown mongodb -R /mongoinit && chgrp mongodb -R /mongoinit
\ No newline at end of file
function getEnvVariable(envVar) {
// Thanks for the tip: https://dev.to/jsheridanwells/dockerizing-a-mongo-database-4jf2
const command = run("sh", "-c", `printenv --null ${envVar} >/mongoinit/${envVar}.txt`);
// note: 'printenv --null' prevents adding line break to value
if (command != 0) return Error("Failed to retrieve env variable : " + envVar);
// .replace(/\0/g, '') removes the NULL characters
return cat(`/mongoinit/${envVar}.txt`).replace(/\0/g, '');
}
db.createUser({
user: getEnvVariable("MONGO_INITDB_NON_ROOT_USERNAME"),
pwd: getEnvVariable("MONGO_INITDB_NON_ROOT_PASSWORD"),
roles: [
{
role: "readWrite",
db: getEnvVariable("MONGO_INITDB_DATABASE")
}
]
});
db = new Mongo().getDB(getEnvVariable("MONGO_INITDB_DATABASE"));
db.createCollection("articles");
db.createCollection("administrators");
db.administrators.createIndex({ "username": 1 }, { unique: true });
db.administrators.createIndex({ "auth_token": 1 }, { unique: true });
run("sh", "-c", "rm -rf /mongoinit/*");
\ No newline at end of file
{
"name": "kuadrado-website",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment