Compare commits

..

No commits in common. "master" and "create-offer" have entirely different histories.

128 changed files with 2138 additions and 7594 deletions

View file

@ -1 +0,0 @@
public/javascript/*

8
.gitignore vendored
View file

@ -136,10 +136,4 @@ dist
.pnp.*
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# webpack bundles
/public/javascript/*
tests-results/

View file

@ -1,6 +0,0 @@
const path = require('path');
module.exports = {
config: path.resolve('src', 'database', 'config.js'),
'migrations-path': path.resolve('src', 'database', 'migrations'),
};

View file

@ -1,10 +0,0 @@
# secajs
## Architecture
This repository contains a webapp. It covers the full stack which consists of:
- The client side code, in `src/front/`.
- The backend service in `src/`, except for `src/front/`.
- A Postgres database. Database connections and migrations are in `src/database/`.
- Besides, there is an admin CLI, with entrypoint in `src/cli.js` and commands in `src/commands/`.

View file

@ -1,22 +1,10 @@
FROM debian:12
FROM debian:latest
# Install dependencies
RUN apt-get update
RUN apt-get install -y \
curl gnupg2 ca-certificates lsb-release apt-transport-https
RUN apt-get install -y \
postgresql
RUN apt-get install -y \
caddy
RUN apt-get install -y \
nodejs npm
RUN rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y \
curl gnupg2 ca-certificates lsb-release apt-transport-https \
postgresql caddy nodejs npm && \
rm -rf /var/lib/apt/lists/*
RUN echo "listen_addresses='*'" >> /etc/postgresql/15/main/postgresql.conf && \
echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/15/main/pg_hba.conf && \

View file

@ -2,6 +2,12 @@
laseca is a social bitcoin to cash exchange, implemented as a webapp.
## Upcoming stories
* [ ] Create offer
* [ ] Display existing offers
* [ ] Archive offer
## How to set up dev environment
* Pre-requisites
@ -10,16 +16,11 @@ laseca is a social bitcoin to cash exchange, implemented as a webapp.
* Installing
+ Run `npm install`
+ You can now start the app in a container by running `npm run start:container` (and shut it down with `npm run stop:container`).
* Building
+ The front-end code gets built with webpack. You can build it anytime with `npm run build`.
+ For development, it's useful to build continuously. You can run `npm run watch` and webpack will build every time you edit a monitored file.
* Running
+ Copy the `.env.dist` file into `.env` and set any values you like.
+ The app will run in a single container, with a Postgres database, a caddy webserver and the nodejs app.
+ Note that the container doesn't come with a volume for Postgres: default behaviour is to start from scratch every time you create the container, delete everything every time you delete the container.
+ You probably want to run migrations to get the database into proper state. You can do so with `npx sequelize-cli db:migrate`.
+ The docker image launches the nodejs app with nodemon, so changes to the code will be available immediately.
+ Furthermore, since the git repository gets mounted live into the docker container, the live changes made by webpack watch mode will also be available as you work on front end files.
+ The Postgres database is reachable from the host, so you can use your favourite SQL client to access it.
+ You can format with `npm run format` and lint with `npm run lint`.

1956
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,13 +20,10 @@
"start": "node src/app.js",
"start:container": "docker compose up -d --build",
"stop:container": "docker compose down",
"migrate": "npx sequelize-cli db:migrate",
"build": "webpack",
"watch": "webpack --watch",
"cli": "node src/cli.js",
"test": "playwright test",
"lint": "eslint . --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\"",
"test": "playwright test"
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\""
},
"keywords": [],
"author": "",
@ -39,9 +36,6 @@
"globals": "^15.15.0",
"playwright": "^1.50.1",
"prettier": "^3.5.1",
"prettier-plugin-ejs": "^1.0.3",
"sequelize-cli": "^6.6.2",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1"
"prettier-plugin-ejs": "^1.0.3"
}
}

View file

@ -1,13 +0,0 @@
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
},
webServer: {
command: 'npm start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

View file

@ -1,64 +0,0 @@
@media (max-width: 768px) {
.over-background > *:first-child {
margin-bottom: 5px;
}
.over-background > *:last-child {
margin-top: 5px;
}
.over-background > * {
margin-bottom: 5px;
margin-top: 5px;
}
}
@media (min-width: 769px) {
.over-background > *:first-child {
margin-bottom: 10px;
}
.over-background > *:last-child {
margin-top: 10px;
}
.over-background > * {
margin-bottom: 10px;
margin-top: 10px;
}
}
.badges {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.badge {
border: 2px solid #e1c300;
border-radius: 10px;
padding: 2%;
margin: 1%;
}
.badge input {
max-width: 80%;
}
.create-profile-card-section > * {
text-align: center;
margin-bottom: 10px;
margin-right: auto;
margin-left: auto;
display: block;
max-width: 1000px;
}
.create-profile-card-section .badge img {
width: 10%;
}
h2 {
margin-top: 0;
margin-bottom: 5px;
}

View file

@ -1,39 +0,0 @@
@media (max-width: 768px) {
#nostr-signup-button {
width: 200px;
}
#invite-card-content {
width: 100%;
}
}
@media (min-width: 769px) {
#nostr-signup-button {
width: 300px;
}
#invite-card-content {
width: 40%;
min-width: min-content;
}
}
#laseca-logo {
width: 200px;
}
#nostr-signup-button p {
font-size: 1.5rem;
}
#invite-card-content {
margin-right: auto;
margin-left: auto;
padding: 20px;
}
#invite-card-content > * {
margin: 1vh auto;
text-align: center;
}

View file

@ -1,39 +0,0 @@
@media (max-width: 768px) {
#login-button {
width: 200px;
}
.logo {
width: 350px;
}
#login-card-content {
width: 100%;
}
}
@media (min-width: 769px) {
#login-button {
width: 300px;
}
.logo {
width: 500px;
}
#login-card-content {
width: 40%;
min-width: min-content;
}
}
#login-card-content {
margin-right: auto;
margin-left: auto;
padding: 20px;
}
#login-card-content > * {
margin: 1vh auto;
text-align: center;
}

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- bolt-lightning icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 64 256 L 93 28 L 64 256 L 93 28 Q 98 2 124 0 L 293 0 L 293 0 Q 304 0 312 8 Q 320 16 320 27 Q 320 32 318 37 L 272 160 L 272 160 L 411 160 L 411 160 Q 427 160 437 171 Q 448 181 448 197 Q 448 208 442 217 L 249 498 L 249 498 Q 240 512 224 512 L 221 512 L 221 512 Q 209 512 200 504 Q 192 496 192 484 Q 192 480 193 477 L 240 288 L 240 288 L 96 288 L 96 288 Q 82 288 73 279 Q 64 270 64 256 L 64 256 Z"
id="path1"
style="fill:#000000;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 829 B

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- bolt-lightning icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="bolt-lightning-black.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 64 256 L 93 28 L 64 256 L 93 28 Q 98 2 124 0 L 293 0 L 293 0 Q 304 0 312 8 Q 320 16 320 27 Q 320 32 318 37 L 272 160 L 272 160 L 411 160 L 411 160 Q 427 160 437 171 Q 448 181 448 197 Q 448 208 442 217 L 249 498 L 249 498 Q 240 512 224 512 L 221 512 L 221 512 Q 209 512 200 504 Q 192 496 192 484 Q 192 480 193 477 L 240 288 L 240 288 L 96 288 L 96 288 Q 82 288 73 279 Q 64 270 64 256 L 64 256 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- bolt-lightning icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 64 256 L 93 28 L 64 256 L 93 28 Q 98 2 124 0 L 293 0 L 293 0 Q 304 0 312 8 Q 320 16 320 27 Q 320 32 318 37 L 272 160 L 272 160 L 411 160 L 411 160 Q 427 160 437 171 Q 448 181 448 197 Q 448 208 442 217 L 249 498 L 249 498 Q 240 512 224 512 L 221 512 L 221 512 Q 209 512 200 504 Q 192 496 192 484 Q 192 480 193 477 L 240 288 L 240 288 L 96 288 L 96 288 Q 82 288 73 279 Q 64 270 64 256 L 64 256 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 829 B

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- bolt-lightning icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="bolt-lightning.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 64 256 L 93 28 L 64 256 L 93 28 Q 98 2 124 0 L 293 0 L 293 0 Q 304 0 312 8 Q 320 16 320 27 Q 320 32 318 37 L 272 160 L 272 160 L 411 160 L 411 160 Q 427 160 437 171 Q 448 181 448 197 Q 448 208 442 217 L 249 498 L 249 498 Q 240 512 224 512 L 221 512 L 221 512 Q 209 512 200 504 Q 192 496 192 484 Q 192 480 193 477 L 240 288 L 240 288 L 96 288 L 96 288 Q 82 288 73 279 Q 64 270 64 256 L 64 256 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- link-horizontal icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 116 102.4 Q 66.4 104 33.6 136 L 33.6 136 L 33.6 136 Q 1.6 168.8 0 218.4 Q 0.8 261.6 27.2 292.8 Q 54.4 324 96.8 332 L 98.4 332.8 L 98.4 332.8 Q 108.8 333.6 117.6 328 Q 125.6 322.4 128 311.2 Q 128.8 300.8 123.2 292 Q 117.6 284 106.4 281.6 L 104.8 281.6 L 104.8 281.6 Q 81.6 277.6 66.4 260 Q 52 242.4 51.2 218.4 Q 52 190.4 70.4 172.8 Q 88 154.4 116 153.6 L 242.4 153.6 L 242.4 153.6 Q 270.4 154.4 288 172.8 Q 306.4 190.4 307.2 218.4 Q 306.4 242.4 292 260 Q 276.8 277.6 253.6 281.6 L 252 281.6 L 252 281.6 Q 240.8 284 235.2 292 Q 229.6 300.8 230.4 311.2 Q 232.8 322.4 240.8 328 Q 249.6 333.6 260 332 L 261.6 332 L 261.6 332 Q 304 324 331.2 292.8 Q 357.6 261.6 358.4 218.4 Q 356.8 168.8 324.8 136 Q 292 104 242.4 102.4 L 116 102.4 L 116 102.4 Z M 396 409.6 Q 445.6 408 478.4 376 L 478.4 376 L 478.4 376 Q 510.4 343.2 512 293.6 Q 511.2 250.4 484.8 219.2 Q 457.6 188 415.2 180 L 413.6 179.2 L 413.6 179.2 Q 403.2 178.4 394.4 184 Q 386.4 189.6 384.8 200.8 Q 383.2 211.2 388.8 220 Q 394.4 228 405.6 230.4 L 407.2 230.4 L 407.2 230.4 Q 430.4 234.4 445.6 252 Q 460.8 269.6 460.8 293.6 Q 460 321.6 441.6 339.2 Q 424 357.6 396 358.4 L 269.6 358.4 L 269.6 358.4 Q 241.6 357.6 224 339.2 Q 205.6 321.6 204.8 293.6 Q 205.6 269.6 220 252 Q 235.2 234.4 258.4 230.4 L 260 230.4 L 260 230.4 Q 271.2 228 276.8 220 Q 282.4 211.2 281.6 200.8 Q 279.2 189.6 271.2 184 Q 262.4 178.4 252 179.2 L 250.4 180 L 250.4 180 Q 208 188 180.8 219.2 Q 154.4 250.4 153.6 293.6 Q 155.2 343.2 187.2 376 Q 220 408 269.6 409.6 L 396 409.6 L 396 409.6 Z"
id="path1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- link-horizontal icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="chains-gray.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 116 102.4 Q 66.4 104 33.6 136 L 33.6 136 L 33.6 136 Q 1.6 168.8 0 218.4 Q 0.8 261.6 27.2 292.8 Q 54.4 324 96.8 332 L 98.4 332.8 L 98.4 332.8 Q 108.8 333.6 117.6 328 Q 125.6 322.4 128 311.2 Q 128.8 300.8 123.2 292 Q 117.6 284 106.4 281.6 L 104.8 281.6 L 104.8 281.6 Q 81.6 277.6 66.4 260 Q 52 242.4 51.2 218.4 Q 52 190.4 70.4 172.8 Q 88 154.4 116 153.6 L 242.4 153.6 L 242.4 153.6 Q 270.4 154.4 288 172.8 Q 306.4 190.4 307.2 218.4 Q 306.4 242.4 292 260 Q 276.8 277.6 253.6 281.6 L 252 281.6 L 252 281.6 Q 240.8 284 235.2 292 Q 229.6 300.8 230.4 311.2 Q 232.8 322.4 240.8 328 Q 249.6 333.6 260 332 L 261.6 332 L 261.6 332 Q 304 324 331.2 292.8 Q 357.6 261.6 358.4 218.4 Q 356.8 168.8 324.8 136 Q 292 104 242.4 102.4 L 116 102.4 L 116 102.4 Z M 396 409.6 Q 445.6 408 478.4 376 L 478.4 376 L 478.4 376 Q 510.4 343.2 512 293.6 Q 511.2 250.4 484.8 219.2 Q 457.6 188 415.2 180 L 413.6 179.2 L 413.6 179.2 Q 403.2 178.4 394.4 184 Q 386.4 189.6 384.8 200.8 Q 383.2 211.2 388.8 220 Q 394.4 228 405.6 230.4 L 407.2 230.4 L 407.2 230.4 Q 430.4 234.4 445.6 252 Q 460.8 269.6 460.8 293.6 Q 460 321.6 441.6 339.2 Q 424 357.6 396 358.4 L 269.6 358.4 L 269.6 358.4 Q 241.6 357.6 224 339.2 Q 205.6 321.6 204.8 293.6 Q 205.6 269.6 220 252 Q 235.2 234.4 258.4 230.4 L 260 230.4 L 260 230.4 Q 271.2 228 276.8 220 Q 282.4 211.2 281.6 200.8 Q 279.2 189.6 271.2 184 Q 262.4 178.4 252 179.2 L 250.4 180 L 250.4 180 Q 208 188 180.8 219.2 Q 154.4 250.4 153.6 293.6 Q 155.2 343.2 187.2 376 Q 220 408 269.6 409.6 L 396 409.6 L 396 409.6 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- link-horizontal icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="chains-lasecagold.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 116 102.4 Q 66.4 104 33.6 136 L 33.6 136 L 33.6 136 Q 1.6 168.8 0 218.4 Q 0.8 261.6 27.2 292.8 Q 54.4 324 96.8 332 L 98.4 332.8 L 98.4 332.8 Q 108.8 333.6 117.6 328 Q 125.6 322.4 128 311.2 Q 128.8 300.8 123.2 292 Q 117.6 284 106.4 281.6 L 104.8 281.6 L 104.8 281.6 Q 81.6 277.6 66.4 260 Q 52 242.4 51.2 218.4 Q 52 190.4 70.4 172.8 Q 88 154.4 116 153.6 L 242.4 153.6 L 242.4 153.6 Q 270.4 154.4 288 172.8 Q 306.4 190.4 307.2 218.4 Q 306.4 242.4 292 260 Q 276.8 277.6 253.6 281.6 L 252 281.6 L 252 281.6 Q 240.8 284 235.2 292 Q 229.6 300.8 230.4 311.2 Q 232.8 322.4 240.8 328 Q 249.6 333.6 260 332 L 261.6 332 L 261.6 332 Q 304 324 331.2 292.8 Q 357.6 261.6 358.4 218.4 Q 356.8 168.8 324.8 136 Q 292 104 242.4 102.4 L 116 102.4 L 116 102.4 Z M 396 409.6 Q 445.6 408 478.4 376 L 478.4 376 L 478.4 376 Q 510.4 343.2 512 293.6 Q 511.2 250.4 484.8 219.2 Q 457.6 188 415.2 180 L 413.6 179.2 L 413.6 179.2 Q 403.2 178.4 394.4 184 Q 386.4 189.6 384.8 200.8 Q 383.2 211.2 388.8 220 Q 394.4 228 405.6 230.4 L 407.2 230.4 L 407.2 230.4 Q 430.4 234.4 445.6 252 Q 460.8 269.6 460.8 293.6 Q 460 321.6 441.6 339.2 Q 424 357.6 396 358.4 L 269.6 358.4 L 269.6 358.4 Q 241.6 357.6 224 339.2 Q 205.6 321.6 204.8 293.6 Q 205.6 269.6 220 252 Q 235.2 234.4 258.4 230.4 L 260 230.4 L 260 230.4 Q 271.2 228 276.8 220 Q 282.4 211.2 281.6 200.8 Q 279.2 189.6 271.2 184 Q 262.4 178.4 252 179.2 L 250.4 180 L 250.4 180 Q 208 188 180.8 219.2 Q 154.4 250.4 153.6 293.6 Q 155.2 343.2 187.2 376 Q 220 408 269.6 409.6 L 396 409.6 L 396 409.6 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,6 +0,0 @@
<!-- link-horizontal icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 116 102.4 Q 66.4 104 33.6 136 L 33.6 136 L 33.6 136 Q 1.6 168.8 0 218.4 Q 0.8 261.6 27.2 292.8 Q 54.4 324 96.8 332 L 98.4 332.8 L 98.4 332.8 Q 108.8 333.6 117.6 328 Q 125.6 322.4 128 311.2 Q 128.8 300.8 123.2 292 Q 117.6 284 106.4 281.6 L 104.8 281.6 L 104.8 281.6 Q 81.6 277.6 66.4 260 Q 52 242.4 51.2 218.4 Q 52 190.4 70.4 172.8 Q 88 154.4 116 153.6 L 242.4 153.6 L 242.4 153.6 Q 270.4 154.4 288 172.8 Q 306.4 190.4 307.2 218.4 Q 306.4 242.4 292 260 Q 276.8 277.6 253.6 281.6 L 252 281.6 L 252 281.6 Q 240.8 284 235.2 292 Q 229.6 300.8 230.4 311.2 Q 232.8 322.4 240.8 328 Q 249.6 333.6 260 332 L 261.6 332 L 261.6 332 Q 304 324 331.2 292.8 Q 357.6 261.6 358.4 218.4 Q 356.8 168.8 324.8 136 Q 292 104 242.4 102.4 L 116 102.4 L 116 102.4 Z M 396 409.6 Q 445.6 408 478.4 376 L 478.4 376 L 478.4 376 Q 510.4 343.2 512 293.6 Q 511.2 250.4 484.8 219.2 Q 457.6 188 415.2 180 L 413.6 179.2 L 413.6 179.2 Q 403.2 178.4 394.4 184 Q 386.4 189.6 384.8 200.8 Q 383.2 211.2 388.8 220 Q 394.4 228 405.6 230.4 L 407.2 230.4 L 407.2 230.4 Q 430.4 234.4 445.6 252 Q 460.8 269.6 460.8 293.6 Q 460 321.6 441.6 339.2 Q 424 357.6 396 358.4 L 269.6 358.4 L 269.6 358.4 Q 241.6 357.6 224 339.2 Q 205.6 321.6 204.8 293.6 Q 205.6 269.6 220 252 Q 235.2 234.4 258.4 230.4 L 260 230.4 L 260 230.4 Q 271.2 228 276.8 220 Q 282.4 211.2 281.6 200.8 Q 279.2 189.6 271.2 184 Q 262.4 178.4 252 179.2 L 250.4 180 L 250.4 180 Q 208 188 180.8 219.2 Q 154.4 250.4 153.6 293.6 Q 155.2 343.2 187.2 376 Q 220 408 269.6 409.6 L 396 409.6 L 396 409.6 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- circle-check icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="circle-check.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6855469"
inkscape:cx="255.70336"
inkscape:cy="256"
inkscape:window-width="1978"
inkscape:window-height="1088"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 256 512 Q 326 511 384 478 L 384 478 L 384 478 Q 442 444 478 384 Q 512 323 512 256 Q 512 189 478 128 Q 442 68 384 34 Q 326 1 256 0 Q 186 1 128 34 Q 70 68 34 128 Q 0 189 0 256 Q 0 323 34 384 Q 70 444 128 478 Q 186 511 256 512 L 256 512 Z M 369 209 L 241 337 L 369 209 L 241 337 Q 224 351 207 337 L 143 273 L 143 273 Q 129 256 143 239 Q 160 225 177 239 L 224 286 L 224 286 L 335 175 L 335 175 Q 352 161 369 175 Q 383 192 369 209 L 369 209 Z"
id="path1"
style="fill:#008000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- circle-check icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="circle-check-white.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6855469"
inkscape:cx="255.70336"
inkscape:cy="256"
inkscape:window-width="1978"
inkscape:window-height="1088"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 256 512 Q 326 511 384 478 L 384 478 L 384 478 Q 442 444 478 384 Q 512 323 512 256 Q 512 189 478 128 Q 442 68 384 34 Q 326 1 256 0 Q 186 1 128 34 Q 70 68 34 128 Q 0 189 0 256 Q 0 323 34 384 Q 70 444 128 478 Q 186 511 256 512 L 256 512 Z M 369 209 L 241 337 L 369 209 L 241 337 Q 224 351 207 337 L 143 273 L 143 273 Q 129 256 143 239 Q 160 225 177 239 L 224 286 L 224 286 L 335 175 L 335 175 Q 352 161 369 175 Q 383 192 369 209 L 369 209 Z"
id="path1"
style="fill:#ffffff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- circle-xmark icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="circle-xmark-gray.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="2.3300781"
inkscape:cx="255.78542"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 256 512 Q 326 511 384 478 L 384 478 L 384 478 Q 442 444 478 384 Q 512 323 512 256 Q 512 189 478 128 Q 442 68 384 34 Q 326 1 256 0 Q 186 1 128 34 Q 70 68 34 128 Q 0 189 0 256 Q 0 323 34 384 Q 70 444 128 478 Q 186 511 256 512 L 256 512 Z M 175 175 Q 192 161 209 175 L 256 222 L 256 222 L 303 175 L 303 175 Q 320 161 337 175 Q 351 192 337 209 L 290 256 L 290 256 L 337 303 L 337 303 Q 351 320 337 337 Q 320 351 303 337 L 256 290 L 256 290 L 209 337 L 209 337 Q 192 351 175 337 Q 161 320 175 303 L 222 256 L 222 256 L 175 209 L 175 209 Q 161 192 175 175 L 175 175 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,6 +0,0 @@
<!-- pen-to-square icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 476.65483234714003 17.16765285996055 Q 458.47731755424064 0 436.2603550295858 0 L 436.2603550295858 0 L 436.2603550295858 0 Q 414.043392504931 0 395.86587771203153 17.16765285996055 L 365.57001972386587 47.463510848126234 L 365.57001972386587 47.463510848126234 L 464.53648915187375 146.42998027613413 L 464.53648915187375 146.42998027613413 L 494.8323471400395 116.13412228796844 L 494.8323471400395 116.13412228796844 Q 512 97.95660749506904 512 75.7396449704142 Q 512 53.52268244575937 494.8323471400395 35.34516765285996 L 476.65483234714003 17.16765285996055 L 476.65483234714003 17.16765285996055 Z M 173.69625246548324 239.33727810650888 Q 164.60749506903352 248.42603550295857 160.5680473372781 261.55424063116374 L 130.27218934911244 350.42209072978306 L 130.27218934911244 350.42209072978306 Q 126.23274161735701 364.560157790927 136.33136094674558 375.6686390532544 Q 147.439842209073 385.767258382643 161.57790927021696 381.7278106508876 L 251.45562130177515 351.43195266272187 L 251.45562130177515 351.43195266272187 Q 263.5739644970414 347.3925049309665 272.66272189349115 338.3037475345168 L 442.3195266272189 168.64694280078896 L 442.3195266272189 168.64694280078896 L 343.35305719921104 69.68047337278107 L 343.35305719921104 69.68047337278107 L 173.69625246548324 239.33727810650888 L 173.69625246548324 239.33727810650888 Z M 96.94674556213018 59.581854043392504 Q 55.54240631163708 60.59171597633136 28.276134122287967 87.85798816568047 L 28.276134122287967 87.85798816568047 L 28.276134122287967 87.85798816568047 Q 1.009861932938856 115.12426035502959 0 156.5285996055227 L 0 415.05325443786984 L 0 415.05325443786984 Q 1.009861932938856 456.4575936883629 28.276134122287967 483.72386587771206 Q 55.54240631163708 510.99013806706114 96.94674556213018 512 L 355.4714003944773 512 L 355.4714003944773 512 Q 396.8757396449704 510.99013806706114 424.14201183431953 483.72386587771206 Q 451.4082840236686 456.4575936883629 452.4181459566075 415.05325443786984 L 452.4181459566075 318.1065088757396 L 452.4181459566075 318.1065088757396 Q 452.4181459566075 303.9684418145957 443.3293885601578 294.879684418146 Q 434.2406311637081 285.79092702169623 420.1025641025641 285.79092702169623 Q 405.96449704142015 285.79092702169623 396.8757396449704 294.879684418146 Q 387.7869822485207 303.9684418145957 387.7869822485207 318.1065088757396 L 387.7869822485207 415.05325443786984 L 387.7869822485207 415.05325443786984 Q 387.7869822485207 429.1913214990138 378.698224852071 438.28007889546353 Q 369.6094674556213 447.3688362919132 355.4714003944773 447.3688362919132 L 96.94674556213018 447.3688362919132 L 96.94674556213018 447.3688362919132 Q 82.80867850098619 447.3688362919132 73.7199211045365 438.28007889546353 Q 64.63116370808679 429.1913214990138 64.63116370808679 415.05325443786984 L 64.63116370808679 156.5285996055227 L 64.63116370808679 156.5285996055227 Q 64.63116370808679 142.3905325443787 73.7199211045365 133.301775147929 Q 82.80867850098619 124.21301775147928 96.94674556213018 124.21301775147928 L 193.89349112426035 124.21301775147928 L 193.89349112426035 124.21301775147928 Q 208.03155818540435 124.21301775147928 217.12031558185404 115.12426035502959 Q 226.20907297830374 106.03550295857988 226.20907297830374 91.8974358974359 Q 226.20907297830374 77.75936883629191 217.12031558185404 68.6706114398422 Q 208.03155818540435 59.581854043392504 193.89349112426035 59.581854043392504 L 96.94674556213018 59.581854043392504 L 96.94674556213018 59.581854043392504 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- money-bill icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eur-bill-gray.svg"
xml:space="preserve"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1504296"
inkscape:cx="319.01127"
inkscape:cy="302.06108"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><g
id="g2"
style="fill:#808080;fill-opacity:1"><path
d="m 99.555556,128 c -0.592593,16 -6.222223,29.33333 -16.888889,40 v 0 0 c -10.666667,10.66667 -24,16.2963 -40,16.88889 v 142.22222 0 c 16,0.59259 29.333333,6.22222 40,16.88889 10.666666,10.66667 16.296296,24 16.888889,40 H 412.44444 v 0 c 0.5926,-16 6.22223,-29.33333 16.88889,-40 10.66667,-10.66667 24,-16.2963 40,-16.88889 v -142.22222 0 c -16,-0.59259 -29.33333,-6.22222 -40,-16.88889 -10.66666,-10.66667 -16.29629,-24 -16.88889,-40 z M 0,142.22222 c 0.59259259,-16 6.2222222,-29.33333 16.888889,-40 v 0 0 c 10.666667,-10.666666 24,-16.296296 40,-16.888889 H 455.11111 v 0 c 16,0.592593 29.33333,6.222223 40,16.888889 10.66667,10.66667 16.2963,24 16.88889,40 v 227.55556 0 c -0.59259,16 -6.22222,29.33333 -16.88889,40 -10.66667,10.66666 -24,16.29629 -40,16.88889 H 56.888889 v 0 c -16,-0.5926 -29.333333,-6.22223 -40,-16.88889 -10.6666668,-10.66667 -16.29629641,-24 -16.888889,-40 z"
id="path1"
sodipodi:nodetypes="cscsccssccssccsscccscsccssccssccsscc"
style="fill:#808080;fill-opacity:1" /><g
style="fill:#808080;fill-opacity:1"
id="g1"
transform="matrix(0.33256844,0,0,0.33256844,170.86248,170.86248)"><path
d="m 256,512 q 70,-1 128,-34 v 0 0 q 58,-34 94,-94 34,-61 34,-128 0,-67 -34,-128 Q 442,68 384,34 326,1 256,0 186,1 128,34 70,68 34,128 0,189 0,256 q 0,67 34,128 36,60 94,94 58,33 128,34 z M 128,272 h 16 -16 16 q -1,-8 -1,-15 0,-9 1,-17 h -16 v 0 q -15,-1 -16,-16 1,-15 16,-16 h 25 v 0 q 15,-36 46,-58 32,-21 73,-22 h 41 v 0 q 22,2 24,24 -2,22 -24,24 h -41 v 0 q -41,1 -64,32 h 80 v 0 q 15,1 16,16 -1,15 -16,16 h -95 v 0 q -2,8 -2,17 0,8 2,15 h 95 v 0 q 15,1 16,16 -1,15 -16,16 h -82 v 0 q 24,32 66,34 h 41 v 0 q 22,2 24,24 -2,21 -24,24 h -41 v 0 q -42,-1 -73,-23 -32,-23 -47,-59 h -24 v 0 q -15,-1 -16,-16 1,-15 16,-16 z"
id="path1-5"
style="fill:#808080;fill-opacity:1" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- money-bill icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eur-bill-lasecagold.svg"
xml:space="preserve"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="268.60024"
inkscape:cy="228.34094"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><g
id="g2"
style="fill:#e1c300;fill-opacity:1"><path
d="m 99.555556,128 c -0.592593,16 -6.222223,29.33333 -16.888889,40 v 0 0 c -10.666667,10.66667 -24,16.2963 -40,16.88889 v 142.22222 0 c 16,0.59259 29.333333,6.22222 40,16.88889 10.666666,10.66667 16.296296,24 16.888889,40 H 412.44444 v 0 c 0.5926,-16 6.22223,-29.33333 16.88889,-40 10.66667,-10.66667 24,-16.2963 40,-16.88889 v -142.22222 0 c -16,-0.59259 -29.33333,-6.22222 -40,-16.88889 -10.66666,-10.66667 -16.29629,-24 -16.88889,-40 z M 0,142.22222 c 0.59259259,-16 6.2222222,-29.33333 16.888889,-40 v 0 0 c 10.666667,-10.666666 24,-16.296296 40,-16.888889 H 455.11111 v 0 c 16,0.592593 29.33333,6.222223 40,16.888889 10.66667,10.66667 16.2963,24 16.88889,40 v 227.55556 0 c -0.59259,16 -6.22222,29.33333 -16.88889,40 -10.66667,10.66666 -24,16.29629 -40,16.88889 H 56.888889 v 0 c -16,-0.5926 -29.333333,-6.22223 -40,-16.88889 -10.6666668,-10.66667 -16.29629641,-24 -16.888889,-40 z"
id="path1"
sodipodi:nodetypes="cscsccssccssccsscccscsccssccssccsscc"
style="fill:#e1c300;fill-opacity:1" /><g
style="fill:#e1c300;fill-opacity:1"
id="g1"
transform="matrix(0.33256844,0,0,0.33256844,170.86248,170.86248)"><path
d="m 256,512 q 70,-1 128,-34 v 0 0 q 58,-34 94,-94 34,-61 34,-128 0,-67 -34,-128 Q 442,68 384,34 326,1 256,0 186,1 128,34 70,68 34,128 0,189 0,256 q 0,67 34,128 36,60 94,94 58,33 128,34 z M 128,272 h 16 -16 16 q -1,-8 -1,-15 0,-9 1,-17 h -16 v 0 q -15,-1 -16,-16 1,-15 16,-16 h 25 v 0 q 15,-36 46,-58 32,-21 73,-22 h 41 v 0 q 22,2 24,24 -2,22 -24,24 h -41 v 0 q -41,1 -64,32 h 80 v 0 q 15,1 16,16 -1,15 -16,16 h -95 v 0 q -2,8 -2,17 0,8 2,15 h 95 v 0 q 15,1 16,16 -1,15 -16,16 h -82 v 0 q 24,32 66,34 h 41 v 0 q 22,2 24,24 -2,21 -24,24 h -41 v 0 q -42,-1 -73,-23 -32,-23 -47,-59 h -24 v 0 q -15,-1 -16,-16 1,-15 16,-16 z"
id="path1-5"
style="fill:#e1c300;fill-opacity:1" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- money-bill icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eur-bill.svg"
xml:space="preserve"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="268.60024"
inkscape:cy="228.34094"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><g
id="g2"><path
d="m 99.555556,128 c -0.592593,16 -6.222223,29.33333 -16.888889,40 v 0 0 c -10.666667,10.66667 -24,16.2963 -40,16.88889 v 142.22222 0 c 16,0.59259 29.333333,6.22222 40,16.88889 10.666666,10.66667 16.296296,24 16.888889,40 H 412.44444 v 0 c 0.5926,-16 6.22223,-29.33333 16.88889,-40 10.66667,-10.66667 24,-16.2963 40,-16.88889 v -142.22222 0 c -16,-0.59259 -29.33333,-6.22222 -40,-16.88889 -10.66666,-10.66667 -16.29629,-24 -16.88889,-40 z M 0,142.22222 c 0.59259259,-16 6.2222222,-29.33333 16.888889,-40 v 0 0 c 10.666667,-10.666666 24,-16.296296 40,-16.888889 H 455.11111 v 0 c 16,0.592593 29.33333,6.222223 40,16.888889 10.66667,10.66667 16.2963,24 16.88889,40 v 227.55556 0 c -0.59259,16 -6.22222,29.33333 -16.88889,40 -10.66667,10.66666 -24,16.29629 -40,16.88889 H 56.888889 v 0 c -16,-0.5926 -29.333333,-6.22223 -40,-16.88889 -10.6666668,-10.66667 -16.29629641,-24 -16.888889,-40 z"
id="path1"
sodipodi:nodetypes="cscsccssccssccsscccscsccssccssccsscc"
style="fill:#000000" /><g
style="fill:#000000"
id="g1"
transform="matrix(0.33256844,0,0,0.33256844,170.86248,170.86248)"><path
d="m 256,512 q 70,-1 128,-34 v 0 0 q 58,-34 94,-94 34,-61 34,-128 0,-67 -34,-128 Q 442,68 384,34 326,1 256,0 186,1 128,34 70,68 34,128 0,189 0,256 q 0,67 34,128 36,60 94,94 58,33 128,34 z M 128,272 h 16 -16 16 q -1,-8 -1,-15 0,-9 1,-17 h -16 v 0 q -15,-1 -16,-16 1,-15 16,-16 h 25 v 0 q 15,-36 46,-58 32,-21 73,-22 h 41 v 0 q 22,2 24,24 -2,22 -24,24 h -41 v 0 q -41,1 -64,32 h 80 v 0 q 15,1 16,16 -1,15 -16,16 h -95 v 0 q -2,8 -2,17 0,8 2,15 h 95 v 0 q 15,1 16,16 -1,15 -16,16 h -82 v 0 q 24,32 66,34 h 41 v 0 q 22,2 24,24 -2,21 -24,24 h -41 v 0 q -42,-1 -73,-23 -32,-23 -47,-59 h -24 v 0 q -15,-1 -16,-16 1,-15 16,-16 z"
id="path1-5"
style="fill:#000000" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="59.403683mm"
height="11.539121mm"
viewBox="0 0 59.403683 11.539121"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(111.82987,-65.432599)"><rect
style="fill:#ffffff;stroke:none;stroke-width:0.91734;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none"
id="rect58-1"
width="86.405785"
height="86.405769"
x="-125.3309"
y="-3.5853474" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.5542px;line-height:1.35;font-family:Bocklin;-inkscape-font-specification:Bocklin;text-align:center;letter-spacing:-1.29658px;text-anchor:middle;fill:#d4aa00;stroke-width:0.0731426"
x="-81.934418"
y="76.533325"
id="text1-6"><tspan
id="tspan1-0"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.5542px;line-height:1.35;font-family:Bocklin;-inkscape-font-specification:Bocklin;letter-spacing:-1.29658px;fill:#d4aa00;stroke-width:0.0731426"
x="-82.58271"
y="76.533325">LASECA</tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- users icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="many-users-gray.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 115.2 179.2 Q 151.2 178.4 170.4 147.2 Q 188 115.2 170.4 83.2 Q 151.2 52 115.2 51.2 Q 79.2 52 60 83.2 Q 42.4 115.2 60 147.2 Q 79.2 178.4 115.2 179.2 L 115.2 179.2 Z M 409.6 179.2 Q 445.6 178.4 464.8 147.2 Q 482.4 115.2 464.8 83.2 Q 445.6 52 409.6 51.2 Q 373.6 52 354.4 83.2 Q 336.8 115.2 354.4 147.2 Q 373.6 178.4 409.6 179.2 L 409.6 179.2 Z M 0 290.4 Q 1.6 305.6 16.8 307.2 L 188 307.2 L 188 307.2 Q 188 307.2 188 307.2 Q 188 307.2 188 307.2 Q 155.2 277.6 153.6 230.4 Q 153.6 221.6 155.2 212.8 Q 138.4 204.8 119.2 204.8 L 85.6 204.8 L 85.6 204.8 Q 48.8 205.6 24.8 229.6 Q 0.8 253.6 0 290.4 L 0 290.4 Z M 324 307.2 L 495.2 307.2 L 324 307.2 L 495.2 307.2 Q 510.4 305.6 512 290.4 Q 511.2 253.6 487.2 229.6 Q 463.2 205.6 426.4 204.8 L 392.8 204.8 L 392.8 204.8 Q 373.6 204.8 356.8 212.8 Q 358.4 221.6 358.4 230.4 Q 356.8 277.6 324 307.2 Q 324 307.2 324 307.2 Q 324 307.2 324 307.2 L 324 307.2 Z M 256 192 Q 277.6 192.8 289.6 211.2 Q 299.2 230.4 289.6 249.6 Q 277.6 268 256 268.8 Q 234.4 268 222.4 249.6 Q 212.8 230.4 222.4 211.2 Q 234.4 192.8 256 192 L 256 192 Z M 256 307.2 Q 276.8 307.2 294.4 296.8 L 294.4 296.8 L 294.4 296.8 Q 312 286.4 322.4 268.8 Q 332.8 250.4 332.8 230.4 Q 332.8 210.4 322.4 192 Q 312 174.4 294.4 164 Q 276.8 153.6 256 153.6 Q 235.2 153.6 217.6 164 Q 200 174.4 189.6 192 Q 179.2 210.4 179.2 230.4 Q 179.2 250.4 189.6 268.8 Q 200 286.4 217.6 296.8 Q 235.2 307.2 256 307.2 L 256 307.2 Z M 208.8 371.2 L 303.2 371.2 L 208.8 371.2 L 303.2 371.2 Q 327.2 372 344.8 385.6 Q 363.2 400 368.8 422.4 L 143.2 422.4 L 143.2 422.4 Q 148.8 400 167.2 385.6 Q 184.8 372 208.8 371.2 L 208.8 371.2 Z M 208.8 332.8 Q 164 333.6 133.6 364 L 133.6 364 L 133.6 364 Q 103.2 394.4 102.4 439.2 Q 104 459.2 124 460.8 L 388 460.8 L 388 460.8 Q 408 459.2 409.6 439.2 Q 408.8 394.4 378.4 364 Q 348 333.6 303.2 332.8 L 208.8 332.8 L 208.8 332.8 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- users icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="many-users-lasecagold.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 115.2 179.2 Q 151.2 178.4 170.4 147.2 Q 188 115.2 170.4 83.2 Q 151.2 52 115.2 51.2 Q 79.2 52 60 83.2 Q 42.4 115.2 60 147.2 Q 79.2 178.4 115.2 179.2 L 115.2 179.2 Z M 409.6 179.2 Q 445.6 178.4 464.8 147.2 Q 482.4 115.2 464.8 83.2 Q 445.6 52 409.6 51.2 Q 373.6 52 354.4 83.2 Q 336.8 115.2 354.4 147.2 Q 373.6 178.4 409.6 179.2 L 409.6 179.2 Z M 0 290.4 Q 1.6 305.6 16.8 307.2 L 188 307.2 L 188 307.2 Q 188 307.2 188 307.2 Q 188 307.2 188 307.2 Q 155.2 277.6 153.6 230.4 Q 153.6 221.6 155.2 212.8 Q 138.4 204.8 119.2 204.8 L 85.6 204.8 L 85.6 204.8 Q 48.8 205.6 24.8 229.6 Q 0.8 253.6 0 290.4 L 0 290.4 Z M 324 307.2 L 495.2 307.2 L 324 307.2 L 495.2 307.2 Q 510.4 305.6 512 290.4 Q 511.2 253.6 487.2 229.6 Q 463.2 205.6 426.4 204.8 L 392.8 204.8 L 392.8 204.8 Q 373.6 204.8 356.8 212.8 Q 358.4 221.6 358.4 230.4 Q 356.8 277.6 324 307.2 Q 324 307.2 324 307.2 Q 324 307.2 324 307.2 L 324 307.2 Z M 256 192 Q 277.6 192.8 289.6 211.2 Q 299.2 230.4 289.6 249.6 Q 277.6 268 256 268.8 Q 234.4 268 222.4 249.6 Q 212.8 230.4 222.4 211.2 Q 234.4 192.8 256 192 L 256 192 Z M 256 307.2 Q 276.8 307.2 294.4 296.8 L 294.4 296.8 L 294.4 296.8 Q 312 286.4 322.4 268.8 Q 332.8 250.4 332.8 230.4 Q 332.8 210.4 322.4 192 Q 312 174.4 294.4 164 Q 276.8 153.6 256 153.6 Q 235.2 153.6 217.6 164 Q 200 174.4 189.6 192 Q 179.2 210.4 179.2 230.4 Q 179.2 250.4 189.6 268.8 Q 200 286.4 217.6 296.8 Q 235.2 307.2 256 307.2 L 256 307.2 Z M 208.8 371.2 L 303.2 371.2 L 208.8 371.2 L 303.2 371.2 Q 327.2 372 344.8 385.6 Q 363.2 400 368.8 422.4 L 143.2 422.4 L 143.2 422.4 Q 148.8 400 167.2 385.6 Q 184.8 372 208.8 371.2 L 208.8 371.2 Z M 208.8 332.8 Q 164 333.6 133.6 364 L 133.6 364 L 133.6 364 Q 103.2 394.4 102.4 439.2 Q 104 459.2 124 460.8 L 388 460.8 L 388 460.8 Q 408 459.2 409.6 439.2 Q 408.8 394.4 378.4 364 Q 348 333.6 303.2 332.8 L 208.8 332.8 L 208.8 332.8 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,6 +0,0 @@
<!-- users icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 115.2 179.2 Q 151.2 178.4 170.4 147.2 Q 188 115.2 170.4 83.2 Q 151.2 52 115.2 51.2 Q 79.2 52 60 83.2 Q 42.4 115.2 60 147.2 Q 79.2 178.4 115.2 179.2 L 115.2 179.2 Z M 409.6 179.2 Q 445.6 178.4 464.8 147.2 Q 482.4 115.2 464.8 83.2 Q 445.6 52 409.6 51.2 Q 373.6 52 354.4 83.2 Q 336.8 115.2 354.4 147.2 Q 373.6 178.4 409.6 179.2 L 409.6 179.2 Z M 0 290.4 Q 1.6 305.6 16.8 307.2 L 188 307.2 L 188 307.2 Q 188 307.2 188 307.2 Q 188 307.2 188 307.2 Q 155.2 277.6 153.6 230.4 Q 153.6 221.6 155.2 212.8 Q 138.4 204.8 119.2 204.8 L 85.6 204.8 L 85.6 204.8 Q 48.8 205.6 24.8 229.6 Q 0.8 253.6 0 290.4 L 0 290.4 Z M 324 307.2 L 495.2 307.2 L 324 307.2 L 495.2 307.2 Q 510.4 305.6 512 290.4 Q 511.2 253.6 487.2 229.6 Q 463.2 205.6 426.4 204.8 L 392.8 204.8 L 392.8 204.8 Q 373.6 204.8 356.8 212.8 Q 358.4 221.6 358.4 230.4 Q 356.8 277.6 324 307.2 Q 324 307.2 324 307.2 Q 324 307.2 324 307.2 L 324 307.2 Z M 256 192 Q 277.6 192.8 289.6 211.2 Q 299.2 230.4 289.6 249.6 Q 277.6 268 256 268.8 Q 234.4 268 222.4 249.6 Q 212.8 230.4 222.4 211.2 Q 234.4 192.8 256 192 L 256 192 Z M 256 307.2 Q 276.8 307.2 294.4 296.8 L 294.4 296.8 L 294.4 296.8 Q 312 286.4 322.4 268.8 Q 332.8 250.4 332.8 230.4 Q 332.8 210.4 322.4 192 Q 312 174.4 294.4 164 Q 276.8 153.6 256 153.6 Q 235.2 153.6 217.6 164 Q 200 174.4 189.6 192 Q 179.2 210.4 179.2 230.4 Q 179.2 250.4 189.6 268.8 Q 200 286.4 217.6 296.8 Q 235.2 307.2 256 307.2 L 256 307.2 Z M 208.8 371.2 L 303.2 371.2 L 208.8 371.2 L 303.2 371.2 Q 327.2 372 344.8 385.6 Q 363.2 400 368.8 422.4 L 143.2 422.4 L 143.2 422.4 Q 148.8 400 167.2 385.6 Q 184.8 372 208.8 371.2 L 208.8 371.2 Z M 208.8 332.8 Q 164 333.6 133.6 364 L 133.6 364 L 133.6 364 Q 103.2 394.4 102.4 439.2 Q 104 459.2 124 460.8 L 388 460.8 L 388 460.8 Q 408 459.2 409.6 439.2 Q 408.8 394.4 378.4 364 Q 348 333.6 303.2 332.8 L 208.8 332.8 L 208.8 332.8 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- trash-can icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="trash-can-darkred.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6855469"
inkscape:cx="255.70336"
inkscape:cy="256"
inkscape:window-width="1978"
inkscape:window-height="1088"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 167 18 Q 176 1 196 0 L 316 0 L 316 0 Q 336 1 345 18 L 352 32 L 352 32 L 448 32 L 448 32 Q 462 32 471 41 Q 480 50 480 64 Q 480 78 471 87 Q 462 96 448 96 L 64 96 L 64 96 Q 50 96 41 87 Q 32 78 32 64 Q 32 50 41 41 Q 50 32 64 32 L 160 32 L 160 32 L 167 18 L 167 18 Z M 64 128 L 448 128 L 64 128 L 448 128 L 448 448 L 448 448 Q 447 475 429 493 Q 411 511 384 512 L 128 512 L 128 512 Q 101 511 83 493 Q 65 475 64 448 L 64 128 L 64 128 Z M 160 192 Q 145 193 144 208 L 144 432 L 144 432 Q 145 447 160 448 Q 175 447 176 432 L 176 208 L 176 208 Q 175 193 160 192 L 160 192 Z M 256 192 Q 241 193 240 208 L 240 432 L 240 432 Q 241 447 256 448 Q 271 447 272 432 L 272 208 L 272 208 Q 271 193 256 192 L 256 192 Z M 352 192 Q 337 193 336 208 L 336 432 L 336 432 Q 337 447 352 448 Q 367 447 368 432 L 368 208 L 368 208 Q 367 193 352 192 L 352 192 Z"
id="path1"
style="fill:#800000;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- trash-can icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="trash-can-lasecagold.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6855469"
inkscape:cx="255.70336"
inkscape:cy="256"
inkscape:window-width="1978"
inkscape:window-height="1088"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 167 18 Q 176 1 196 0 L 316 0 L 316 0 Q 336 1 345 18 L 352 32 L 352 32 L 448 32 L 448 32 Q 462 32 471 41 Q 480 50 480 64 Q 480 78 471 87 Q 462 96 448 96 L 64 96 L 64 96 Q 50 96 41 87 Q 32 78 32 64 Q 32 50 41 41 Q 50 32 64 32 L 160 32 L 160 32 L 167 18 L 167 18 Z M 64 128 L 448 128 L 64 128 L 448 128 L 448 448 L 448 448 Q 447 475 429 493 Q 411 511 384 512 L 128 512 L 128 512 Q 101 511 83 493 Q 65 475 64 448 L 64 128 L 64 128 Z M 160 192 Q 145 193 144 208 L 144 432 L 144 432 Q 145 447 160 448 Q 175 447 176 432 L 176 208 L 176 208 Q 175 193 160 192 L 160 192 Z M 256 192 Q 241 193 240 208 L 240 432 L 240 432 Q 241 447 256 448 Q 271 447 272 432 L 272 208 L 272 208 Q 271 193 256 192 L 256 192 Z M 352 192 Q 337 193 336 208 L 336 432 L 336 432 Q 337 447 352 448 Q 367 447 368 432 L 368 208 L 368 208 Q 367 193 352 192 L 352 192 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,6 +0,0 @@
<!-- trash-can icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 167 18 Q 176 1 196 0 L 316 0 L 316 0 Q 336 1 345 18 L 352 32 L 352 32 L 448 32 L 448 32 Q 462 32 471 41 Q 480 50 480 64 Q 480 78 471 87 Q 462 96 448 96 L 64 96 L 64 96 Q 50 96 41 87 Q 32 78 32 64 Q 32 50 41 41 Q 50 32 64 32 L 160 32 L 160 32 L 167 18 L 167 18 Z M 64 128 L 448 128 L 64 128 L 448 128 L 448 448 L 448 448 Q 447 475 429 493 Q 411 511 384 512 L 128 512 L 128 512 Q 101 511 83 493 Q 65 475 64 448 L 64 128 L 64 128 Z M 160 192 Q 145 193 144 208 L 144 432 L 144 432 Q 145 447 160 448 Q 175 447 176 432 L 176 208 L 176 208 Q 175 193 160 192 L 160 192 Z M 256 192 Q 241 193 240 208 L 240 432 L 240 432 Q 241 447 256 448 Q 271 447 272 432 L 272 208 L 272 208 Q 271 193 256 192 L 256 192 Z M 352 192 Q 337 193 336 208 L 336 432 L 336 432 Q 337 447 352 448 Q 367 447 368 432 L 368 208 L 368 208 Q 367 193 352 192 L 352 192 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- user icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="user-gray.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 256 256 Q 291 256 320 239 L 320 239 L 320 239 Q 349 222 367 192 Q 384 162 384 128 Q 384 94 367 64 Q 349 34 320 17 Q 291 0 256 0 Q 221 0 192 17 Q 163 34 145 64 Q 128 94 128 128 Q 128 162 145 192 Q 163 222 192 239 Q 221 256 256 256 L 256 256 Z M 210 304 Q 135 306 84 356 L 84 356 L 84 356 Q 34 407 32 482 Q 32 495 41 503 Q 49 512 62 512 L 450 512 L 450 512 Q 463 512 471 503 Q 480 495 480 482 Q 478 407 428 356 Q 377 306 302 304 L 210 304 L 210 304 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- user-group icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="user-group-gray.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 179.2 89.6 Q 215.2 90.4 234.4 121.6 Q 252 153.6 234.4 185.6 Q 215.2 216.8 179.2 217.6 Q 143.2 216.8 124 185.6 Q 106.4 153.6 124 121.6 Q 143.2 90.4 179.2 89.6 L 179.2 89.6 Z M 179.2 256 Q 207.2 256 230.4 242.4 L 230.4 242.4 L 230.4 242.4 Q 253.6 228.8 268 204.8 Q 281.6 180.8 281.6 153.6 Q 281.6 126.4 268 102.4 Q 253.6 78.4 230.4 64.8 Q 207.2 51.2 179.2 51.2 Q 151.2 51.2 128 64.8 Q 104.8 78.4 90.4 102.4 Q 76.8 126.4 76.8 153.6 Q 76.8 180.8 90.4 204.8 Q 104.8 228.8 128 242.4 Q 151.2 256 179.2 256 L 179.2 256 Z M 142.4 332.8 L 216 332.8 L 142.4 332.8 L 216 332.8 Q 256 333.6 284.8 358.4 Q 312.8 383.2 319.2 422.4 L 39.2 422.4 L 39.2 422.4 Q 45.6 383.2 73.6 358.4 Q 102.4 333.6 142.4 332.8 L 142.4 332.8 Z M 142.4 294.4 Q 82.4 296 41.6 336 L 41.6 336 L 41.6 336 Q 1.6 376.8 0 436.8 Q 0 447.2 7.2 453.6 Q 13.6 460.8 24 460.8 L 334.4 460.8 L 334.4 460.8 Q 344.8 460.8 351.2 453.6 Q 358.4 447.2 358.4 436.8 Q 356.8 376.8 316.8 336 Q 276 296 216 294.4 L 142.4 294.4 L 142.4 294.4 Z M 487.2 460.8 Q 497.6 460.8 504.8 453.6 L 504.8 453.6 L 504.8 453.6 Q 512 446.4 512 436 Q 510.4 381.6 474.4 344.8 Q 437.6 308.8 383.2 307.2 L 333.6 307.2 L 333.6 307.2 Q 328.8 307.2 323.2 308 Q 351.2 331.2 367.2 364 Q 384 397.6 384 436.8 Q 384 450.4 377.6 460.8 L 487.2 460.8 L 487.2 460.8 Z M 345.6 256 Q 384 255.2 408.8 229.6 L 408.8 229.6 L 408.8 229.6 Q 434.4 204.8 435.2 166.4 Q 434.4 128 408.8 103.2 Q 384 77.6 345.6 76.8 Q 315.2 77.6 292.8 94.4 Q 307.2 120.8 307.2 153.6 Q 306.4 196.8 282.4 229.6 Q 307.2 255.2 345.6 256 L 345.6 256 Z"
id="path1"
style="fill:#808080;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- user-group icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="user-group-lasecagold.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 179.2 89.6 Q 215.2 90.4 234.4 121.6 Q 252 153.6 234.4 185.6 Q 215.2 216.8 179.2 217.6 Q 143.2 216.8 124 185.6 Q 106.4 153.6 124 121.6 Q 143.2 90.4 179.2 89.6 L 179.2 89.6 Z M 179.2 256 Q 207.2 256 230.4 242.4 L 230.4 242.4 L 230.4 242.4 Q 253.6 228.8 268 204.8 Q 281.6 180.8 281.6 153.6 Q 281.6 126.4 268 102.4 Q 253.6 78.4 230.4 64.8 Q 207.2 51.2 179.2 51.2 Q 151.2 51.2 128 64.8 Q 104.8 78.4 90.4 102.4 Q 76.8 126.4 76.8 153.6 Q 76.8 180.8 90.4 204.8 Q 104.8 228.8 128 242.4 Q 151.2 256 179.2 256 L 179.2 256 Z M 142.4 332.8 L 216 332.8 L 142.4 332.8 L 216 332.8 Q 256 333.6 284.8 358.4 Q 312.8 383.2 319.2 422.4 L 39.2 422.4 L 39.2 422.4 Q 45.6 383.2 73.6 358.4 Q 102.4 333.6 142.4 332.8 L 142.4 332.8 Z M 142.4 294.4 Q 82.4 296 41.6 336 L 41.6 336 L 41.6 336 Q 1.6 376.8 0 436.8 Q 0 447.2 7.2 453.6 Q 13.6 460.8 24 460.8 L 334.4 460.8 L 334.4 460.8 Q 344.8 460.8 351.2 453.6 Q 358.4 447.2 358.4 436.8 Q 356.8 376.8 316.8 336 Q 276 296 216 294.4 L 142.4 294.4 L 142.4 294.4 Z M 487.2 460.8 Q 497.6 460.8 504.8 453.6 L 504.8 453.6 L 504.8 453.6 Q 512 446.4 512 436 Q 510.4 381.6 474.4 344.8 Q 437.6 308.8 383.2 307.2 L 333.6 307.2 L 333.6 307.2 Q 328.8 307.2 323.2 308 Q 351.2 331.2 367.2 364 Q 384 397.6 384 436.8 Q 384 450.4 377.6 460.8 L 487.2 460.8 L 487.2 460.8 Z M 345.6 256 Q 384 255.2 408.8 229.6 L 408.8 229.6 L 408.8 229.6 Q 434.4 204.8 435.2 166.4 Q 434.4 128 408.8 103.2 Q 384 77.6 345.6 76.8 Q 315.2 77.6 292.8 94.4 Q 307.2 120.8 307.2 153.6 Q 306.4 196.8 282.4 229.6 Q 307.2 255.2 345.6 256 L 345.6 256 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,6 +0,0 @@
<!-- user-group icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 179.2 89.6 Q 215.2 90.4 234.4 121.6 Q 252 153.6 234.4 185.6 Q 215.2 216.8 179.2 217.6 Q 143.2 216.8 124 185.6 Q 106.4 153.6 124 121.6 Q 143.2 90.4 179.2 89.6 L 179.2 89.6 Z M 179.2 256 Q 207.2 256 230.4 242.4 L 230.4 242.4 L 230.4 242.4 Q 253.6 228.8 268 204.8 Q 281.6 180.8 281.6 153.6 Q 281.6 126.4 268 102.4 Q 253.6 78.4 230.4 64.8 Q 207.2 51.2 179.2 51.2 Q 151.2 51.2 128 64.8 Q 104.8 78.4 90.4 102.4 Q 76.8 126.4 76.8 153.6 Q 76.8 180.8 90.4 204.8 Q 104.8 228.8 128 242.4 Q 151.2 256 179.2 256 L 179.2 256 Z M 142.4 332.8 L 216 332.8 L 142.4 332.8 L 216 332.8 Q 256 333.6 284.8 358.4 Q 312.8 383.2 319.2 422.4 L 39.2 422.4 L 39.2 422.4 Q 45.6 383.2 73.6 358.4 Q 102.4 333.6 142.4 332.8 L 142.4 332.8 Z M 142.4 294.4 Q 82.4 296 41.6 336 L 41.6 336 L 41.6 336 Q 1.6 376.8 0 436.8 Q 0 447.2 7.2 453.6 Q 13.6 460.8 24 460.8 L 334.4 460.8 L 334.4 460.8 Q 344.8 460.8 351.2 453.6 Q 358.4 447.2 358.4 436.8 Q 356.8 376.8 316.8 336 Q 276 296 216 294.4 L 142.4 294.4 L 142.4 294.4 Z M 487.2 460.8 Q 497.6 460.8 504.8 453.6 L 504.8 453.6 L 504.8 453.6 Q 512 446.4 512 436 Q 510.4 381.6 474.4 344.8 Q 437.6 308.8 383.2 307.2 L 333.6 307.2 L 333.6 307.2 Q 328.8 307.2 323.2 308 Q 351.2 331.2 367.2 364 Q 384 397.6 384 436.8 Q 384 450.4 377.6 460.8 L 487.2 460.8 L 487.2 460.8 Z M 345.6 256 Q 384 255.2 408.8 229.6 L 408.8 229.6 L 408.8 229.6 Q 434.4 204.8 435.2 166.4 Q 434.4 128 408.8 103.2 Q 384 77.6 345.6 76.8 Q 315.2 77.6 292.8 94.4 Q 307.2 120.8 307.2 153.6 Q 306.4 196.8 282.4 229.6 Q 307.2 255.2 345.6 256 L 345.6 256 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- user icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg
height="1em"
fill="currentColor"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="user-lasecagold.svg"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6269531"
inkscape:cx="255.69268"
inkscape:cy="256"
inkscape:window-width="1850"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 256 256 Q 291 256 320 239 L 320 239 L 320 239 Q 349 222 367 192 Q 384 162 384 128 Q 384 94 367 64 Q 349 34 320 17 Q 291 0 256 0 Q 221 0 192 17 Q 163 34 145 64 Q 128 94 128 128 Q 128 162 145 192 Q 163 222 192 239 Q 221 256 256 256 L 256 256 Z M 210 304 Q 135 306 84 356 L 84 356 L 84 356 Q 34 407 32 482 Q 32 495 41 503 Q 49 512 62 512 L 450 512 L 450 512 Q 463 512 471 503 Q 480 495 480 482 Q 478 407 428 356 Q 377 306 302 304 L 210 304 L 210 304 Z"
id="path1"
style="fill:#e1c300;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,6 +0,0 @@
<!-- user icon by Free Icons (https://free-icons.github.io/free-icons/) -->
<svg xmlns="http://www.w3.org/2000/svg" height="1em" fill="currentColor" viewBox="0 0 512 512">
<path
d="M 256 256 Q 291 256 320 239 L 320 239 L 320 239 Q 349 222 367 192 Q 384 162 384 128 Q 384 94 367 64 Q 349 34 320 17 Q 291 0 256 0 Q 221 0 192 17 Q 163 34 145 64 Q 128 94 128 128 Q 128 162 145 192 Q 163 222 192 239 Q 221 256 256 256 L 256 256 Z M 210 304 Q 135 306 84 356 L 84 356 L 84 356 Q 34 407 32 482 Q 32 495 41 503 Q 49 512 62 512 L 450 512 L 450 512 Q 463 512 471 503 Q 480 495 480 482 Q 478 407 428 356 Q 377 306 302 304 L 210 304 L 210 304 Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 650 B

View file

@ -1,35 +1,29 @@
const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');
const { buildDependencies } = require('./dependencies');
function createApp(dependencies) {
const app = express();
const port = 3000;
const app = express();
const port = 3000;
app.set('port', port);
app.use(cookieParser());
app.use(cookieParser());
app.use(express.json());
app.use(express.json());
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
const createSessionMiddleware = require('./middlewares/sessionMiddleware');
app.use(dependencies.middlewares.createSessionMiddleware);
app.use(createSessionMiddleware);
app.use('/', dependencies.webRoutes);
app.use('/api', dependencies.apiRoutes);
const webRoutes = require('./routes/webRoutes');
const apiRoutes = require('./routes/apiRoutes');
app.use(express.static(path.join(__dirname, '../public')));
app.use('/', webRoutes);
app.use('/api', apiRoutes);
app.disable('etag'); //avoids 304 responses
app.use(express.static(path.join(__dirname, 'public')));
return app;
}
const dependencies = buildDependencies();
const app = createApp(dependencies);
app.listen(app.get('port'), () => {
console.log(`Server started on port ${app.get('port')}`);
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});

View file

@ -1,31 +1,13 @@
const { Command } = require('commander');
const { buildDependencies } = require('./dependencies');
const program = new Command();
function buildCLIDependencies() {
const appDependencies = buildDependencies();
const createAppInviteCommand = require('./commands/createAppInvite');
const CreateAppInviteProvider = require('./commands/createAppInvite');
const createAppInvite = new CreateAppInviteProvider({
invitesService: appDependencies.services.invitesService,
}).provide();
program.version('1.0.0').description('CLI for managing web app tasks');
return { createAppInviteCommand: createAppInvite };
}
program
.command('createAppInvite <inviterNpub>')
.description('Create an invite')
.action(createAppInviteCommand);
function buildCLI({ createAppInviteCommand }) {
const wipCli = new Command();
wipCli.version('1.0.0').description('CLI for managing web app tasks');
wipCli
.command('createAppInvite <inviterNpub>')
.description('Create an invite')
.action(createAppInviteCommand);
return wipCli;
}
const cliDependencies = buildCLIDependencies();
const cli = buildCLI(cliDependencies);
cli.parse(process.argv);
program.parse(process.argv);

View file

@ -1,17 +1,7 @@
class CreateAppInviteProvider {
constructor({ invitesService }) {
this.invitesService = invitesService;
}
const invitesService = require('../services/invitesService');
provide() {
const createAppInvite = async (inviterNpub) => {
const appInvite = await this.invitesService.createAppInvite(inviterNpub);
console.log('Invite created');
console.log(`Check at http://localhost/invite/${appInvite.uuid}`);
};
return createAppInvite;
}
}
module.exports = CreateAppInviteProvider;
module.exports = async function createAppInvite(inviterNpub) {
const appInvite = await invitesService.createAppInvite(inviterNpub);
console.log('Invite created');
console.log(`Check at http://localhost/invite/${appInvite.uuid}`);
};

View file

@ -1,15 +1,7 @@
const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;
const DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS = 60 * 60 * 24 * 30;
const DEFAULT_REDIRECT_DELAY = 3 * 1000; // 3seconds times milliseconds;
const API_PATHS = {
createProfile: '/createProfile',
home: '/home',
};
module.exports = {
DEFAULT_SESSION_DURATION_SECONDS,
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS,
API_PATHS,
DEFAULT_REDIRECT_DELAY,
};

View file

@ -10,11 +10,6 @@ const sequelize = new Sequelize({
database: process.env.POSTGRES_DB,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
/* logging: (msg) => {
if (msg && (msg.includes('ERROR') || msg.includes('error'))) {
console.error(msg);
}
}, */
define: {
timestamps: false,
freezeTableName: true,
@ -23,4 +18,13 @@ const sequelize = new Sequelize({
},
});
sequelize
.sync()
.then(() => {
console.log('Database synced');
})
.catch((err) => {
console.error('Error syncing the database:', err);
});
module.exports = sequelize;

View file

@ -1,36 +0,0 @@
class AssociationsDefiner {
constructor({ models, DataTypes }) {
this.models = models;
this.DataTypes = DataTypes;
}
define() {
this.models.NostrChallengeCreated.hasOne(
this.models.NostrChallengeCompleted,
{
foreignKey: 'challenge',
}
);
this.models.NostrChallengeCompleted.belongsTo(
this.models.NostrChallengeCreated,
{
foreignKey: {
name: 'challenge',
},
}
);
this.models.OfferCreated.hasOne(this.models.OfferDeleted, {
foreignKey: 'offer_uuid',
});
this.models.OfferDeleted.belongsTo(this.models.OfferCreated, {
foreignKey: {
name: 'offer_uuid',
type: this.DataTypes.UUID,
allowNull: false,
},
});
}
}
module.exports = AssociationsDefiner;

View file

@ -1,21 +0,0 @@
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
development: {
dialect: 'postgres',
host: process.env.POSTGRES_HOST,
port: 5432,
database: process.env.POSTGRES_DB,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
logging: console.log,
define: {
timestamps: false,
freezeTableName: true,
underscored: true,
quoteIdentifiers: false,
},
},
};

View file

@ -1,379 +0,0 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.createTable(
'app_invite_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
inviter_pub_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'contact_details_set',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
encrypted_contact_details: {
type: Sequelize.TEXT,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'nostr_challenge_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
expires_at: {
type: Sequelize.DATE,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'login_challenge_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'nostr_challenge_completed',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
signed_event: {
type: Sequelize.JSONB,
allowNull: false,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'login_challenge_completed',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'nym_set',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
nym: {
type: Sequelize.TEXT,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'offer_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'offer_deleted',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
offer_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'offer_details_set',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
offer_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
wants: {
type: Sequelize.STRING,
allowNull: false,
},
premium: {
type: Sequelize.DECIMAL(5, 2),
allowNull: false,
},
trade_amount_eur: {
type: Sequelize.INTEGER,
allowNull: false,
},
location_details: {
type: Sequelize.TEXT,
allowNull: false,
},
time_availability_details: {
type: Sequelize.TEXT,
allowNull: false,
},
show_offer_to_trusted: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
show_offer_to_trusted_trusted: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
show_offer_to_all_members: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
is_onchain_accepted: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
is_lightning_accepted: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
are_big_notes_accepted: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'session_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
expires_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'session_related_to_public_key',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
session_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'sign_up_challenge_created',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
app_invite_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
queryInterface.createTable(
'sign_up_challenge_completed',
{
uuid: {
type: Sequelize.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
app_invite_uuid: {
type: Sequelize.UUID,
allowNull: false,
},
public_key: {
type: Sequelize.STRING,
allowNull: false,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{ transaction: t }
),
]);
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
},
};

View file

@ -1,152 +0,0 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction((t) => {
return Promise.all([
queryInterface.addConstraint(
'login_challenge_created',
{
fields: ['nostr_challenge_uuid'],
type: 'foreign key',
references: {
table: 'nostr_challenge_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'nostr_challenge_completed',
{
fields: ['challenge'],
type: 'foreign key',
references: {
table: 'nostr_challenge_created',
field: 'challenge',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'login_challenge_completed',
{
fields: ['nostr_challenge_completed_uuid'],
type: 'foreign key',
references: {
table: 'nostr_challenge_completed',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'offer_deleted',
{
fields: ['offer_uuid'],
type: 'foreign key',
references: {
table: 'offer_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'offer_details_set',
{
fields: ['offer_uuid'],
type: 'foreign key',
references: {
table: 'offer_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'session_related_to_public_key',
{
fields: ['session_uuid'],
type: 'foreign key',
references: {
table: 'session_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'sign_up_challenge_created',
{
fields: ['nostr_challenge_uuid'],
type: 'foreign key',
references: {
table: 'nostr_challenge_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'sign_up_challenge_created',
{
fields: ['app_invite_uuid'],
type: 'foreign key',
references: {
table: 'app_invite_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'sign_up_challenge_completed',
{
fields: ['nostr_challenge_completed_uuid'],
type: 'foreign key',
references: {
table: 'nostr_challenge_completed',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
queryInterface.addConstraint(
'sign_up_challenge_completed',
{
fields: ['app_invite_uuid'],
type: 'foreign key',
references: {
table: 'app_invite_created',
field: 'uuid',
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
{ transaction: t }
),
]);
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
},
};

View file

@ -1,52 +0,0 @@
const express = require('express');
function buildDependencies() {
const dependencies = {};
const errors = require('./errors');
const constants = require('./constants');
const sequelize = require('./database/database');
const { DataTypes } = require('sequelize');
const ModelsProvider = require('./models');
const models = new ModelsProvider({ sequelize, DataTypes }).provide();
const AssociationsDefiner = require('./database/associations');
new AssociationsDefiner({ models, DataTypes }).define();
const ServicesProvider = require('./services');
const services = new ServicesProvider({
models,
constants,
errors,
sequelize,
}).provide();
dependencies.services = services;
const MiddlewaresProvider = require('./middlewares');
const middlewares = new MiddlewaresProvider({
constants,
sessionService: services.sessionService,
profileService: services.profileService,
}).provide();
dependencies.middlewares = middlewares;
const WebRoutesProvider = require('./routes/webRoutes');
const webRoutesProvider = new WebRoutesProvider({
express,
middlewares,
invitesService: services.invitesService,
});
dependencies.webRoutes = webRoutesProvider.provide();
const ApiRoutesProvider = require('./routes/apiRoutes');
const apiRoutesProvider = new ApiRoutesProvider({
express,
middlewares,
services,
errors,
});
dependencies.apiRoutes = apiRoutesProvider.provide();
return dependencies;
}
module.exports = { buildDependencies };

View file

@ -1,58 +0,0 @@
class BuyOrSellButtonGroup {
constructor({ parentElement, id }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.buyButton = null;
this.sellButton = null;
}
render() {
const groupDiv = document.createElement('div');
groupDiv.className = 'button-group';
groupDiv.id = this.id;
const buyButton = document.createElement('button');
buyButton.dataset.value = 'buy-bitcoin';
buyButton.id = 'button-buy-bitcoin';
buyButton.className = 'selected';
buyButton.textContent = 'Quiero comprar Bitcoin';
this.buyButton = buyButton;
const sellButton = document.createElement('button');
sellButton.dataset.value = 'sell-bitcoin';
sellButton.id = 'button-sell-bitcoin';
sellButton.textContent = 'Quiero vender Bitcoin';
this.sellButton = sellButton;
groupDiv.appendChild(this.buyButton);
groupDiv.appendChild(this.sellButton);
for (const button of [this.buyButton, this.sellButton]) {
button.addEventListener('click', () => {
[this.buyButton, this.sellButton].forEach((aButton) => {
if (aButton.classList.contains('selected')) {
aButton.classList.remove('selected');
} else {
aButton.classList.add('selected');
}
});
});
}
this.element = groupDiv;
this.parentElement.appendChild(this.element);
}
wants() {
if (this.buyButton.classList.contains('selected')) {
return 'BTC';
}
if (this.sellButton.classList.contains('selected')) {
return 'EUR';
}
}
}
module.exports = BuyOrSellButtonGroup;

View file

@ -1,25 +0,0 @@
class PopupNotification {
constructor({ parentElement, id, text }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.text = text;
}
render() {
const div = document.createElement('div');
div.id = this.id;
div.className = 'top-notification-good';
div.innerHTML = `<img src="/img/circle-check-white.svg" />
<p>${this.text}</p>`;
this.element = div;
this.parentElement.appendChild(div);
}
display() {
this.element.classList.add('revealed');
}
}
module.exports = PopupNotification;

View file

@ -1,68 +0,0 @@
class PremiumSelector {
constructor({ parentElement, id, eventSink }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.eventSink = eventSink;
this.premiumValue = null;
}
render() {
const premiumSelectorArea = document.createElement('div');
premiumSelectorArea.id = this.id;
const premiumValue = document.createElement('div');
premiumValue.id = 'premium-value';
premiumValue.textContent = '0%';
this.premiumValue = premiumValue;
const premiumButtonsContainer = document.createElement('div');
premiumButtonsContainer.id = 'premium-buttons-container';
const increaseButton = document.createElement('button');
increaseButton.classList.add('premium-button');
increaseButton.id = 'button-increase-premium';
increaseButton.textContent = '+';
const decreaseButton = document.createElement('button');
decreaseButton.classList.add('premium-button');
decreaseButton.id = 'button-decrease-premium';
decreaseButton.textContent = '-';
premiumButtonsContainer.appendChild(increaseButton);
premiumButtonsContainer.appendChild(decreaseButton);
premiumSelectorArea.appendChild(premiumValue);
premiumSelectorArea.appendChild(premiumButtonsContainer);
increaseButton.addEventListener('click', () => {
this.modifyPremiumValue(1);
});
decreaseButton.addEventListener('click', () => {
this.modifyPremiumValue(-1);
});
this.element = premiumSelectorArea;
this.parentElement.appendChild(this.element);
}
modifyPremiumValue(delta) {
const regexExpression = /-*\d+/;
const numValue = parseInt(
this.premiumValue.innerText.match(regexExpression)[0]
);
const newValue = `${numValue + delta}%`;
this.premiumValue.innerText = newValue;
this.eventSink.dispatchEvent(new Event('premium-changed'));
}
getPremium() {
return parseInt(this.premiumValue.textContent.match(/-?\d+/)[0]) / 100;
}
}
module.exports = PremiumSelector;

View file

@ -1,64 +0,0 @@
const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces');
class PriceDisplay {
constructor({
parentElement,
id,
premiumProvidingCallback,
priceProvidingCallback,
}) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.premiumProvidingCallback = premiumProvidingCallback;
this.priceProvidingCallback = priceProvidingCallback;
}
render() {
const container = document.createElement('div');
container.id = 'premium-price-display-area';
const offerParagraph = document.createElement('p');
offerParagraph.id = 'offer-price-paragraph';
offerParagraph.textContent = 'Tu precio: ';
const offerSpan = document.createElement('span');
offerSpan.id = 'offer-price';
this.offerPriceSpan = offerSpan;
offerParagraph.appendChild(offerSpan);
offerParagraph.append('€/BTC');
const marketParagraph = document.createElement('p');
marketParagraph.id = 'market-price-paragraph';
marketParagraph.textContent = '(Precio mercado: ';
const marketSpan = document.createElement('span');
marketSpan.id = 'market-price';
this.marketPriceSpan = marketSpan;
marketParagraph.appendChild(marketSpan);
marketParagraph.append('€/BTC)');
container.appendChild(offerParagraph);
container.appendChild(marketParagraph);
this.updatePrices();
this.element = container;
this.parentElement.appendChild(this.element);
}
updatePrices() {
const marketPrice = this.priceProvidingCallback();
const marketPriceString = formatNumberWithSpaces(marketPrice);
const offerPriceString = formatNumberWithSpaces(
Math.round(marketPrice * (1 + this.premiumProvidingCallback()))
);
this.marketPriceSpan.innerText = marketPriceString;
this.offerPriceSpan.innerText = offerPriceString;
}
}
module.exports = PriceDisplay;

View file

@ -1,21 +0,0 @@
class PublishOfferButton {
constructor({ parentElement, id, onClickCallback }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.onClickCallback = onClickCallback;
}
render() {
const button = document.createElement('button');
button.id = this.id;
button.className = 'button-primary button-large';
button.innerText = 'Publicar oferta';
button.addEventListener('click', this.onClickCallback);
this.element = button;
this.parentElement.appendChild(this.element);
}
}
module.exports = PublishOfferButton;

View file

@ -1,25 +0,0 @@
class WarningDiv {
constructor({ parentElement, id, innerHTML }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.innerHTML = innerHTML;
}
render() {
const div = document.createElement('div');
div.id = this.id;
div.className = 'card-secondary';
div.style.display = 'none';
div.innerHTML = this.innerHTML;
this.element = div;
this.parentElement.appendChild(div);
}
display() {
this.element.style.display = 'block';
}
}
module.exports = WarningDiv;

View file

@ -1,63 +0,0 @@
class NostrButton {
constructor({ parentElement, id, onClickCallback, buttonText }) {
this.element = null;
this.parentElement = parentElement;
this.id = id;
this.onClickCallback = onClickCallback;
this.buttonText = buttonText;
}
render() {
const thisButton = document.createElement('button');
thisButton.id = this.id;
thisButton.type = 'submit';
thisButton.className = 'button-large button-nostr';
const figure = document.createElement('figure');
const img = document.createElement('img');
img.src = '/img/white_ostrich.svg';
img.style.width = '40%';
img.style.margin = '-5% -5%';
figure.appendChild(img);
const paragraph = document.createElement('p');
paragraph.textContent = this.buttonText;
thisButton.appendChild(figure);
thisButton.appendChild(paragraph);
thisButton.addEventListener('click', () => {
this.onClickCallback();
});
this.element = thisButton;
this.parentElement.appendChild(this.element);
}
disable() {
if (this.element) {
this.element.disabled = true;
}
}
}
class NostrSignupButton extends NostrButton {
constructor({ parentElement, id, onClickCallback }) {
super({ parentElement, id, onClickCallback, buttonText: 'Alta con Nostr' });
}
}
class NostrLoginButton extends NostrButton {
constructor({ parentElement, id, onClickCallback }) {
super({
parentElement,
id,
onClickCallback,
buttonText: 'Login con Nostr',
});
}
}
module.exports = { NostrSignupButton, NostrLoginButton };

View file

@ -1,125 +0,0 @@
const createProfilesFunction = () => {
const createProfileConfirmation = document.querySelector(
'#create-profile-success'
);
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const validateNymInput = debounce(() => {
const nymValue = nymInput.value.trim();
const isValid = nymValue.length >= 3 && nymValue.length <= 128;
if (isValid) {
nymInputValidationWarning.style.display = 'none';
} else {
nymInputValidationWarning.style.display = 'block';
}
}, 500);
const checkIfSubmittable = debounce((allInputs) => {
const nymIsFilled = allInputs.nymInput.value !== '';
let atLeastOneContactIsFilled = false;
for (const contactInput of allInputs.contactInputs) {
if (contactInput.value !== '') {
atLeastOneContactIsFilled = true;
}
}
const buttonShouldBeDisabled = !(nymIsFilled && atLeastOneContactIsFilled);
submitProfileButton.disabled = buttonShouldBeDisabled;
}, 500);
async function createProfile(allInputs) {
const contactDetails = [];
for (const someInput of allInputs.contactInputs) {
contactDetails.push({
type: someInput.getAttribute('data-type'),
value: someInput.value,
});
}
const encryptedContactDetails = await window.nostr.nip04.encrypt(
await window.nostr.getPublicKey(),
JSON.stringify(contactDetails)
);
await fetch('/api/set-contact-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ encryptedContactDetails }),
});
const nym = allInputs.nymInput.value;
await fetch('/api/set-nym', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ nym }),
});
createProfileConfirmation.classList.add('revealed');
setTimeout(() => {
window.location.href = '/home';
}, 5000);
}
function onLoadErrands(allInputs, submitProfileButton) {
allInputs.nymInput.addEventListener('input', validateNymInput);
for (const someInput of allInputs.allInputs) {
someInput.addEventListener('input', () => {
checkIfSubmittable(allInputs);
});
}
checkIfSubmittable(allInputs);
submitProfileButton.addEventListener('click', () => {
createProfile(allInputs);
});
}
const nymInput = document.getElementById('nym-input');
const nymInputValidationWarning = document.getElementById(
'nym-input-validation-warning'
);
const phoneInput = document.getElementById('phone-input');
const whatsappInput = document.getElementById('whatsapp-input');
const telegramInput = document.getElementById('telegram-input');
const emailInput = document.getElementById('email-input');
const nostrInput = document.getElementById('nostr-input');
const signalInput = document.getElementById('signal-input');
const submitProfileButton = document.getElementById('submit-profile-button');
const allInputs = {
nymInput: nymInput,
contactInputs: [
phoneInput,
whatsappInput,
telegramInput,
emailInput,
nostrInput,
signalInput,
],
allInputs: [
nymInput,
phoneInput,
whatsappInput,
telegramInput,
emailInput,
nostrInput,
signalInput,
],
};
onLoadErrands(allInputs, submitProfileButton);
};
createProfilesFunction();

View file

@ -1,14 +0,0 @@
const homeFunction = () => {
const navbuttonHome = document.getElementById('navbutton-home');
const navbuttonOffers = document.getElementById('navbutton-offers');
navbuttonHome.addEventListener('click', () => {
window.location.href = '/home';
});
navbuttonOffers.addEventListener('click', () => {
window.location.href = '/offers';
});
};
homeFunction();

View file

@ -1,110 +0,0 @@
const checkNostrExtension = require('../utils/checkNostrExtension');
const inviteService = require('../services/inviteService');
const constants = require('../../constants');
const { NostrSignupButton } = require('../components/nostrButtons');
const WarningDiv = require('../components/WarningDiv');
const PopupNotification = require('../components/PopupNotification');
const invitesFunction = () => {
const body = document.querySelector('body');
const warningsContainer = document.getElementById('warnings-container');
const nostrSignupArea = document.getElementById('nostr-signup-area');
const signupButton = new NostrSignupButton({
parentElement: nostrSignupArea,
id: 'nostr-signup-button',
onClickCallback: async () => {
const verifyResponse =
await inviteService.requestAndRespondSignUpChallenge({
onNostrErrorCallback: () => {
rejectedNostrWarning.display();
},
});
if (verifyResponse.ok) {
signUpSuccessPopup.display();
setTimeout(() => {
window.location.href = constants.API_PATHS.createProfile;
}, constants.DEFAULT_REDIRECT_DELAY);
}
},
});
signupButton.render();
const noExtensionWarning = new WarningDiv({
parentElement: warningsContainer,
id: 'no-extension-nudges',
innerHTML: `<p>
¡Atención! No se ha encontrado una extensión de Nostr en tu
navegador. Puedes usar:
</p>
<p><strong>Firefox</strong></p>
<p>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</p>
<p>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
target="_blank"
rel="noopener noreferrer"
>nos2x-fox</a
>
</p>
<p><strong>Chrome</strong></p>
<p>
<a
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</p>
<p>
<a
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
target="_blank"
rel="noopener noreferrer"
>nos2x</a
>
</p>`,
});
noExtensionWarning.render();
const rejectedNostrWarning = new WarningDiv({
parentElement: warningsContainer,
id: 'rejected-nostr-nudges',
innerHTML: `<p>
Ups, parece que no has aceptado que usemos tus claves. Si te has
equivocado, puedes intentarlo de nuevo.
</p>`,
});
rejectedNostrWarning.render();
const signUpSuccessPopup = new PopupNotification({
parentElement: body,
id: 'sign-up-success',
text: '¡Bien! Hemos dado de alta tu clave de Nostr. Te vamos a redirigir a la seca, espera un momento.',
});
signUpSuccessPopup.render();
window.onload = () => {
checkNostrExtension({
window,
successCallback: () => {
console.log('Nostr extension present');
},
failureCallback: () => {
console.log('Nostr extension not present');
signupButton.disable();
noExtensionWarning.display();
},
});
};
};
invitesFunction();

View file

@ -1,127 +0,0 @@
const checkNostrExtension = require('../utils/checkNostrExtension');
const loginService = require('../services/loginService');
const WarningDiv = require('../components/WarningDiv');
const { NostrLoginButton } = require('../components/nostrButtons');
const PopupNotification = require('../components/PopupNotification');
const constants = require('../../constants');
const loginFunction = () => {
const loginButtonArea = document.getElementById('login-button-area');
const warningsArea = document.getElementById('warnings-area');
const successPopup = new PopupNotification({
parentElement: document.querySelector('body'),
id: 'login-success-popup',
text: '¡Éxito! Te estamos llevando a la app...',
});
successPopup.render();
const nostrLoginButton = new NostrLoginButton({
parentElement: loginButtonArea,
id: 'login-button',
onClickCallback: async () => {
const verifyResponse = await loginService.requestAndRespondLoginChallenge(
{
onRejectedPubKeyCallback: () => {
nostrRejectedWarning.display();
},
onRejectedSignatureCallback: () => {
nostrRejectedWarning.display();
},
}
);
if (verifyResponse.status === 403) {
notRegisteredPubkeyWarning.display();
}
if (verifyResponse.ok) {
nostrLoginButton.disable();
successPopup.display();
setTimeout(() => {
window.location.href = constants.API_PATHS.home;
}, constants.DEFAULT_REDIRECT_DELAY);
}
},
});
nostrLoginButton.render();
const notRegisteredPubkeyWarning = new WarningDiv({
parentElement: warningsArea,
id: 'rejected-public-key',
innerHTML: `<p>
Ups, esa clave no está registrada en la seca. ¿Quizás estás usando un
perfil equivocado?
</p>`,
});
notRegisteredPubkeyWarning.render();
const noExtensionWarning = new WarningDiv({
parentElement: warningsArea,
id: 'no-extension-nudges',
innerHTML: `<p>
¡Atención! No se ha encontrado una extensión de Nostr en tu navegador.
Puedes usar:
</p>
<p><strong>Firefox</strong></p>
<p>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</p>
<p>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
target="_blank"
rel="noopener noreferrer"
>nos2x-fox</a
>
</p>
<p><strong>Chrome</strong></p>
<p>
<a
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</p>
<p>
<a
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
target="_blank"
rel="noopener noreferrer"
>nos2x</a
>
</p>`,
});
noExtensionWarning.render();
const nostrRejectedWarning = new WarningDiv({
parentElement: warningsArea,
id: 'rejected-nostr-nudges',
innerHTML: `<p>
Ups, parece que no has aceptado que usemos tus claves. Si te has
equivocado, puedes intentarlo de nuevo.
</p>`,
});
nostrRejectedWarning.render();
window.onload = () => {
checkNostrExtension({
window,
successCallback: () => {
console.log('Nostr extension present');
},
failureCallback: () => {
console.log('Nostr extension not present');
nostrLoginButton.disable();
noExtensionWarning.display();
},
});
};
};
loginFunction();

View file

@ -1,694 +0,0 @@
const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces');
const PublishOfferButton = require('../components/PublishOfferButton');
const BuyOrSellButtonGroup = require('../components/BuyOrSellButtonGroup');
const PremiumSelector = require('../components/PremiumSelector');
const PriceDisplay = require('../components/PriceDisplay');
function offersPage() {
const createOfferEventBus = new EventTarget();
const publishOfferButton = new PublishOfferButton({
parentElement: document.getElementById('submit-button-area'),
id: 'button-submit-offer',
onClickCallback: async () => {
await publishOffer();
await myOffers.getOffersFromApi();
await myOffers.render();
},
});
publishOfferButton.render();
const buyOrSellButtonGroup = new BuyOrSellButtonGroup({
parentElement: document.getElementById('buy-or-sell-area'),
id: 'button-group-buy-or-sell',
});
buyOrSellButtonGroup.render();
const premiumSelector = new PremiumSelector({
parentElement: document.getElementById('premium-content-area'),
id: 'premium-selector-area',
eventSink: createOfferEventBus,
});
premiumSelector.render();
const priceDisplay = new PriceDisplay({
parentElement: document.getElementById('premium-content-area'),
id: 'premium-price-display-area',
premiumProvidingCallback: () => {
return premiumSelector.getPremium();
},
priceProvidingCallback: () => {
return Math.floor(Math.random() * (95000 - 70000 + 1) + 70000);
},
});
priceDisplay.render();
createOfferEventBus.addEventListener('premium-changed', () => {
priceDisplay.updatePrices();
});
// -----------
const navbuttonHome = document.getElementById('navbutton-home');
const navbuttonOffers = document.getElementById('navbutton-offers');
navbuttonHome.addEventListener('click', () => {
window.location.href = '/home';
});
navbuttonOffers.addEventListener('click', () => {
window.location.href = '/offers';
});
const buttonStartCreateOffer = document.getElementById(
'button-start-create-offer'
);
const buttonViewMyOffers = document.getElementById('button-view-my-offers');
const closeOffer = document.getElementById('close-offer');
const createOfferModalRoot = document.getElementById(
'create-offer-modal-root'
);
const viewMyOffersRoot = document.getElementById('view-my-offers-root');
const eurAmountInput = document.getElementById('input-eur-amount');
const btcAmountInput = document.getElementById('input-btc-amount');
const placeInput = document.getElementById('place-input');
const timeInput = document.getElementById('time-input');
const onchainCheckbox = document.getElementById('onchain-checkbox');
const lightningCheckbox = document.getElementById('lightning-checkbox');
const btcMethodCheckboxes = [onchainCheckbox, lightningCheckbox];
const myTrustedCheckbox = document.getElementById('my-trusted-checkbox');
const myTrustedTrustedCheckbox = document.getElementById(
'my-trusted-trusted-checkbox'
);
const allMembersCheckbox = document.getElementById('all-members-checkbox');
const bigNotesAcceptedCheckbox = document.getElementById(
'large-bills-checkbox'
);
const offerCreatedPopup = document.getElementById(
'offer-created-confirmation'
);
const offerDeletedPopup = document.getElementById(
'offer-deleted-confirmation'
);
const ownOffersContainer = document.getElementById('own-offers-container');
function toggleCreateOfferModal() {
createOfferModalRoot.classList.toggle('shown');
}
function toggleViewMyOffersPanel() {
viewMyOffersRoot.style.display =
viewMyOffersRoot.style.display === 'block' ? 'none' : 'block';
}
function readIntFromEurAmountInput() {
const eurAmountFieldValue = eurAmountInput.value;
const regularExpression = /([\d\s]+)/;
const matchResult = eurAmountFieldValue.match(regularExpression);
if (!matchResult) {
return null;
}
const numberString = matchResult[1];
const cleanInputNumber = parseInt(numberString.replace(/\s/gi, ''));
return cleanInputNumber;
}
function validateAndFormatEurAmountInput() {
const cleanInputNumber = readIntFromEurAmountInput();
eurAmountInput.classList.remove('input-is-valid', 'input-is-invalid');
if (cleanInputNumber) {
eurAmountInput.value = formatNumberWithSpaces(cleanInputNumber);
eurAmountInput.classList.add('input-is-valid');
return;
}
eurAmountInput.classList.add('input-is-invalid');
}
function updateBtcInput() {
const eurToSatRate = 1021;
const cleanEurAmount = readIntFromEurAmountInput();
const satsAmount = cleanEurAmount * eurToSatRate;
const formattedSatsAmount = formatNumberWithSpaces(satsAmount);
btcAmountInput.value = formattedSatsAmount;
}
function validateBitcoinMethodCheckboxes(clickedCheckbox) {
let checkedCount = btcMethodCheckboxes.filter((cb) => cb.checked).length;
if (checkedCount === 0) {
clickedCheckbox.checked = true;
}
}
function applyTrustCheckboxConstraints(pressedCheckbox) {
if (pressedCheckbox === myTrustedTrustedCheckbox) {
console.log('first case!');
if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) {
allMembersCheckbox.checked = false;
}
}
if (pressedCheckbox === allMembersCheckbox) {
console.log('second case!');
if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) {
myTrustedTrustedCheckbox.checked = true;
}
}
}
async function publishOffer() {
const wants = buyOrSellButtonGroup.wants();
const premium = premiumSelector.getPremium();
const trade_amount_eur = eurAmountInput.value;
const location_details = placeInput.value;
const time_availability_details = timeInput.value;
const is_onchain_accepted = onchainCheckbox.checked;
const is_lightning_accepted = lightningCheckbox.checked;
const show_offer_to_trusted = myTrustedCheckbox.checked;
const show_offer_to_trusted_trusted = myTrustedTrustedCheckbox.checked;
const show_offer_to_all_members = allMembersCheckbox.checked;
const are_big_notes_accepted = bigNotesAcceptedCheckbox.checked;
const offerDetails = {
wants,
premium,
trade_amount_eur,
location_details,
time_availability_details,
is_onchain_accepted,
is_lightning_accepted,
show_offer_to_trusted,
show_offer_to_trusted_trusted,
show_offer_to_all_members,
are_big_notes_accepted,
};
await fetch('/api/offer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ offerDetails }),
});
toggleOfferCreatedAlert();
toggleCreateOfferModal();
}
function toggleOfferCreatedAlert() {
offerCreatedPopup.classList.remove('max-size-zero');
offerCreatedPopup.classList.add('revealed');
setTimeout(() => {
offerCreatedPopup.classList.remove('revealed');
}, 3000);
setTimeout(() => {
offerCreatedPopup.classList.add('max-size-zero');
}, 4000);
}
function toggleOfferDeletedAlert() {
offerDeletedPopup.classList.remove('max-size-zero');
offerDeletedPopup.classList.add('revealed');
setTimeout(() => {
offerDeletedPopup.classList.remove('revealed');
}, 3000);
setTimeout(() => {
offerDeletedPopup.classList.add('max-size-zero');
}, 4000);
}
class Offer {
constructor(offerData) {
this.uuid = offerData.uuid;
this.public_key = offerData.public_key;
this.wants = offerData.wants;
this.premium = offerData.premium;
this.trade_amount_eur = offerData.trade_amount_eur;
this.location_details = offerData.location_details;
this.time_availability_details = offerData.time_availability_details;
this.show_offer_to_trusted = offerData.show_offer_to_trusted;
this.show_offer_to_trusted_trusted =
offerData.show_offer_to_trusted_trusted;
this.show_offer_to_all_members = offerData.show_offer_to_all_members;
this.is_onchain_accepted = offerData.is_onchain_accepted;
this.is_lightning_accepted = offerData.is_lightning_accepted;
this.are_big_notes_accepted = offerData.are_big_notes_accepted;
this.created_at = offerData.created_at;
this.last_updated_at = offerData.last_updated_at;
}
buildHTML() {
const offerCard = document.createElement('div');
offerCard.classList.add('myoffer-card');
offerCard.classList.add('shadowed-round-area');
const tradeDescDiv = document.createElement('div');
tradeDescDiv.classList.add('trade-desc');
const youBuyText = document.createElement('p');
youBuyText.classList.add('offer-card-content-title');
youBuyText.innerText = 'Compras';
tradeDescDiv.append(youBuyText);
const youBuyData = document.createElement('p');
youBuyData.classList.add('offer-card-content-data');
if (this.wants === 'BTC') {
youBuyData.innerText = `${this.trade_amount_eur * 1021} sats`;
}
if (this.wants === 'EUR') {
youBuyData.innerText = `${this.trade_amount_eur}`;
}
tradeDescDiv.append(youBuyData);
const youSellText = document.createElement('p');
youSellText.id = 'you-sell-title';
youSellText.classList.add('offer-card-content-title');
youSellText.innerText = 'Vendes';
tradeDescDiv.append(youSellText);
const youSellData = document.createElement('p');
youSellData.classList.add('offer-card-content-data');
if (this.wants === 'BTC') {
youSellData.innerText = `${this.trade_amount_eur}`;
}
if (this.wants === 'EUR') {
youSellData.innerText = `${this.trade_amount_eur * 1021} sats`;
}
tradeDescDiv.append(youSellData);
const premiumDescDiv = document.createElement('div');
premiumDescDiv.classList.add('premium-desc');
const premiumTitle = document.createElement('p');
premiumTitle.classList.add('offer-card-content-title');
premiumTitle.innerText = 'Premium';
premiumDescDiv.append(premiumTitle);
const premiumData = document.createElement('p');
premiumData.classList.add('offer-card-content-data');
premiumData.innerText = `${this.premium * 100} %`;
premiumDescDiv.append(premiumData);
const offerPriceTitle = document.createElement('p');
offerPriceTitle.classList.add('offer-card-content-title');
offerPriceTitle.innerText = 'Precio oferta';
premiumDescDiv.append(offerPriceTitle);
const offerPriceData = document.createElement('p');
offerPriceData.classList.add('offer-card-content-data');
offerPriceData.innerText = `90000 €/BTC`;
premiumDescDiv.append(offerPriceData);
const marketPriceTitle = document.createElement('p');
marketPriceTitle.classList.add('offer-card-content-title');
marketPriceTitle.innerText = 'Precio mercado';
premiumDescDiv.append(marketPriceTitle);
const marketPriceData = document.createElement('p');
marketPriceData.innerText = `88000 €/BTC`;
premiumDescDiv.append(marketPriceData);
const whereDescDiv = document.createElement('div');
whereDescDiv.classList.add('where-desc');
const whereDescTitle = document.createElement('p');
whereDescTitle.classList.add('offer-card-content-title');
whereDescTitle.innerText = 'Dónde';
whereDescDiv.append(whereDescTitle);
const whereDescData = document.createElement('p');
whereDescData.classList.add('offer-long-text');
whereDescData.innerText = `${this.location_details}`;
whereDescDiv.append(whereDescData);
const whenDescDiv = document.createElement('div');
whenDescDiv.classList.add('when-desc');
const whenDescTitle = document.createElement('p');
whenDescTitle.classList.add('offer-card-content-title');
whenDescTitle.innerText = 'Cúando';
whenDescDiv.append(whenDescTitle);
const whenDescData = document.createElement('p');
whenDescData.classList.add('offer-long-text');
whenDescData.innerText = `${this.time_availability_details}`;
whenDescDiv.append(whenDescData);
const bitcoinMethodsDiv = document.createElement('div');
bitcoinMethodsDiv.classList.add('bitcoin-methods-desc');
const bitcoinMethodsTitle = document.createElement('p');
bitcoinMethodsTitle.classList.add('offer-card-content-title');
bitcoinMethodsTitle.innerText = 'Protocolos Bitcoin aceptados';
bitcoinMethodsDiv.append(bitcoinMethodsTitle);
const onchainAcceptedContainer = document.createElement('div');
onchainAcceptedContainer.classList.add('left-icon-checkboxed-field');
if (this.is_onchain_accepted) {
const onchainIcon = document.createElement('img');
onchainIcon.src = '/img/chains-lasecagold.svg';
const onchainText = document.createElement('p');
onchainText.innerText = 'Onchain';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon);
} else {
const onchainIcon = document.createElement('img');
onchainIcon.src = '/img/chains-gray.svg';
const onchainText = document.createElement('p');
onchainText.innerText = 'Onchain';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon);
}
const lightningAcceptedContainer = document.createElement('div');
lightningAcceptedContainer.classList.add('left-icon-checkboxed-field');
if (this.is_lightning_accepted) {
const lightningIcon = document.createElement('img');
lightningIcon.src = '/img/bolt-lightning-lasecagold.svg';
const lightningText = document.createElement('p');
lightningText.innerText = 'Lightning';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
lightningAcceptedContainer.append(
lightningIcon,
lightningText,
checkIcon
);
} else {
const lightningIcon = document.createElement('img');
lightningIcon.src = '/img/bolt-lightning-gray.svg';
const lightningText = document.createElement('p');
lightningText.innerText = 'Lightning';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
lightningAcceptedContainer.append(
lightningIcon,
lightningText,
checkIcon
);
}
bitcoinMethodsDiv.append(
onchainAcceptedContainer,
lightningAcceptedContainer
);
const visibilityDiv = document.createElement('div');
visibilityDiv.classList.add('visibility-desc');
const visibilityTitle = document.createElement('p');
visibilityTitle.classList.add('offer-card-content-title');
visibilityTitle.innerText = 'Visibilidad';
visibilityDiv.append(visibilityTitle);
const showOfferToTrustedContainer = document.createElement('div');
showOfferToTrustedContainer.classList.add('right-icon-checkboxed-field');
if (this.show_offer_to_trusted) {
const showOfferToTrustedIcon = document.createElement('img');
showOfferToTrustedIcon.src = '/img/user-lasecagold.svg';
const showOfferToTrustedText = document.createElement('p');
showOfferToTrustedText.innerText = 'Confiados';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
showOfferToTrustedContainer.append(
showOfferToTrustedIcon,
showOfferToTrustedText,
checkIcon
);
} else {
const showOfferToTrustedIcon = document.createElement('img');
showOfferToTrustedIcon.src = '/img/user-gray.svg';
const showOfferToTrustedText = document.createElement('p');
showOfferToTrustedText.innerText = 'Confiados';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
showOfferToTrustedContainer.append(
showOfferToTrustedIcon,
showOfferToTrustedText,
checkIcon
);
}
const showOfferToTrustedTrustedContainer = document.createElement('div');
showOfferToTrustedTrustedContainer.classList.add(
'right-icon-checkboxed-field'
);
if (this.show_offer_to_trusted_trusted) {
const showOfferToTrustedTrustedIcon = document.createElement('img');
showOfferToTrustedTrustedIcon.src = '/img/user-group-lasecagold.svg';
const showOfferToTrustedTrustedText = document.createElement('p');
showOfferToTrustedTrustedText.innerText = 'Sus confiados';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
showOfferToTrustedTrustedContainer.append(
showOfferToTrustedTrustedIcon,
showOfferToTrustedTrustedText,
checkIcon
);
} else {
const showOfferToTrustedTrustedIcon = document.createElement('img');
showOfferToTrustedTrustedIcon.src = '/img/user-group-gray.svg';
const showOfferToTrustedTrustedText = document.createElement('p');
showOfferToTrustedTrustedText.innerText = 'Sus confiados';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
showOfferToTrustedTrustedContainer.append(
showOfferToTrustedTrustedIcon,
showOfferToTrustedTrustedText,
checkIcon
);
}
const showOfferToAllMembersContainer = document.createElement('div');
showOfferToAllMembersContainer.classList.add(
'right-icon-checkboxed-field'
);
if (this.show_offer_to_all_members) {
const showOfferToAllMembersIcon = document.createElement('img');
showOfferToAllMembersIcon.src = '/img/many-users-lasecagold.svg';
const showOfferToAllMembersText = document.createElement('p');
showOfferToAllMembersText.innerText = 'Todos';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
showOfferToAllMembersContainer.append(
showOfferToAllMembersIcon,
showOfferToAllMembersText,
checkIcon
);
} else {
const showOfferToAllMembersIcon = document.createElement('img');
showOfferToAllMembersIcon.src = '/img/many-users-gray.svg';
const showOfferToAllMembersText = document.createElement('p');
showOfferToAllMembersText.innerText = 'Todos';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
showOfferToAllMembersContainer.append(
showOfferToAllMembersIcon,
showOfferToAllMembersText,
checkIcon
);
}
visibilityDiv.append(
showOfferToTrustedContainer,
showOfferToTrustedTrustedContainer,
showOfferToAllMembersContainer
);
const otherOfferFeaturesDiv = document.createElement('div');
otherOfferFeaturesDiv.classList.add('other-desc');
const otherOfferFeaturesTitle = document.createElement('p');
otherOfferFeaturesTitle.classList.add('offer-card-content-title');
otherOfferFeaturesTitle.innerText = 'Otros';
otherOfferFeaturesDiv.append(otherOfferFeaturesTitle);
const areBigNotesAcceptedContainer = document.createElement('div');
areBigNotesAcceptedContainer.classList.add('left-icon-checkboxed-field');
if (this.are_big_notes_accepted) {
const areBigNotesAcceptedIcon = document.createElement('img');
areBigNotesAcceptedIcon.src = '/img/eur-bill-lasecagold.svg';
const areBigNotesAcceptedText = document.createElement('p');
areBigNotesAcceptedText.innerText = 'Billetes grandes';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-check-green.svg';
areBigNotesAcceptedContainer.append(
areBigNotesAcceptedIcon,
areBigNotesAcceptedText,
checkIcon
);
} else {
const areBigNotesAcceptedIcon = document.createElement('img');
areBigNotesAcceptedIcon.src = '/img/eur-bill-gray.svg';
const areBigNotesAcceptedText = document.createElement('p');
areBigNotesAcceptedText.innerText = 'Billetes grandes';
const checkIcon = document.createElement('img');
checkIcon.src = '/img/circle-xmark-gray.svg';
areBigNotesAcceptedContainer.append(
areBigNotesAcceptedIcon,
areBigNotesAcceptedText,
checkIcon
);
}
otherOfferFeaturesDiv.append(areBigNotesAcceptedContainer);
const actionButtonsArea = document.createElement('p');
actionButtonsArea.classList.add('offer-action-buttons-area');
const editActionArea = document.createElement('div');
editActionArea.classList.add('offer-action-area');
editActionArea.classList.add('subtle-box');
const editActionIcon = document.createElement('img');
editActionIcon.src = '/img/edit.svg';
const editActionText = document.createElement('p');
editActionText.innerText = 'Editar';
editActionArea.append(editActionIcon, editActionText);
const deleteActionArea = document.createElement('div');
deleteActionArea.classList.add('offer-action-area');
deleteActionArea.classList.add('subtle-box');
const deleteActionIcon = document.createElement('img');
deleteActionIcon.src = '/img/trash-can-darkred.svg';
const deleteActionText = document.createElement('p');
deleteActionText.innerText = 'Eliminar';
deleteActionArea.append(deleteActionIcon, deleteActionText);
deleteActionArea.addEventListener('click', async () => {
await deleteOfferByUuid(this.uuid);
await myOffers.getOffersFromApi();
await myOffers.render();
toggleOfferDeletedAlert();
});
actionButtonsArea.append(editActionArea, deleteActionArea);
offerCard.append(
tradeDescDiv,
premiumDescDiv,
whereDescDiv,
whenDescDiv,
bitcoinMethodsDiv,
visibilityDiv,
otherOfferFeaturesDiv,
actionButtonsArea
);
return offerCard;
}
}
class MyOffers {
constructor(ownOffersContainerElement) {
this.ownOffersContainerElement = ownOffersContainerElement;
this.offers = [];
}
async getOffersFromApi() {
const offersResponse = await fetch('/api/publickey-offers');
this.offers = [];
const offersData = (await offersResponse.json()).data;
if (offersResponse.ok) {
for (const record of offersData) {
this.offers.push(new Offer(record));
}
}
}
async render() {
if (this.offers.length === 0) {
this.ownOffersContainerElement.innerHTML =
'<p class="shadowed-round-area">Vaya, no hay nada por aquí...</p>';
return;
}
this.ownOffersContainerElement.innerHTML = '';
for (const someOffer of this.offers) {
this.ownOffersContainerElement.append(someOffer.buildHTML());
}
}
}
async function deleteOfferByUuid(offerUuid) {
await fetch(`/api/offer/${offerUuid}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
buttonStartCreateOffer.addEventListener('click', () => {
toggleCreateOfferModal();
});
buttonViewMyOffers.addEventListener('click', async () => {
await myOffers.getOffersFromApi();
await myOffers.render();
toggleViewMyOffersPanel();
});
closeOffer.addEventListener('click', () => {
toggleCreateOfferModal();
});
eurAmountInput.addEventListener('blur', () => {
validateAndFormatEurAmountInput();
updateBtcInput();
});
eurAmountInput.addEventListener('input', () => {
eurAmountInput.value = eurAmountInput.value.replace(/[^0-9]/g, '');
updateBtcInput();
});
for (const btcMethodCheckbox of btcMethodCheckboxes) {
btcMethodCheckbox.addEventListener('click', () => {
validateBitcoinMethodCheckboxes(btcMethodCheckbox);
});
}
myTrustedTrustedCheckbox.addEventListener('click', () => {
applyTrustCheckboxConstraints(myTrustedTrustedCheckbox);
});
allMembersCheckbox.addEventListener('click', () => {
applyTrustCheckboxConstraints(allMembersCheckbox);
});
updateBtcInput();
const myOffers = new MyOffers(ownOffersContainer);
}
offersPage();

View file

@ -1,9 +0,0 @@
function checkNostrExtension({ window, successCallback, failureCallback }) {
if (!window.nostr) {
failureCallback();
} else {
successCallback();
}
}
module.exports = checkNostrExtension;

View file

@ -1,20 +1,14 @@
class AttachPublicKeyMiddlewareProvider {
constructor({ sessionService }) {
this.sessionService = sessionService;
}
const sessionService = require('../services/sessionService');
provide() {
return async (req, res, next) => {
const publicKey = await this.sessionService.getPublicKeyRelatedToSession(
req.cookies.sessionUuid
);
async function attachPublicKeyMiddleware(req, res, next) {
const publicKey = await sessionService.getPublicKeyRelatedToSession(
req.cookies.sessionUuid
);
if (publicKey) {
req.cookies.publicKey = publicKey;
}
next();
};
if (publicKey) {
req.cookies.publicKey = publicKey;
}
next();
}
module.exports = AttachPublicKeyMiddlewareProvider;
module.exports = attachPublicKeyMiddleware;

View file

@ -1,39 +0,0 @@
const uuid = require('uuid');
class CreateSessionMiddlewareProvider {
constructor({ constants, sessionService }) {
this.constants = constants;
this.sessionService = sessionService;
}
provide() {
return async (req, res, next) => {
const sessionUuid = req.cookies.sessionUuid;
if (!sessionUuid) {
const newSession = await this.setAndPersistNewSession(res);
req.cookies.sessionUuid = newSession.uuid;
}
if (sessionUuid) {
if (!(await this.sessionService.isSessionValid(sessionUuid))) {
const newSession = await this.setAndPersistNewSession(res);
req.cookies.sessionUuid = newSession.uuid;
}
}
next();
};
}
async setAndPersistNewSession(res) {
const sessionUuid = uuid.v7();
res.cookie('sessionUuid', sessionUuid, {
httpOnly: true,
maxAge: this.constants.DEFAULT_SESSION_DURATION_SECONDS * 1000,
});
return await this.sessionService.createSession(sessionUuid);
}
}
module.exports = CreateSessionMiddlewareProvider;

View file

@ -1,55 +0,0 @@
class MiddlewaresProvider {
constructor({ constants, sessionService, profileService }) {
this.constants = constants;
this.sessionService = sessionService;
this.profileService = profileService;
}
provide() {
const AttachPublicKeyMiddlewareProvider = require('./attachPublicKeyMiddleware');
const attachPublicKeyMiddleware = new AttachPublicKeyMiddlewareProvider({
sessionService: this.sessionService,
}).provide();
const CreateSessionMiddlewareProvider = require('./createSessionMiddleware');
const createSessionMiddleware = new CreateSessionMiddlewareProvider({
constants: this.constants,
sessionService: this.sessionService,
}).provide();
const RejectIfNotAuthorizedMiddleware = require('./rejectIfNotAuthorizedMiddleware');
const rejectIfNotAuthorizedMiddleware = new RejectIfNotAuthorizedMiddleware(
{
sessionService: this.sessionService,
}
).provide();
const RedirectHomeIfAuthorized = require('./redirectHomeIfAuthorized');
const redirectHomeIfAuthorized = new RedirectHomeIfAuthorized({
sessionService: this.sessionService,
}).provide();
const RedirectIfNotAuthorizedMiddleware = require('./redirectIfNotAuthorizedMiddleware');
const redirectIfNotAuthorizedMiddleware =
new RedirectIfNotAuthorizedMiddleware({
sessionService: this.sessionService,
}).provide();
const RedirectIfMissingProfileDetailsMiddleware = require('./redirectIfMissingProfileDetailsMiddleware');
const redirectIfMissingProfileDetailsMiddleware =
new RedirectIfMissingProfileDetailsMiddleware({
profileService: this.profileService,
}).provide();
return {
redirectIfNotAuthorizedMiddleware,
attachPublicKeyMiddleware,
redirectIfMissingProfileDetailsMiddleware,
redirectHomeIfAuthorized,
rejectIfNotAuthorizedMiddleware,
createSessionMiddleware,
};
}
}
module.exports = MiddlewaresProvider;

View file

@ -1,18 +1,10 @@
class RedirectHomeIfAuthorized {
constructor({ sessionService }) {
this.sessionService = sessionService;
}
const sessionService = require('../services/sessionService');
provide() {
return async (req, res, next) => {
if (
await this.sessionService.isSessionAuthorized(req.cookies.sessionUuid)
) {
return res.redirect('/home');
}
next();
};
async function redirectHomeIfAuthorized(req, res, next) {
if (await sessionService.isSessionAuthorized(req.cookies.sessionUuid)) {
return res.redirect('/home');
}
next();
}
module.exports = RedirectHomeIfAuthorized;
module.exports = redirectHomeIfAuthorized;

View file

@ -1,17 +1,12 @@
class RedirectIfMissingProfileDetailsMiddleware {
constructor({ profileService }) {
this.profileService = profileService;
const profileService = require('../services/profileService');
async function redirectIfMissingProfileDetailsMiddleware(req, res, next) {
const publicKey = req.cookies.publicKey;
if (!(await profileService.areProfileDetailsComplete(publicKey))) {
res.redirect('/createProfile');
}
provide() {
return async (req, res, next) => {
const publicKey = req.cookies.publicKey;
if (!(await this.profileService.areProfileDetailsComplete(publicKey))) {
res.redirect('/createProfile');
}
next();
};
}
next();
}
module.exports = RedirectIfMissingProfileDetailsMiddleware;
module.exports = redirectIfMissingProfileDetailsMiddleware;

View file

@ -1,20 +1,10 @@
class RedirectIfNotAuthorizedMiddleware {
constructor({ sessionService }) {
this.sessionService = sessionService;
}
const sessionService = require('../services/sessionService');
provide() {
return async (req, res, next) => {
if (
!(await this.sessionService.isSessionAuthorized(
req.cookies.sessionUuid
))
) {
return res.redirect('/login');
}
next();
};
async function redirectIfNotAuthorizedMiddleware(req, res, next) {
if (!(await sessionService.isSessionAuthorized(req.cookies.sessionUuid))) {
return res.redirect('/login');
}
next();
}
module.exports = RedirectIfNotAuthorizedMiddleware;
module.exports = redirectIfNotAuthorizedMiddleware;

View file

@ -1,23 +1,13 @@
class RejectIfNotAuthorizedMiddleware {
constructor({ sessionService }) {
this.sessionService = sessionService;
}
const sessionService = require('../services/sessionService');
provide() {
return async (req, res, next) => {
if (
!(await this.sessionService.isSessionAuthorized(
req.cookies.sessionUuid
))
) {
return res.status(403).json({
success: false,
message: 'Your session is not authorized.',
});
}
next();
};
async function rejectIfNotAuthorizedMiddleware(req, res, next) {
if (!(await sessionService.isSessionAuthorized(req.cookies.sessionUuid))) {
return res.status(403).json({
success: false,
message: 'Your session is not authorized.',
});
}
next();
}
module.exports = RejectIfNotAuthorizedMiddleware;
module.exports = rejectIfNotAuthorizedMiddleware;

View file

@ -0,0 +1,33 @@
const uuid = require('uuid');
const sessionService = require('../services/sessionService');
const constants = require('../constants');
async function setAndPersistNewSession(res) {
const sessionUuid = uuid.v7();
res.cookie('sessionUuid', sessionUuid, {
httpOnly: true,
maxAge: constants.DEFAULT_SESSION_DURATION_SECONDS * 1000,
});
return await sessionService.createSession(sessionUuid);
}
async function createSessionMiddleware(req, res, next) {
const sessionUuid = req.cookies.sessionUuid;
if (!sessionUuid) {
const newSession = await setAndPersistNewSession(res);
req.cookies.sessionUuid = newSession.uuid;
}
if (sessionUuid) {
if (!(await sessionService.isSessionValid(sessionUuid))) {
const newSession = await setAndPersistNewSession(res);
req.cookies.sessionUuid = newSession.uuid;
}
}
next();
}
module.exports = createSessionMiddleware;

View file

@ -1,34 +1,27 @@
class AppInviteCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const AppInviteCreated = this.sequelize.define(
'AppInviteCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
inviter_pub_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'app_invite_created',
}
);
return AppInviteCreated;
const AppInviteCreated = sequelize.define(
'AppInviteCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
inviter_pub_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'app_invite_created',
}
}
);
module.exports = AppInviteCreatedProvider;
module.exports = AppInviteCreated;

View file

@ -1,38 +1,31 @@
class ContactDetailsSetProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const ContactDetailsSet = this.sequelize.define(
'ContactDetailsSet',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
encrypted_contact_details: {
type: this.DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'contact_details_set',
}
);
return ContactDetailsSet;
const ContactDetailsSet = sequelize.define(
'ContactDetailsSet',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
encrypted_contact_details: {
type: DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'contact_details_set',
}
}
);
module.exports = ContactDetailsSetProvider;
module.exports = ContactDetailsSet;

View file

@ -1,38 +1,31 @@
class LoginChallengeCompletedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const LoginChallengeCompleted = this.sequelize.define(
'LoginChallengeCompleted',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_completed',
}
);
return LoginChallengeCompleted;
const LoginChallengeCompleted = sequelize.define(
'LoginChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_completed',
}
}
);
module.exports = LoginChallengeCompletedProvider;
module.exports = LoginChallengeCompleted;

View file

@ -1,34 +1,27 @@
class LoginChallengeCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const LoginChallengeCreated = this.sequelize.define(
'LoginChallengeCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_created',
}
);
return LoginChallengeCreated;
const LoginChallengeCreated = sequelize.define(
'LoginChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'login_challenge_created',
}
}
);
module.exports = LoginChallengeCreatedProvider;
module.exports = LoginChallengeCreated;

View file

@ -1,43 +1,35 @@
class NostrChallengeCompletedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const NostrChallengeCompleted = this.sequelize.define(
'NostrChallengeCompleted',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: this.DataTypes.STRING,
allowNull: false,
unique: true,
},
signed_event: {
type: this.DataTypes.JSONB,
allowNull: false,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nostr_challenge_completed',
}
);
return NostrChallengeCompleted;
const NostrChallengeCompleted = sequelize.define(
'NostrChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: DataTypes.STRING,
allowNull: false,
},
signed_event: {
type: DataTypes.JSONB,
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nostr_challenge_completed',
}
}
);
module.exports = NostrChallengeCompletedProvider;
module.exports = NostrChallengeCompleted;

View file

@ -1,39 +1,31 @@
class NostrChallengeCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const NostrChallengeCreated = this.sequelize.define(
'NostrChallengeCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: this.DataTypes.STRING,
allowNull: false,
unique: true,
},
expires_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nostr_challenge_created',
}
);
return NostrChallengeCreated;
const NostrChallengeCreated = sequelize.define(
'NostrChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
challenge: {
type: DataTypes.STRING,
allowNull: false,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nostr_challenge_created',
}
}
);
module.exports = NostrChallengeCreatedProvider;
module.exports = NostrChallengeCreated;

View file

@ -1,38 +1,31 @@
class NymSetProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const NymSet = this.sequelize.define(
'NymSet',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
nym: {
type: this.DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nym_set',
}
);
return NymSet;
const NymSet = sequelize.define(
'NymSet',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
nym: {
type: DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'nym_set',
}
}
);
module.exports = NymSetProvider;
module.exports = NymSet;

View file

@ -1,34 +1,27 @@
class OfferCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const OfferCreated = this.sequelize.define(
'OfferCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'offer_created',
}
);
return OfferCreated;
const OfferCreated = sequelize.define(
'OfferCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'offer_created',
}
}
);
module.exports = OfferCreatedProvider;
module.exports = OfferCreated;

View file

@ -1,34 +0,0 @@
class OfferDeletedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
provide() {
const OfferDeleted = this.sequelize.define(
'OfferDeleted',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
offer_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'offer_deleted',
}
);
return OfferDeleted;
}
}
module.exports = OfferDeletedProvider;

View file

@ -1,78 +1,71 @@
class OfferDetailsSetProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const OfferDetailsSet = this.sequelize.define(
'OfferDetailsSet',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
offer_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
wants: {
type: this.DataTypes.STRING,
allowNull: false,
},
premium: {
type: this.DataTypes.DECIMAL(5, 2),
allowNull: false,
},
trade_amount_eur: {
type: this.DataTypes.INTEGER,
allowNull: false,
},
location_details: {
type: this.DataTypes.TEXT,
allowNull: false,
},
time_availability_details: {
type: this.DataTypes.TEXT,
allowNull: false,
},
show_offer_to_trusted: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
show_offer_to_trusted_trusted: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
show_offer_to_all_members: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
is_onchain_accepted: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
is_lightning_accepted: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
are_big_notes_accepted: {
type: this.DataTypes.BOOLEAN,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'offer_details_set',
}
);
return OfferDetailsSet;
const OfferDetailsSet = sequelize.define(
'OfferDetailsSet',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
offer_uuid: {
type: DataTypes.STRING,
allowNull: false,
},
wants: {
type: DataTypes.STRING,
allowNull: false,
},
premium: {
type: DataTypes.DECIMAL(5, 2),
allowNull: false,
},
trade_amount_eur: {
type: DataTypes.INTEGER,
allowNull: false,
},
location_details: {
type: DataTypes.TEXT,
allowNull: false,
},
time_availability_details: {
type: DataTypes.TEXT,
allowNull: false,
},
show_offer_to_trusted: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
show_offer_to_trusted_trusted: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
show_offer_to_all_members: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
is_onchain_accepted: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
is_lightning_accepted: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
are_big_notes_accepted: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'offer_details_set',
}
}
);
module.exports = OfferDetailsSetProvider;
module.exports = OfferDetailsSet;

View file

@ -1,34 +1,27 @@
class SessionCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const SessionCreated = this.sequelize.define(
'SessionCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
expires_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'session_created',
}
);
return SessionCreated;
const SessionCreated = sequelize.define(
'SessionCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'session_created',
}
}
);
module.exports = SessionCreatedProvider;
module.exports = SessionCreated;

View file

@ -1,37 +1,31 @@
class SessionRelatedToPublickeyProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const SessionRelatedToPublickey = this.sequelize.define(
'SessionRelatedToPublickey',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
session_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'session_related_to_public_key',
}
);
return SessionRelatedToPublickey;
const SessionRelatedToPublickey = sequelize.define(
'SessionRelatedToPublickey',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
session_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'session_related_to_public_key',
}
}
module.exports = SessionRelatedToPublickeyProvider;
);
module.exports = SessionRelatedToPublickey;

View file

@ -1,42 +1,35 @@
class SignUpChallengeCompletedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
provide() {
const SignUpChallengeCompleted = this.sequelize.define(
'SignUpChallengeCompleted',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
app_invite_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
public_key: {
type: this.DataTypes.STRING,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_completed',
}
);
return SignUpChallengeCompleted;
const SignUpChallengeCompleted = sequelize.define(
'SignUpChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
app_invite_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_completed',
}
}
);
module.exports = SignUpChallengeCompletedProvider;
module.exports = SignUpChallengeCompleted;

View file

@ -1,39 +1,31 @@
class SignUpChallengeCreatedProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const SignUpChallengeCreated = sequelize.define(
'SignUpChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
app_invite_uuid: {
type: DataTypes.UUID,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_created',
}
);
provide() {
const SignUpChallengeCreated = this.sequelize.define(
'SignUpChallengeCreated',
{
uuid: {
type: this.DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true,
},
nostr_challenge_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
app_invite_uuid: {
type: this.DataTypes.UUID,
allowNull: false,
},
created_at: {
type: this.DataTypes.DATE,
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_created',
}
);
return SignUpChallengeCreated;
}
}
module.exports = SignUpChallengeCreatedProvider;
module.exports = SignUpChallengeCreated;

View file

@ -1,97 +0,0 @@
class ModelsProvider {
constructor({ sequelize, DataTypes }) {
this.sequelize = sequelize;
this.DataTypes = DataTypes;
}
provide() {
const AppInviteCreatedProvider = require('./AppInviteCreated');
const AppInviteCreated = new AppInviteCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const ContactDetailsSetProvider = require('./ContactDetailsSet');
const ContactDetailsSet = new ContactDetailsSetProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const LoginChallengeCompletedProvider = require('./LoginChallengeCompleted');
const LoginChallengeCompleted = new LoginChallengeCompletedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const LoginChallengeCreatedProvider = require('./LoginChallengeCreated');
const LoginChallengeCreated = new LoginChallengeCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const NostrChallengeCompletedProvider = require('./NostrChallengeCompleted');
const NostrChallengeCompleted = new NostrChallengeCompletedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const NostrChallengeCreatedProvider = require('./NostrChallengeCreated');
const NostrChallengeCreated = new NostrChallengeCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const NymSetProvider = require('./NymSet');
const NymSet = new NymSetProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const OfferCreatedProvider = require('./OfferCreated');
const OfferCreated = new OfferCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const OfferDeletedProvider = require('./OfferDeleted');
const OfferDeleted = new OfferDeletedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const OfferDetailsSetProvider = require('./OfferDetailsSet');
const OfferDetailsSet = new OfferDetailsSetProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const SessionCreatedProvider = require('./SessionCreated');
const SessionCreated = new SessionCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const SessionRelatedToPublickeyProvider = require('./SessionRelatedToPublickey');
const SessionRelatedToPublickey = new SessionRelatedToPublickeyProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const SignUpChallengeCompletedProvider = require('./SignUpChallengeCompleted');
const SignUpChallengeCompleted = new SignUpChallengeCompletedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
const SignUpChallengeCreatedProvider = require('./SignUpChallengeCreated');
const SignUpChallengeCreated = new SignUpChallengeCreatedProvider({
sequelize: this.sequelize,
DataTypes: this.DataTypes,
}).provide();
return {
SignUpChallengeCreated,
SignUpChallengeCompleted,
SessionRelatedToPublickey,
SessionCreated,
OfferDeleted,
OfferDetailsSet,
OfferCreated,
NymSet,
NostrChallengeCreated,
NostrChallengeCompleted,
LoginChallengeCompleted,
LoginChallengeCreated,
ContactDetailsSet,
AppInviteCreated,
};
}
}
module.exports = ModelsProvider;

View file

@ -19,8 +19,8 @@
width: 50%;
}
.create-offer-step {
width: 95%;
#create-offer-controls > * {
width: 100%;
}
.checkbox-row {
@ -31,13 +31,6 @@
flex-shrink: 0;
margin-left: 5%;
}
.myoffer-card {
width: 100%;
height: 500px;
margin: 5px 5px;
padding: 10px;
}
}
@media (min-width: 769px) {
@ -58,12 +51,10 @@
width: 33%;
}
.create-offer-step {
#create-offer-controls > * {
margin-left: auto;
margin-right: auto;
width: 70%;
min-width: 500px;
max-width: 95%;
}
.checkbox-row {
@ -74,175 +65,12 @@
flex-shrink: 0;
margin-left: 20%;
}
.myoffer-card {
width: 23%;
min-width: 339px;
height: 500px;
margin: 10px 10px;
padding: 10px;
}
}
#view-my-offers-root {
display: none;
#create-offer-controls > * {
text-align: center;
}
#view-my-offers-root > * {
margin-left: auto;
margin-right: auto;
}
#own-offers-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.myoffer-card {
display: grid;
grid-template-columns: 48% 4% 48%;
grid-template-rows: 25% 1% 30% 1% 18% 1% 14% 1% 10%;
}
.myoffer-card > div {
padding: 5px;
}
.myoffer-card p {
margin: 0;
}
.trade-desc {
grid-column: 1;
grid-row: 1;
text-align: left;
}
.left-icon-checkboxed-field {
display: flex;
align-items: center;
margin-bottom: 2px;
font-size: 0.9em;
}
.left-icon-checkboxed-field > * {
margin-right: 2px;
margin-left: 2px;
}
.left-icon-checkboxed-field img {
height: 1.1em;
}
.right-icon-checkboxed-field {
display: flex;
align-items: center;
justify-content: end;
margin-bottom: 2px;
font-size: 0.9em;
}
.right-icon-checkboxed-field > * {
margin-right: 2px;
margin-left: 2px;
}
.right-icon-checkboxed-field img {
height: 1.2em;
}
.offer-card-content-title {
color: gray;
font-size: 0.8em;
font-weight: 100;
}
.offer-card-content-data {
font-size: 1.1em;
font-weight: bold;
}
.premium-desc {
grid-column: 3;
grid-row: 1;
text-align: right;
}
.where-desc {
grid-column: 1;
grid-row: 3;
text-align: left;
overflow-y: auto;
overflow-x: hidden;
}
.when-desc {
grid-column: 3;
grid-row: 3;
text-align: right;
overflow-y: auto;
overflow-x: hidden;
}
.bitcoin-methods-desc {
grid-column: 1;
grid-row: 5;
text-align: left;
}
.visibility-desc {
grid-column: 3;
grid-row: 5;
text-align: right;
}
.other-desc {
grid-column: 1;
grid-row: 7;
text-align: left;
}
.offer-action-buttons-area {
grid-column-start: 1;
grid-column-end: 4;
grid-row: 9;
display: flex;
align-items: center;
justify-content: flex-end;
}
.offer-action-area {
cursor: pointer;
margin-right: 20px;
padding: 3px;
}
.offer-long-text {
font-size: 0.9em;
}
#create-offer-controls {
text-align: center;
overflow-y: auto;
max-height: 800px;
padding: 20px;
}
.create-offer-step {
text-align: center;
border-radius: 20px;
box-shadow: 0 0 13px #ccc;
padding: 20px 0;
margin-top: 10px;
margin-bottom: 10px;
margin-left: auto;
margin-right: auto;
}
.create-offer-step h3 {
margin-top: 0;
}
#close-offer-controls-area {
display: flex;
justify-content: end;
@ -280,9 +108,6 @@
.premium-button {
background: white;
border: 2px solid #e1c300;
width: 100%;
height: 50%;
font-size: 1em;
}
.premium-button:hover {
@ -297,6 +122,12 @@
border-bottom-right-radius: 10px;
}
.premium-button {
width: 100%;
height: 50%;
font-size: 1em;
}
#premium-price-display-area {
margin-left: 0;
margin-right: auto;
@ -385,8 +216,3 @@
#button-submit-offer {
width: 33%;
}
#close-offer {
margin-left: auto;
margin-right: auto;
}

View file

@ -13,17 +13,10 @@
}
.button-medium {
padding: 0.5em 1em;
height: 3em;
padding: 1em;
border-radius: 10px;
}
.top-notification-good {
top: 25px;
}
#app-nav-bar > #laseca-logo {
height: 30px;
}
}
@media (min-width: 769px) {
@ -36,22 +29,23 @@
}
.button-medium {
height: 3em;
padding: 1em;
border-radius: 10px;
}
.top-notification-good {
top: 50px;
}
#app-nav-bar > #laseca-logo {
height: 30px;
}
}
#app-nav-bar > #laseca-logo {
margin-top: 0;
transform: translateY(20%);
.badges {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.badge {
border: 2px solid #e1c300;
border-radius: 10px;
padding: 2%;
margin: 2%;
}
body {
@ -78,72 +72,6 @@ h1 {
border: 2px solid red;
}
.max-size-zero {
max-width: 0;
max-height: 0;
}
.top-notification-good {
border-radius: 10px;
background-color: rgb(9, 165, 9);
color: white;
font-weight: 800;
opacity: 0.8;
position: fixed;
padding: 20px;
display: flex;
justify-content: space-evenly;
align-items: center;
left: 50%;
transform: translate(-50%, -500px);
transition: transform 1s ease-in-out;
}
.top-notification-good.revealed {
transform: translate(-50%, 0%);
}
.top-notification-good > * {
margin: 0 20px;
}
.top-notification-good > *:first-child {
margin-left: 0;
}
.top-notification-good > *:last-child {
margin-right: 0;
}
.top-notification-good img {
height: 50px;
}
.full-screen-modal-background {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.75);
transition: all 0.5s ease-in-out;
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.full-screen-modal-background.shown {
opacity: 1;
visibility: visible;
pointer-events: all;
}
.full-screen-modal {
background-color: white;
border-radius: 20px;
padding: 10px;
width: fit-content;
max-width: 95%;
margin: 20px auto;
}
.button-group button {
border: 0;
padding: 1em;
@ -179,21 +107,10 @@ h1 {
.over-background {
background-color: white;
border-radius: 1vw;
padding: 10px;
padding: 2vw;
margin: 1%;
}
.shadowed-round-area {
border-radius: 20px;
box-shadow: 0 0 13px #ccc;
padding: 20px;
}
.subtle-box {
border-radius: 10px;
border: 1px solid rgb(202, 202, 202);
}
#login-card > * {
text-align: center;
margin-right: auto;
@ -202,6 +119,18 @@ h1 {
margin-bottom: 1vh;
}
.create-profile-card-section > * {
text-align: center;
margin-right: auto;
margin-left: auto;
display: block;
max-width: 1000px;
}
.create-profile-card-section .badge img {
width: 10%;
}
.text-warning {
color: red;
font-weight: 800;
@ -223,25 +152,27 @@ h1 {
cursor: default;
}
.button-secondary {
background: white;
border: 0;
color: #e1c300;
cursor: pointer;
border: 3px solid #e1c300;
transition: all 0.5 ease-in-out;
}
.button-secondary:hover {
font-weight: bold;
}
.button-large {
font-weight: bold;
font-size: 1.5em;
padding: 0.5em;
}
.invite-card {
margin: 5vh 20vw;
height: 100%;
}
.invite-card-content {
margin: 3% 5%;
padding: 0 10%;
}
.invite-card-content * {
margin: 1vh auto;
text-align: center;
}
.card-secondary {
background-color: #369;
border-radius: 1vw;
@ -263,7 +194,6 @@ h1 {
color: white;
padding: 1%;
cursor: pointer;
border: 0;
}
.button-nostr:hover {
@ -275,3 +205,12 @@ h1 {
cursor: default;
border: 0;
}
#nostr-signup-button {
width: 20vw;
border: 0;
}
#nostr-signup-button p {
font-size: 1.5rem;
}

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

@ -0,0 +1,10 @@
const navbuttonHome = document.getElementById('navbutton-home');
const navbuttonOffers = document.getElementById('navbutton-offers');
navbuttonHome.addEventListener('click', () => {
window.location.href = '/home';
});
navbuttonOffers.addEventListener('click', () => {
window.location.href = '/offers';
});

View file

@ -0,0 +1,116 @@
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const validateNymInput = debounce(() => {
const nymValue = nymInput.value.trim();
const isValid = nymValue.length >= 3 && nymValue.length <= 128;
if (isValid) {
nymInputValidationWarning.style.display = 'none';
} else {
nymInputValidationWarning.style.display = 'block';
}
}, 500);
const checkIfSubmittable = debounce((allInputs) => {
const nymIsFilled = allInputs.nymInput.value !== '';
let atLeastOneContactIsFilled = false;
for (const contactInput of allInputs.contactInputs) {
if (contactInput.value !== '') {
atLeastOneContactIsFilled = true;
}
}
const buttonShouldBeDisabled = !(nymIsFilled && atLeastOneContactIsFilled);
submitProfileButton.disabled = buttonShouldBeDisabled;
}, 500);
async function createProfile(allInputs) {
const contactDetails = [];
for (const someInput of allInputs.contactInputs) {
contactDetails.push({
type: someInput.getAttribute('data-type'),
value: someInput.value,
});
}
const encryptedContactDetails = await window.nostr.nip04.encrypt(
await window.nostr.getPublicKey(),
JSON.stringify(contactDetails)
);
await fetch('/api/set-contact-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ encryptedContactDetails }),
});
const nym = allInputs.nymInput.value;
await fetch('/api/set-nym', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ nym }),
});
setTimeout(() => {
window.location.href = '/home';
}, 1000);
}
function onLoadErrands(allInputs, submitProfileButton) {
allInputs.nymInput.addEventListener('input', validateNymInput);
for (const someInput of allInputs.allInputs) {
someInput.addEventListener('input', () => {
checkIfSubmittable(allInputs);
});
}
checkIfSubmittable(allInputs);
submitProfileButton.addEventListener('click', () => {
createProfile(allInputs);
});
}
const nymInput = document.getElementById('nym-input');
const nymInputValidationWarning = document.getElementById(
'nym-input-validation-warning'
);
const phoneInput = document.getElementById('phone-input');
const whatsappInput = document.getElementById('whatsapp-input');
const telegramInput = document.getElementById('telegram-input');
const emailInput = document.getElementById('email-input');
const nostrInput = document.getElementById('nostr-input');
const signalInput = document.getElementById('signal-input');
const submitProfileButton = document.getElementById('submit-profile-button');
const allInputs = {
nymInput: nymInput,
contactInputs: [
phoneInput,
whatsappInput,
telegramInput,
emailInput,
nostrInput,
signalInput,
],
allInputs: [
nymInput,
phoneInput,
whatsappInput,
telegramInput,
emailInput,
nostrInput,
signalInput,
],
};
onLoadErrands(allInputs, submitProfileButton);

Some files were not shown because too many files have changed in this diff Show more