├── .dockerignore ├── static ├── images │ ├── favicon.ico │ └── default_listing.png └── css │ ├── fonts │ ├── 1Ptug8zYS_SKggPNyC0ITw.woff2 │ ├── 1Ptug8zYS_SKggPNyCAIT5lu.woff2 │ ├── 1Ptug8zYS_SKggPNyCIIT5lu.woff2 │ ├── 1Ptug8zYS_SKggPNyCMIT5lu.woff2 │ └── 1Ptug8zYS_SKggPNyCkIT5lu.woff2 │ ├── style.css │ ├── normalize.min.css │ ├── fonts.css │ └── skeleton.min.css ├── templates ├── logout.html.tera ├── deleted.html.tera ├── users.html.tera ├── index.html.tera ├── activeusers.html.tera ├── disabledusers.html.tera ├── mypaidorders.html.tera ├── myunpaidorders.html.tera ├── myactivelistings.html.tera ├── mypendinglistings.html.tera ├── myrejectedlistings.html.tera ├── mydeactivatedlistings.html.tera ├── myunsubmittedlistings.html.tera ├── reviewpendinglistings.html.tera ├── search.html.tera ├── deactivatedlistingsindex.html.tera ├── listingsindex.html.tera ├── myprocessingorders.html.tera ├── unreadmessagepage.html.tera ├── marketliabilities.html.tera ├── myaccountbalance.html.tera ├── sellerhistory.html.tera ├── search_form.html.tera ├── login.html.tera ├── signup.html.tera ├── withdrawal.html.tera ├── updatepgpinfo.html.tera ├── updateuserpgpinfo.html.tera ├── updatemarketname.html.tera ├── updatefeerate.html.tera ├── withdraw.html.tera ├── updateuserbondprice.html.tera ├── updatemaxallowedusers.html.tera ├── userprofile.html.tera ├── about.html.tera ├── deletelisting.html.tera ├── topsellers.html.tera ├── newlisting.html.tera ├── usertablepage.html.tera ├── deactivateaccount.html.tera ├── admin.html.tera ├── updatesqueaknodeinfo.html.tera ├── updateusersqueaknodeinfo.html.tera ├── accountbalancechangepage.html.tera ├── listingtablepage.html.tera ├── ordercardpage.html.tera ├── user.html.tera ├── searchlistingtablepage.html.tera ├── account.html.tera ├── accountactivation.html.tera ├── updatelistingimages.html.tera ├── prepareorder.html.tera ├── updateshippingoptions.html.tera ├── base.html.tera └── listing.html.tera ├── Rocket.toml ├── src ├── db.rs ├── util.rs ├── activate_account.rs ├── lightning.rs ├── config.rs ├── image_util.rs ├── user_account.rs ├── base.rs ├── admin.rs ├── main.rs ├── active_users.rs ├── disabled_users.rs ├── deactivated_listings.rs ├── top_sellers.rs ├── my_paid_orders.rs ├── review_pending_listings.rs ├── withdrawal.rs ├── my_unpaid_orders.rs ├── account.rs ├── my_processing_orders.rs ├── user_profile.rs ├── listings.rs ├── my_deactivated_listings.rs ├── order_expiry.rs ├── my_active_listings.rs ├── my_pending_listings.rs ├── my_rejected_listings.rs ├── my_unsubmitted_listings.rs ├── search.rs ├── market_liabilities.rs ├── my_account_balance.rs ├── user_account_expiry.rs ├── tests.rs ├── about.rs ├── update_pgp_info.rs ├── seller_history.rs ├── update_max_allowed_users.rs ├── update_market_name.rs ├── update_user_bond_price.rs ├── update_user_pgp_info.rs ├── update_fee_rate.rs ├── payment_processor.rs ├── update_squeaknode_info.rs ├── delete_listing.rs ├── update_user_squeaknode_info.rs ├── account_activation.rs ├── new_listing.rs └── user.rs ├── Makefile ├── .github └── workflows │ ├── audit.yaml │ ├── ci.yaml │ └── docker.yaml ├── entrypoint.sh ├── Dockerfile ├── .gitignore ├── LICENSE ├── Cargo.toml ├── README.md └── db └── migrations └── 20220729073857_initial_migration.sql /.dockerignore: -------------------------------------------------------------------------------- 1 | /.dockerignore 2 | /.git 3 | /.gitignore 4 | /Dockerfile 5 | /target 6 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/default_listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/images/default_listing.png -------------------------------------------------------------------------------- /templates/logout.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | You're logged out 4 | 5 | {% endblock body %} 6 | -------------------------------------------------------------------------------- /Rocket.toml: -------------------------------------------------------------------------------- 1 | [default.databases.squeakroad] 2 | url = "db.sqlite" 3 | 4 | [default.limits] 5 | file = "10 MiB" 6 | data-form="10 MiB" -------------------------------------------------------------------------------- /static/css/fonts/1Ptug8zYS_SKggPNyC0ITw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/css/fonts/1Ptug8zYS_SKggPNyC0ITw.woff2 -------------------------------------------------------------------------------- /static/css/fonts/1Ptug8zYS_SKggPNyCAIT5lu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/css/fonts/1Ptug8zYS_SKggPNyCAIT5lu.woff2 -------------------------------------------------------------------------------- /static/css/fonts/1Ptug8zYS_SKggPNyCIIT5lu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/css/fonts/1Ptug8zYS_SKggPNyCIIT5lu.woff2 -------------------------------------------------------------------------------- /static/css/fonts/1Ptug8zYS_SKggPNyCMIT5lu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/css/fonts/1Ptug8zYS_SKggPNyCMIT5lu.woff2 -------------------------------------------------------------------------------- /static/css/fonts/1Ptug8zYS_SKggPNyCkIT5lu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yzernik/squeakroad/HEAD/static/css/fonts/1Ptug8zYS_SKggPNyCkIT5lu.woff2 -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use rocket_db_pools::{sqlx, Database}; 2 | 3 | #[derive(Database)] 4 | #[database("squeakroad")] 5 | pub struct Db(pub sqlx::SqlitePool); 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | clean: 4 | cargo clean 5 | 6 | test: 7 | cargo test 8 | 9 | lint: 10 | cargo clippy 11 | 12 | .PHONY: all clean test lint 13 | -------------------------------------------------------------------------------- /templates/deleted.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | You're account has been deleted. You can verify here 4 | {% endblock body %} -------------------------------------------------------------------------------- /templates/users.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 10 | {% endblock body %} -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | all: 7 | name: All 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/audit-check@v1 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /templates/index.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | {% if user %} 4 | Hello there, your email is {{ user.email }}. 5 |
Delete your account. 6 | 7 | {% endif %} 8 | {% if not user %} 9 | Hello, anonymous user. 10 | {% endif %} 11 |
12 | Show all users 13 | {% endblock body %} 14 | -------------------------------------------------------------------------------- /templates/activeusers.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Active Users

12 | 13 |
14 | {% include "usertablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/disabledusers.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Disabled Users

12 | 13 |
14 | {% include "usertablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/mypaidorders.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Paid Orders

12 | 13 |
14 | {% include "ordercardpage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/myunpaidorders.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Unpaid Orders

12 | 13 |
14 | {% include "ordercardpage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/myactivelistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Active Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/mypendinglistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Pending Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/myrejectedlistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Rejected Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/mydeactivatedlistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Deactivated Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/myunsubmittedlistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Unsubmitted Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/reviewpendinglistings.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Review Pending Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/search.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Search Results: "{{ search_text }}"

12 | 13 |
14 | {% include "searchlistingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/deactivatedlistingsindex.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | Add New Listing 5 | 6 |
7 |

8 | 9 |
10 | 11 |

Deactivated Listings

12 | 13 |
14 | {% include "listingtablepage" %} 15 |
16 |
17 |
18 | 19 | 20 | {% endblock body %} 21 | -------------------------------------------------------------------------------- /templates/listingsindex.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | {% include "search_form" %} 5 | 6 | {% if not admin_user %} 7 | Add New Listing 8 | {% endif %} 9 | 10 |
11 |

12 | 13 |
14 |
15 | {% include "listingtablepage" %} 16 |
17 |
18 |
19 | 20 | 21 | {% endblock body %} 22 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Required environment variables 4 | # export SQUEAKROAD_DB_URL= 5 | # export SQUEAKROAD_ADMIN_USERNAME= 6 | # export SQUEAKROAD_ADMIN_PASSWORD= 7 | # export SQUEAKROAD_LND_HOST= 8 | # export SQUEAKROAD_LND_PORT= 9 | # export SQUEAKROAD_LND_TLS_CERT_PATH= 10 | # export SQUEAKROAD_LND_MACAROON_PATH= 11 | 12 | # Generate a secret 13 | export ROCKET_SECRET_KEY=$(openssl rand -base64 32) 14 | 15 | # if lnd enabled, attempt to connect 16 | exec squeakroad 17 | -------------------------------------------------------------------------------- /templates/myprocessingorders.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Processing Orders

12 |

Orders you have received that you have not yet shipped

13 | 14 |
15 | {% include "ordercardpage" %} 16 |
17 |
18 |
19 | 20 | 21 | {% endblock body %} 22 | -------------------------------------------------------------------------------- /templates/unreadmessagepage.html.tera: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {% if page_num > 1 %}Prev{% else %}Prev{% endif %} - Page {{ page_num }} - Next 15 | -------------------------------------------------------------------------------- /templates/marketliabilities.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Market Liabilies

12 | 13 |
14 | 15 |

Total Market Liabilities: {{ total_market_liabilities_sat }} sats

16 | 17 | {% include "accountbalancechangepage" %} 18 | 19 |
20 |
21 |
22 | 23 | 24 | {% endblock body %} 25 | -------------------------------------------------------------------------------- /templates/myaccountbalance.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

My Account Balance

12 | 13 |
14 | 15 |

Account Balance: {{ account_balance_sat }} sats

16 |

Withdraw Funds

17 | 18 | {% include "accountbalancechangepage" %} 19 | 20 |
21 |
22 |
23 | 24 | 25 | {% endblock body %} 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.62.0-buster AS builder 2 | 3 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ 4 | apt-get install -y \ 5 | libgexiv2-dev \ 6 | cmake 7 | 8 | COPY . ./ 9 | 10 | RUN cargo install --path . 11 | 12 | FROM debian:buster-slim 13 | 14 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ 15 | apt-get install -y \ 16 | openssl \ 17 | libgexiv2-dev 18 | 19 | COPY --from=builder /usr/local/cargo/bin/squeakroad /usr/local/bin/squeakroad 20 | COPY ./static /static 21 | COPY ./templates /templates 22 | 23 | COPY "entrypoint.sh" . 24 | RUN chmod +x entrypoint.sh 25 | 26 | CMD ["./entrypoint.sh"] 27 | -------------------------------------------------------------------------------- /templates/sellerhistory.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Seller History

12 | 13 |
14 | 15 |

User: {{ visited_user.username }}

16 |

Total Amount Sold: {{ amount_sold_sat }} sats

17 |

Weighted Average Rating: {{ weighted_average_rating | round(method="ceil", precision=2) }}

18 | 19 | {% include "ordercardpage" %} 20 | 21 |
22 |
23 |
24 | 25 | 26 | {% endblock body %} 27 | -------------------------------------------------------------------------------- /templates/search_form.html.tera: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |
5 | 6 |
7 |
8 | 11 | {% if flash %} 12 | 13 | {{ flash.1 }} 14 | 15 | {% endif %} 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | target 12 | 13 | # Generated databases 14 | db.sqlite 15 | db.sqlite-shm 16 | db.sqlite-wal 17 | 18 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 19 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 20 | Cargo.lock 21 | 22 | # Cargo config directory 23 | .cargo/ 24 | 25 | # The upload script, for now. 26 | scripts/upload-docs.sh 27 | scripts/redirect.html 28 | 29 | # Backup files. 30 | *.bak 31 | 32 | # Uploads in pastebin example. 33 | examples/pastebin/upload/* 34 | 35 | # App config file 36 | config.toml 37 | -------------------------------------------------------------------------------- /templates/login.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 |
4 | 5 |

Login

6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /templates/signup.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 |
4 | 5 |

Sign Up

6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /templates/withdrawal.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | 5 |
6 |

7 | 8 |
9 | 10 | 11 | {% if flash %} 12 | 13 | {{ flash.1 }} 14 | 15 | {% endif %} 16 | 17 |

Withdrawal

18 | 19 |

Withdrawal id: {{ withdrawal.public_id }}

20 |

User: {% if maybe_withdrawal_user %}{{ maybe_withdrawal_user.username }}{% else %}Not found{% endif %}

21 |

Amount: {{ withdrawal.amount_sat }} sats

22 |

Withdrawal time: {{ (withdrawal.created_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }}

23 |

Invoice hash: {{ withdrawal.invoice_hash }}

24 | 25 |
26 |
27 | 28 | 29 | {% endblock body %} 30 | -------------------------------------------------------------------------------- /templates/updatepgpinfo.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update PGP Info

12 | 13 |

PGP Key: {% if admin_settings.pgp_key %}

{{ admin_settings.pgp_key }}
{% else %}Not set{% endif %}

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /templates/updateuserpgpinfo.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update PGP Info

12 | 13 |

PGP Key: {% if user_settings.pgp_key %}

{{ user_settings.pgp_key }}
{% else %}Not set{% endif %}

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /templates/updatemarketname.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update Market Name

12 | 13 |

Market Name: {{ admin_settings.market_name }}

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use qr_code::QrCode; 2 | use rocket::serde::uuid::Uuid; 3 | use std::io::Cursor; 4 | use std::time::SystemTime; 5 | use std::time::UNIX_EPOCH; 6 | 7 | pub fn create_uuid() -> String { 8 | Uuid::new_v4().to_string() 9 | } 10 | 11 | pub fn current_time_millis() -> u64 { 12 | SystemTime::now() 13 | .duration_since(UNIX_EPOCH) 14 | .unwrap() 15 | .as_millis() as u64 16 | } 17 | 18 | pub fn generate_qr(payment_request: &str) -> Vec { 19 | let qr = QrCode::new(&payment_request.as_bytes()).unwrap(); 20 | let bmp = qr.to_bmp().add_white_border(4).unwrap().mul(4).unwrap(); 21 | let mut cursor = Cursor::new(vec![]); 22 | bmp.write(&mut cursor).unwrap(); 23 | cursor.into_inner() 24 | } 25 | 26 | pub fn to_hex(bytes: &Vec) -> String { 27 | hex::encode(bytes) 28 | } 29 | 30 | pub fn from_hex(hex_str: &str) -> Vec { 31 | hex::decode(hex_str).unwrap() 32 | } 33 | 34 | pub fn to_base64(bytes: &Vec) -> String { 35 | base64::encode(bytes) 36 | } 37 | -------------------------------------------------------------------------------- /templates/updatefeerate.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update Fee Rate

12 | 13 |

Fee Rate: {{ admin_settings.fee_rate_basis_points / 100 }}%

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /templates/withdraw.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 |

6 | 7 |
8 | 9 | {% if flash %} 10 | 11 | {{ flash.1 }} 12 | 13 | {% endif %} 14 | 15 |

Withdraw

16 | 17 |

Account Balance: {{ account_balance_sat }} sats

18 |

View Account Balance

19 | 20 |
21 |
22 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 | {% endblock body %} 36 | -------------------------------------------------------------------------------- /templates/updateuserbondprice.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update User Bond Price

12 | 13 |

Bond Price: {{ admin_settings.user_bond_price_sat }} sats

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /templates/updatemaxallowedusers.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update Max Allowed Users

12 | 13 |

Max allowed users: {{ admin_settings.max_allowed_users }}

14 | 15 |
16 |
17 | 20 | {% if flash %} 21 | 22 | {{ flash.1 }} 23 | 24 | {% endif %} 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | {% endblock body %} 35 | -------------------------------------------------------------------------------- /src/activate_account.rs: -------------------------------------------------------------------------------- 1 | use crate::db::Db; 2 | use crate::models::UserAccount; 3 | use rocket::fairing::AdHoc; 4 | use rocket::response::Redirect; 5 | use rocket_auth::User; 6 | use rocket_db_pools::Connection; 7 | 8 | #[get("/")] 9 | async fn index(mut db: Connection, user: Option) -> Result { 10 | match user { 11 | Some(user) => { 12 | let maybe_user_account = UserAccount::single(&mut db, user.id()).await.ok(); 13 | match maybe_user_account { 14 | Some(user_account) => Ok(Redirect::to(format!( 15 | "/{}/{}", 16 | "account_activation", user_account.public_id 17 | ))), 18 | None => Err(Redirect::to(uri!("/login"))), 19 | } 20 | } 21 | None => Err(Redirect::to(uri!("/login"))), 22 | } 23 | } 24 | 25 | pub fn activate_account_stage() -> AdHoc { 26 | AdHoc::on_ignite("Activate_Account Stage", |rocket| async { 27 | rocket.mount("/activate_account", routes![index]) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /templates/userprofile.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

User Profile: {{ visited_user.username }}

12 | 13 |

User: {{ visited_user.username }}

14 | 15 |

PGP Key: {% if visited_user_settings.pgp_key %}

{{ visited_user_settings.pgp_key }}
{% else %}Not set{% endif %}

16 | 17 |
Subscribe to updates from this user on Squeaknode:
18 | 19 |

Squeaknode Pubkey: {% if visited_user_settings.squeaknode_pubkey %}{{ visited_user_settings.squeaknode_pubkey }}{% else %}Not set{% endif %}

20 |

Squeaknode Address: {% if visited_user_settings.squeaknode_address %}{{ visited_user_settings.squeaknode_address }}{% else %}Not set{% endif %}

21 |
22 | 23 |
24 |
25 | 26 | 27 | {% endblock body %} 28 | -------------------------------------------------------------------------------- /templates/about.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |

About

5 | 6 |

Market Name: {{ admin_settings.market_name }}

7 |

Market Fee Rate: {{ admin_settings.fee_rate_basis_points / 100 }}% of each sale

8 |

User Bond Price: {{ admin_settings.user_bond_price_sat }} sats

9 |

Lightning Node: {{ lightning_node_pubkey }}

10 | 11 |

PGP Key: {% if admin_settings.pgp_key %}

{{ admin_settings.pgp_key }}
{% else %}Not set{% endif %}

12 | 13 |
Subscribe to updates from this market on Squeaknode:
14 |

Squeaknode Pubkey: {% if admin_settings.squeaknode_pubkey %}{{ admin_settings.squeaknode_pubkey }}{% else %}Not set{% endif %}

15 |

Squeaknode Address: {% if admin_settings.squeaknode_address %}{{ admin_settings.squeaknode_address }}{% else %}Not set{% endif %}

16 | 17 | {% endblock body %} 18 | -------------------------------------------------------------------------------- /templates/deletelisting.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 |

6 | 7 |

{{ listing_display.listing.title }}

8 | 9 |
Delete Listing
10 |

Back to listing

11 | 12 |
13 | 14 |
15 | 16 |
17 |

18 | 19 |
20 | 21 |

Are you sure you want to delete this listing?

22 | 23 |
24 | 25 | {% if flash %} 26 | 27 | {{ flash.1 }} 28 | 29 | {% endif %} 30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 | 42 | {% endblock body %} 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonathan Zernik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lightning.rs: -------------------------------------------------------------------------------- 1 | use tonic_openssl_lnd::connect_invoices; 2 | use tonic_openssl_lnd::connect_lightning; 3 | use tonic_openssl_lnd::LndInvoicesClient; 4 | use tonic_openssl_lnd::LndLightningClient; 5 | 6 | pub async fn get_lnd_lightning_client( 7 | lnd_host: String, 8 | lnd_port: u32, 9 | lnd_tls_cert_path: String, 10 | lnd_macaroon_path: String, 11 | ) -> Result { 12 | // TODO: don't use unwrap. 13 | let client = connect_lightning(lnd_host, lnd_port, lnd_tls_cert_path, lnd_macaroon_path) 14 | .await 15 | .map_err(|e| format!("Failed to get lightning lnd client: {:?}", e))?; 16 | 17 | Ok(client) 18 | } 19 | 20 | pub async fn get_lnd_invoices_client( 21 | lnd_host: String, 22 | lnd_port: u32, 23 | lnd_tls_cert_path: String, 24 | lnd_macaroon_path: String, 25 | ) -> Result { 26 | // TODO: don't use unwrap. 27 | let client = connect_invoices(lnd_host, lnd_port, lnd_tls_cert_path, lnd_macaroon_path) 28 | .await 29 | .map_err(|e| format!("Failed to get lightning invoices client: {:?}", e))?; 30 | 31 | Ok(client) 32 | } 33 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .field-error { 2 | border: 1px solid #ff0000 !important; 3 | } 4 | 5 | .field-error-msg { 6 | color: #ff0000; 7 | display: block; 8 | margin: -10px 0 10px 0; 9 | } 10 | 11 | .field-success { 12 | border: 1px solid #5AB953 !important; 13 | } 14 | 15 | .field-success-msg { 16 | color: #5AB953; 17 | display: block; 18 | margin: -10px 0 10px 0; 19 | } 20 | 21 | span.completed { 22 | text-decoration: line-through; 23 | } 24 | 25 | form.inline { 26 | display: inline; 27 | } 28 | 29 | form.link, 30 | button.link { 31 | display: inline; 32 | color: #1EAEDB; 33 | border: none; 34 | outline: none; 35 | background: none; 36 | cursor: pointer; 37 | padding: 0; 38 | margin: 0 0 0 0; 39 | height: inherit; 40 | text-decoration: underline; 41 | font-size: inherit; 42 | text-transform: none; 43 | font-weight: normal; 44 | line-height: inherit; 45 | letter-spacing: inherit; 46 | } 47 | 48 | form.link:hover, button.link:hover { 49 | color: #0FA0CE; 50 | } 51 | 52 | button.small { 53 | height: 20px; 54 | padding: 0 10px; 55 | font-size: 10px; 56 | line-height: 20px; 57 | margin: 0 2.5px; 58 | } 59 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use figment::{ 2 | providers::{Env, Format, Serialized, Toml}, 3 | Figment, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Deserialize, Serialize, Clone, Debug)] 8 | pub struct Config { 9 | pub db_url: String, 10 | pub admin_username: String, 11 | pub admin_password: String, 12 | pub lnd_host: String, 13 | pub lnd_port: u32, 14 | pub lnd_tls_cert_path: String, 15 | pub lnd_macaroon_path: String, 16 | } 17 | 18 | impl Default for Config { 19 | fn default() -> Config { 20 | Config { 21 | db_url: "db.sqlite".into(), 22 | admin_username: "admin".into(), 23 | admin_password: "pass".into(), 24 | lnd_host: "localhost".into(), 25 | lnd_port: 10009, 26 | lnd_tls_cert_path: "~/.lnd/tls.cert".into(), 27 | lnd_macaroon_path: "~/.lnd/data/chain/bitcoin/testnet/admin.macaroon".into(), 28 | } 29 | } 30 | } 31 | 32 | impl Config { 33 | pub fn get_config() -> Figment { 34 | Figment::from(Serialized::defaults(Config::default())) 35 | .merge(Toml::file("config.toml")) 36 | .merge(Env::prefixed("SQUEAKROAD_")) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/topsellers.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

Top Sellers

12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for seller_info in seller_infos %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 |
UserTotal Amount SoldRating
{{ seller_info.username }}{{ seller_info.total_amount_sold_sat }} sats{{ seller_info.weighted_average_rating | round(method="ceil", precision=2) }} (See rating)
34 | 35 | {% if page_num > 1 %}Prev{% else %}Prev{% endif %} - Page {{ page_num }} - Next 36 | 37 |
38 |
39 |
40 | 41 | 42 | {% endblock body %} 43 | -------------------------------------------------------------------------------- /templates/newlisting.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 |

Create New Listing

11 |

Market will collect a {{ admin_settings.fee_rate_basis_points / 100 }}% fee from each sale

12 |
13 | 14 | 17 | 18 | 20 | 21 | 23 | {% if flash %} 24 | 25 | {{ flash.1 }} 26 | 27 | {% endif %} 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 | 36 | {% endblock body %} 37 | -------------------------------------------------------------------------------- /src/image_util.rs: -------------------------------------------------------------------------------- 1 | use crate::util; 2 | use rexiv2::Metadata; 3 | use std::fs; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::Path; 7 | 8 | pub fn get_stripped_image_bytes(image_bytes: &Vec) -> Result, String> { 9 | let path_string = format!("/tmp/image_file_{}.data", util::create_uuid()); 10 | let path: &Path = Path::new(&path_string); 11 | fs::write(path, image_bytes).map_err(|_| "failed to write image to file.")?; 12 | 13 | match get_stripped_image_from_path(path) { 14 | Ok(data) => { 15 | fs::remove_file(path).ok(); 16 | Ok(data) 17 | } 18 | Err(e) => { 19 | fs::remove_file(path).ok(); 20 | Err(e) 21 | } 22 | } 23 | } 24 | 25 | pub fn get_stripped_image_from_path(file_path: &Path) -> Result, String> { 26 | let metadata = Metadata::new_from_path(file_path) 27 | .map_err(|_| "failed to load metadata from image buffer.")?; 28 | metadata.clear(); 29 | metadata 30 | .save_to_file(file_path) 31 | .map_err(|_| "failed to save cleared metadata back to file.")?; 32 | let mut f = File::open(file_path).map_err(|_| "failed to open the cleared image file.")?; 33 | let mut buffer = Vec::new(); 34 | f.read_to_end(&mut buffer) 35 | .map_err(|_| "failed to read cleared image file to bytes.")?; 36 | 37 | Ok(buffer) 38 | } 39 | -------------------------------------------------------------------------------- /templates/usertablepage.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for card in user_cards %} 12 | 13 | 14 | 19 | 22 | 25 | 34 | 35 | 36 | {% endfor %} 37 |
UsernameCreation TimeUser Bond AmountStatus
15 | 16 | {{ card.user.username }} 17 | 18 | 20 | {{ (card.user_account.created_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }} 21 | 23 | {{ card.user_account.amount_owed_sat }} sats 24 | 26 | {% if card.user_account.disabled %} 27 | Disabled by admin 28 | {% elif card.user_account.paid %} 29 | Paid 30 | {% else %} 31 | Not paid 32 | {% endif %} 33 |
38 | 39 | 40 | {% if page_num > 1 %} 41 |
42 | 43 | 44 |
45 | {% else %} 46 | Prev 47 | {% endif %} 48 | - Page {{ page_num }} - 49 |
50 | 51 | 52 |
53 | -------------------------------------------------------------------------------- /templates/deactivateaccount.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 |

6 | 7 |
8 | 9 | {% if flash %} 10 | 11 | {{ flash.1 }} 12 | 13 | {% endif %} 14 | 15 |

Deactivate Account

16 |

This action is permanent. Once you deactivate your account, you will not be able to access your account anymore.

17 |

Be sure to withdraw all funds from your account before deactivating.

18 |

When you are ready to deactive, you can submit a payment request for the amount of your account activation bond.

19 | 20 |

View Account Balance

21 | 22 |

Account Activation Deposit: {{ user_account.amount_owed_sat }} sats

23 | 24 |
25 |
26 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | {% endblock body %} 39 | -------------------------------------------------------------------------------- /templates/admin.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |

Admin

5 | 6 | Username: {{ user.email }}. 7 | 8 |
9 | Show market liabilities 10 | 11 |
12 |

13 |
14 |
Settings
15 | Update Market Name 16 |
17 | Update PGP Info 18 |
19 | Update Squeaknode Info 20 |
21 | Update Fee Rate 22 |
23 | Update User Bond Price 24 |
25 | Update Max Allowed Users 26 |
27 |
28 | 29 | 30 |
31 |

32 |
33 |
Listings
34 | Review Pending Listings 35 |
36 | Deactivated Listings 37 |
38 |
39 | 40 |
41 |

42 |
43 |
Users
44 | Number of users: {{ num_users }} 45 | Active Users 46 |
47 | Disabled Users 48 |
49 |
50 | 51 |
52 | 53 | {% endblock body %} 54 | -------------------------------------------------------------------------------- /templates/updatesqueaknodeinfo.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update Squeaknode Info

12 | 13 | {% if admin_settings.squeaknode_pubkey %} 14 |

Pubkey: {{ admin_settings.squeaknode_pubkey }}

15 | {% endif %} 16 | {% if admin_settings.squeaknode_address %} 17 |

Address: {{ admin_settings.squeaknode_address }}

18 | {% endif %} 19 | 20 |
21 |
22 | 25 | 28 | {% if flash %} 29 | 30 | {{ flash.1 }} 31 | 32 | {% endif %} 33 |
34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 | {% endblock body %} 43 | -------------------------------------------------------------------------------- /templates/updateusersqueaknodeinfo.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 | 7 |
8 |

9 | 10 |
11 |

Update User Squeaknode Info

12 | 13 |

Squeaknode Pubkey: {% if user_settings.squeaknode_pubkey %}{{ user_settings.squeaknode_pubkey }}{% else %}Not set{% endif %}

14 |

Squeaknode Address: {% if user_settings.squeaknode_address %}{{ user_settings.squeaknode_address }}{% else %}Not set{% endif %}

15 | 16 |
17 |
18 | 21 | 24 | {% if flash %} 25 | 26 | {{ flash.1 }} 27 | 28 | {% endif %} 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 | {% endblock body %} 39 | -------------------------------------------------------------------------------- /templates/accountbalancechangepage.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for account_balance_change in account_balance_changes %} 12 | 13 | 14 | 15 | 16 | 17 | 30 | 31 | 32 | {% endfor %} 33 |
Event timeAmount changeEvent typeEvent id
{{ (account_balance_change.event_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }}{{ account_balance_change.amount_change_sat }} sats{{ account_balance_change.event_type }}{{ account_balance_change.event_id }}
34 | 35 | 36 | {% if page_num > 1 %}Prev{% else %}Prev{% endif %} - Page {{ page_num }} - Next 37 | -------------------------------------------------------------------------------- /templates/listingtablepage.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for card in listing_cards %} 10 | 11 | 12 | 21 | 33 | 34 | 35 | {% endfor %} 36 |
13 | 14 | {% if card.image %} 15 | 16 | {% else %} 17 | 18 | {% endif %} 19 | 20 | 22 | 23 |

{{ card.listing.title }}

24 |
25 | 26 | {{ card.listing.price_sat }} sats 27 | 28 |
29 | 30 | {{ card.user.username }} 31 | 32 |
37 | 38 | 39 | {% if page_num > 1 %} 40 |
41 | 42 | 43 |
44 | {% else %} 45 | Prev 46 | {% endif %} 47 | - Page {{ page_num }} - 48 |
49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /templates/ordercardpage.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for order_card in order_cards %} 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 |
Payment TimeStatusOrderListingSale AmountRating
{% if order_card.order.paid %}{{ (order_card.order.payment_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }}{% else %}None{% endif %} 18 | {% if not order_card.order.paid %} 19 | Not paid 20 | {% else %} 21 | {% if order_card.order.shipped %}Shipped{% elif order_card.order.canceled_by_seller%}Canceled by seller{% elif order_card.order.canceled_by_buyer %}Canceled by buyer{% else %}Processing (waiting for seller to ship){% endif %} 22 | {% endif %} 23 | {{ order_card.order.public_id }}{% if order_card.listing %}{{ order_card.listing.title }}{% else %}Not found{% endif %}{{ order_card.order.amount_owed_sat }} sats{% if order_card.order.reviewed %}{{ order_card.order.review_rating }}{% else %}Unrated{% endif %}
32 | 33 | 34 | {% if page_num > 1 %}Prev{% else %}Prev{% endif %} - Page {{ page_num }} - Next 35 | -------------------------------------------------------------------------------- /templates/user.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 | 6 |
7 |

8 | 9 |
10 | 11 |

User: {{ visited_user.username }}

12 | 13 | {% if flash %} 14 | 15 | {{ flash.1 }} 16 | 17 | {% endif %} 18 | 19 |

User rating: {{ weighted_average_rating | round(method="ceil", precision=2) }} (See rating)

20 |

User account status: {% if visited_user_account.disabled %} 21 | Disabled by admin 22 | {% elif visited_user_account.paid %} 23 | Paid 24 | {% else %} 25 | Not paid 26 | {% endif %}

27 | {% if admin_user %} 28 | {% if not visited_user_account.disabled %} 29 |
30 | 31 | 32 |
33 | {% else %} 34 |
35 | 36 | 37 |
38 | {% endif %} 39 | {% endif %} 40 | 41 |

User Profile

42 | 43 |
44 | {% include "listingtablepage" %} 45 |
46 |
47 |
48 | 49 | 50 | {% endblock body %} 51 | -------------------------------------------------------------------------------- /src/user_account.rs: -------------------------------------------------------------------------------- 1 | use crate::db::Db; 2 | use crate::models::UserAccount; 3 | use rocket::http::Status; 4 | use rocket::outcome::try_outcome; 5 | use rocket::request::{FromRequest, Outcome, Request}; 6 | use rocket_auth::User; 7 | use rocket_db_pools::Connection; 8 | 9 | #[derive(Debug)] 10 | pub struct ActiveUser { 11 | //pub user: User, 12 | //pub user_account: UserAccount, 13 | pub user: User, 14 | pub user_account: UserAccount, 15 | } 16 | 17 | #[rocket::async_trait] 18 | impl<'r> FromRequest<'r> for ActiveUser { 19 | type Error = (); 20 | 21 | async fn from_request(request: &'r Request<'_>) -> Outcome { 22 | // This will unconditionally query the database! 23 | let user_outcome = request 24 | .guard::() 25 | .await 26 | .map_failure(|(status, _)| (status, ())); 27 | let user = try_outcome!(user_outcome); 28 | let db_outcome = request 29 | .guard::>() 30 | .await 31 | .map_failure(|(status, _)| (status, ())); 32 | let mut db = try_outcome!(db_outcome); 33 | 34 | // TODO: Query the database for the user account. 35 | let maybe_user_account = UserAccount::single(&mut db, user.id()).await.ok(); 36 | 37 | if let Some(user_account) = maybe_user_account { 38 | if user_account.paid && !user_account.disabled { 39 | Outcome::Success(ActiveUser { user, user_account }) 40 | } else { 41 | Outcome::Failure((Status::Unauthorized, ())) 42 | } 43 | } else { 44 | Outcome::Failure((Status::Unauthorized, ())) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/base.rs: -------------------------------------------------------------------------------- 1 | use crate::db::Db; 2 | use crate::models::{AccountInfo, AdminInfo, AdminSettings}; 3 | use rocket::serde::Serialize; 4 | use rocket_auth::AdminUser; 5 | use rocket_auth::User; 6 | use rocket_db_pools::Connection; 7 | 8 | #[derive(Debug, Serialize)] 9 | #[serde(crate = "rocket::serde")] 10 | pub struct BaseContext { 11 | user: Option, 12 | account_info: Option, 13 | admin_user: Option, 14 | admin_info: Option, 15 | admin_settings: Option, 16 | } 17 | 18 | impl BaseContext { 19 | pub async fn raw( 20 | db: &mut Connection, 21 | user: Option, 22 | admin_user: Option, 23 | ) -> Result { 24 | let account_info = match user { 25 | Some(ref u) => Some( 26 | AccountInfo::account_info_for_user(db, u.id()) 27 | .await 28 | .map_err(|_| "failed to get account info.")?, 29 | ), 30 | None => None, 31 | }; 32 | let admin_info = match admin_user { 33 | Some(_) => Some( 34 | AdminInfo::admin_info(db) 35 | .await 36 | .map_err(|_| "failed to get admin info.")?, 37 | ), 38 | None => None, 39 | }; 40 | let admin_settings = AdminSettings::single(db) 41 | .await 42 | .map_err(|_| "failed to get admin settings.")?; 43 | Ok(BaseContext { 44 | user, 45 | account_info, 46 | admin_user, 47 | admin_info, 48 | admin_settings: Some(admin_settings), 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /templates/searchlistingtablepage.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for card in listing_cards %} 10 | 11 | 12 | 21 | 33 | 34 | 35 | {% endfor %} 36 |
13 | 14 | {% if card.image %} 15 | 16 | {% else %} 17 | 18 | {% endif %} 19 | 20 | 22 | 23 |

{{ card.listing.title }}

24 |
25 | 26 | {{ card.listing.price_sat }} sats 27 | 28 |
29 | 30 | {{ card.user.username }} 31 | 32 |
37 | 38 | 39 | {% if page_num > 1 %} 40 |
41 | 42 | 43 | 44 |
45 | {% else %} 46 | Prev 47 | {% endif %} 48 | - Page {{ page_num }} - 49 |
50 | 51 | 52 | 53 |
54 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "squeakroad" 3 | version = "0.1.14" 4 | description = "Anonymous market with Lighting Network payments" 5 | authors = ["Jonathan Zernik "] 6 | license = "MIT" 7 | edition = "2021" 8 | publish = false 9 | 10 | [dependencies] 11 | # rocket = { version = "0.5.0-rc.2", features = ["json", "uuid"] } 12 | rocket = { git = "https://github.com/yzernik/Rocket", branch = "upgrade_sqlx_v_0_6", features = ["json", "uuid", "secrets"] } 13 | serde_json = "1.0.59" 14 | base64 = "0.13.0" 15 | figment = { version = "0.10", features = ["toml", "env"] } 16 | serde = "1.0.138" 17 | hex = "0.4.3" 18 | qr_code = { version = "1.1.0", features = ["bmp"] } 19 | pgp = "0.8.0" 20 | tonic_openssl_lnd = "0.1.5" 21 | rexiv2 = "0.9.1" 22 | 23 | [dependencies.sqlx] 24 | version = "0.6.0" 25 | default-features = false 26 | features = ["runtime-tokio-rustls", "macros", "offline", "migrate"] 27 | 28 | [dependencies.rocket_db_pools] 29 | # version = "0.1.0-rc.2" 30 | git = "https://github.com/yzernik/Rocket" 31 | branch = "upgrade_sqlx_v_0_6" 32 | features = ["sqlx_sqlite"] 33 | 34 | [dependencies.rocket_sync_db_pools] 35 | # version = "0.1.0-rc.2" 36 | git = "https://github.com/yzernik/Rocket" 37 | branch = "upgrade_sqlx_v_0_6" 38 | features = ["sqlite_pool"] 39 | 40 | [dependencies.rocket_auth] 41 | # version = "0.4.0" 42 | git = "https://github.com/yzernik/rocket_auth" 43 | branch = "internal_branch_for_rocket_sqlx_0_6_0_post3" 44 | features = ["sqlx-sqlite"] 45 | 46 | [dependencies.rocket_dyn_templates] 47 | version = "0.1.0-rc.1" 48 | git = "https://github.com/yzernik/Rocket" 49 | branch = "upgrade_sqlx_v_0_6" 50 | features = ["tera"] 51 | 52 | [dependencies.uuid] 53 | version = "1.1.2" 54 | features = [ 55 | "v4", # Lets you generate random UUIDs 56 | "fast-rng", # Use a faster (but still sufficiently random) RNG 57 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 58 | ] 59 | -------------------------------------------------------------------------------- /templates/account.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |

My Account

5 | 6 | Username: {{ user.email }} 7 |
8 | 9 | Account Balance: {{ account_info.account_balance_sat }} sats 10 |
11 | 12 | Account Balance History 13 |
14 | 15 | Withdraw Funds 16 |
17 | 18 | Account Activation 19 |
20 | 21 | 22 |
23 |

24 |
25 |
Seller Info
26 | My Shipped Orders 27 |
28 | My Processing Orders 29 |
30 | Add New Listing 31 |
32 | My Unsubmitted Listings 33 |
34 | My Pending Listings 35 |
36 | My Rejected Listings 37 |
38 | My Active Listings 39 |
40 | My Deactivated Listings 41 |
42 |
43 | 44 |
45 |

46 |
47 |
Buyer Info
48 | My Unpaid Orders 49 |
50 | My Paid Orders 51 |
52 |
53 | 54 |
55 |

56 |
57 |
Contact
58 | User Profile 59 | Update My PGP Info 60 | Update My Squeaknode Info 61 |
62 |
63 |
64 | 65 | 66 | {% endblock body %} 67 | -------------------------------------------------------------------------------- /static/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} -------------------------------------------------------------------------------- /src/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::UserAccount; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::AdminUser; 8 | use rocket_auth::User; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | #[derive(Debug, Serialize)] 13 | #[serde(crate = "rocket::serde")] 14 | struct Context { 15 | base_context: BaseContext, 16 | flash: Option<(String, String)>, 17 | num_users: u64, 18 | user: Option, 19 | admin_user: Option, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | mut db: Connection, 25 | flash: Option<(String, String)>, 26 | user: Option, 27 | admin_user: Option, 28 | ) -> Result { 29 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 30 | .await 31 | .map_err(|_| "failed to get base template.")?; 32 | let num_users = UserAccount::number_of_users(&mut db) 33 | .await 34 | .map_err(|_| "failed to get number of users.")?; 35 | Ok(Context { 36 | base_context, 37 | flash, 38 | num_users, 39 | user, 40 | admin_user, 41 | }) 42 | } 43 | } 44 | 45 | #[get("/")] 46 | async fn index( 47 | flash: Option>, 48 | db: Connection, 49 | user: User, 50 | admin_user: AdminUser, 51 | ) -> Result { 52 | let flash = flash.map(FlashMessage::into_inner); 53 | let context = Context::raw(db, flash, Some(user), Some(admin_user)) 54 | .await 55 | .map_err(|_| "failed to get template context.")?; 56 | 57 | Ok(Template::render("admin", context)) 58 | } 59 | 60 | pub fn admin_stage() -> AdHoc { 61 | AdHoc::on_ignite("Admin Stage", |rocket| async { 62 | rocket.mount("/admin", routes![index]) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - master 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | all: 17 | name: All 18 | 19 | strategy: 20 | matrix: 21 | os: 22 | - ubuntu-latest 23 | include: 24 | - os: ubuntu-latest 25 | rustflags: --deny warnings 26 | 27 | runs-on: ${{ matrix.os }} 28 | 29 | env: 30 | RUSTFLAGS: ${{ matrix.rustflags }} 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | 35 | - name: Make openssl-src Use Strawberry Perl 36 | if: matrix.os == 'windows-latest' 37 | run: echo OPENSSL_SRC_PERL=C:/Strawberry/perl/bin/perl >> $GITHUB_ENV 38 | 39 | - name: Install gexiv2 40 | run: sudo apt-get install -y libgexiv2-dev 41 | 42 | - name: Get latest CMake and ninja 43 | uses: lukka/get-cmake@latest 44 | 45 | - name: Install Rust Toolchain Components 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | components: clippy, rustfmt 49 | override: true 50 | toolchain: 1.62.0 51 | 52 | - uses: Swatinem/rust-cache@v1 53 | with: 54 | key: 0 55 | 56 | # - name: Check Lockfile 57 | # run: cargo update --locked --package squeakroad 58 | 59 | - name: Check 60 | run: | 61 | cargo check --all 62 | cargo check --tests 63 | cargo check --tests --all-features 64 | 65 | - name: Test 66 | run: cargo test --all --all-features 67 | 68 | - name: Clippy 69 | run: | 70 | cargo clippy --all-targets --all-features 71 | cargo clippy --all-targets --all-features --tests 72 | 73 | - name: Check Formatting 74 | run: cargo fmt --all -- --check 75 | 76 | # - name: Check for Forbidden Words 77 | # if: matrix.os == 'ubuntu-latest' 78 | # run: | 79 | # sudo apt-get update 80 | # sudo apt-get install ripgrep 81 | # ./bin/forbid 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # squeakroad 2 | 3 | [![GitHub release](https://img.shields.io/github/release/yzernik/squeakroad.svg)](https://github.com/yzernik/squeakroad/releases) 4 | [![GitHub CI workflow](https://github.com/yzernik/squeakroad/actions/workflows/ci.yaml/badge.svg)](https://github.com/yzernik/squeakroad/actions/workflows/ci.yaml) 5 | 6 | Open source darknet market with lightning network payments and withdrawals. 7 | 8 | ## Installation 9 | 10 | ### Requirements 11 | * an LND node 12 | * Rust and Cargo 13 | * openssl `apt install libssl-dev` 14 | * gexiv2 `apt install libgexiv2-dev` 15 | * compiler dependencies `apt install libprotobuf-dev protobuf-compiler cmake` 16 | 17 | ### Step 1. Create the configuration 18 | > Create a **config.toml** file and fill in the relevant sections to connect to your LND node: 19 | 20 | ``` 21 | db_url="db.sqlite" 22 | admin_username="admin" 23 | admin_password="pass" 24 | lnd_host="localhost" 25 | lnd_port=10009 26 | lnd_tls_cert_path="~/.lnd/tls.cert" 27 | lnd_macaroon_path="~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" 28 | ``` 29 | 30 | ### Step 2. Start squeakroad: 31 | 32 | ``` 33 | cargo run 34 | ``` 35 | 36 | Go to http://localhost:8000/ and use the username/password in **config.toml** to log in. 37 | 38 | ## Test 39 | 40 | ``` 41 | cargo test 42 | ``` 43 | 44 | ## Database Migrations 45 | 46 | Use [sqlx-cli](https://crates.io/crates/sqlx-cli/). 47 | 48 | `cargo install sqlx-cli` 49 | 50 | `cargo sqlx migrate --source db/migrations add ` 51 | 52 | Then put your SQL changes in the new file. 53 | 54 | `cargo sqlx migrate --source db/migrations run` 55 | 56 | After running migrations, generate the schema for compile-time type-checking: 57 | 58 | `cargo sqlx prepare --database-url sqlite3://db.sqlite` 59 | 60 | Optional: create a `.env` with `DATABASE_URL=sqlite3://db.sqlite` to avoid passing `--database-url` 61 | 62 | ## Telegram 63 | 64 | [Join our Telegram group!](https://t.me/squeakroad) 65 | 66 | ## License 67 | 68 | Distributed under the MIT License. See [LICENSE file](LICENSE). 69 | -------------------------------------------------------------------------------- /templates/accountactivation.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | 5 |
6 |

7 | 8 | {% if flash %} 9 | 10 | {{ flash.1 }} 11 | 12 | {% endif %} 13 | 14 |

Account Activation

15 | 16 |

User: {% if maybe_account_user %}{{ maybe_account_user.email }}{% else %}Not found{% endif %}

17 |

Status: 18 | {% if user_account.disabled %} 19 | Disabled by admin 20 | {% elif user_account.paid %} 21 | Paid 22 | {% else %} 23 | Not paid 24 | {% endif %}

25 |

Account creation time: {{ (user_account.created_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }}

26 | 27 |

Payment amount: {{ user_account.amount_owed_sat }} sats

28 | 29 | {% if user_account.disabled %} 30 | Please contact the admin to find out how to get your account re-enabled. 31 | {% elif user_account.paid %} 32 |

Payment time: {{ (user_account.payment_time_ms / 1000) | int | date(format="%Y-%m-%d %H:%M") }}

33 | {% if user.id == user_account.user_id %} 34 |

Continue as activated user

35 |

Deactivate account

36 | {% endif %} 37 | {% else %} 38 |

A user bond is required to activate your account.

39 |

You can withdraw the bond at any time in the future when you deactivate your account (as long as you don't misbehave).

40 |

Payment request:

41 | 42 |

43 |

44 |       {{ user_account.invoice_payment_request }}
45 |     
46 |

47 |

Open a channel to the market: {{ lightning_node_pubkey }}

48 | {% endif %} 49 | 50 |
51 | 52 | 53 | {% endblock body %} 54 | -------------------------------------------------------------------------------- /templates/updatelistingimages.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 |
5 |

6 | 7 |

{{ listing_display.listing.title }}

8 | 9 |
Add Listing Images
10 |

Back to listing

11 | 12 |
13 | 14 |
15 | 16 |
17 |

18 | 19 |
20 | 21 |
22 | 23 | 26 | {% if flash %} 27 | 28 | {{ flash.1 }} 29 | 30 | {% endif %} 31 | 32 | 33 |
34 | 35 | {% for image in listing_display.images %} 36 |
  • 37 | 38 |
    39 | 40 | 41 |
    42 |
    43 | 44 | 45 |
    46 |
  • 47 | {% endfor %} 48 | 49 |
    50 |
    51 | 52 |
    53 | 54 | 55 |
    56 | 57 | 58 | {% endblock body %} 59 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | 4 | #[cfg(test)] 5 | mod tests; 6 | 7 | mod about; 8 | mod account; 9 | mod account_activation; 10 | mod activate_account; 11 | mod active_users; 12 | mod admin; 13 | mod auth; 14 | mod base; 15 | mod config; 16 | mod db; 17 | mod deactivate_account; 18 | mod deactivated_listings; 19 | mod delete_listing; 20 | mod disabled_users; 21 | mod image_util; 22 | mod lightning; 23 | mod listing; 24 | mod listings; 25 | mod market_liabilities; 26 | mod models; 27 | mod my_account_balance; 28 | mod my_active_listings; 29 | mod my_deactivated_listings; 30 | mod my_paid_orders; 31 | mod my_pending_listings; 32 | mod my_processing_orders; 33 | mod my_rejected_listings; 34 | mod my_unpaid_orders; 35 | mod my_unsubmitted_listings; 36 | mod new_listing; 37 | mod order; 38 | mod order_expiry; 39 | mod payment_processor; 40 | mod prepare_order; 41 | mod review_pending_listings; 42 | mod routes; 43 | mod search; 44 | mod seller_history; 45 | mod top_sellers; 46 | mod update_fee_rate; 47 | mod update_listing_images; 48 | mod update_market_name; 49 | mod update_max_allowed_users; 50 | mod update_pgp_info; 51 | mod update_shipping_options; 52 | mod update_squeaknode_info; 53 | mod update_user_bond_price; 54 | mod update_user_pgp_info; 55 | mod update_user_squeaknode_info; 56 | mod user; 57 | mod user_account; 58 | mod user_account_expiry; 59 | mod user_profile; 60 | mod util; 61 | mod withdraw; 62 | mod withdrawal; 63 | 64 | #[launch] 65 | fn rocket() -> _ { 66 | let config_figment = config::Config::get_config(); 67 | let config: config::Config = config_figment.extract().unwrap(); 68 | println!("Starting with config: {:?}", config); 69 | 70 | let figment = rocket::Config::figment().merge(( 71 | "databases.squeakroad", 72 | rocket_db_pools::Config { 73 | url: config.clone().db_url, 74 | min_connections: None, 75 | max_connections: 1024, 76 | connect_timeout: 3, 77 | idle_timeout: None, 78 | }, 79 | )); 80 | 81 | rocket::custom(figment).attach(routes::stage(config)) 82 | } 83 | -------------------------------------------------------------------------------- /src/active_users.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::UserCard; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::response::status::NotFound; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | user_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let user_cards = UserCard::all_active(&mut db, PAGE_SIZE, page_num) 36 | .await 37 | .map_err(|_| "failed to get active users.")?; 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | user_cards, 42 | page_num, 43 | }) 44 | } 45 | } 46 | 47 | #[get("/?")] 48 | async fn index( 49 | flash: Option>, 50 | db: Connection, 51 | page_num: Option, 52 | user: User, 53 | admin_user: AdminUser, 54 | ) -> Result> { 55 | let flash = flash.map(FlashMessage::into_inner); 56 | Ok(Template::render( 57 | "activeusers", 58 | Context::raw(flash, db, page_num, user, Some(admin_user)).await, 59 | )) 60 | } 61 | 62 | pub fn active_users_stage() -> AdHoc { 63 | AdHoc::on_ignite("Active Users Stage", |rocket| async { 64 | rocket.mount("/active_users", routes![index]) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | permissions: 4 | packages: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - '**' 10 | tags: 11 | - '*.*.*' 12 | pull_request: 13 | 14 | jobs: 15 | main: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Set Swap Space to 10GB 19 | uses: pierotofy/set-swap-space@v1.0 20 | with: 21 | swap-size-gb: 10 22 | 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Docker meta 27 | id: docker_meta 28 | uses: docker/metadata-action@v3 29 | with: 30 | images: ghcr.io/${{ github.repository_owner }}/squeakroad 31 | tags: | 32 | type=ref,event=branch 33 | type=ref,event=pr 34 | type=semver,pattern=v{{version}} 35 | type=semver,pattern=v{{major}}.{{minor}} 36 | type=semver,pattern=v{{major}} 37 | type=sha 38 | 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v1 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v1 44 | 45 | - name: Cache Docker layers 46 | uses: actions/cache@v2 47 | with: 48 | path: /tmp/.buildx-cache 49 | key: ${{ runner.os }}-buildx-${{ github.sha }} 50 | restore-keys: | 51 | ${{ runner.os }}-buildx- 52 | 53 | - name: Login to GitHub Container Registry 54 | uses: docker/login-action@v1 55 | with: 56 | registry: ghcr.io 57 | username: ${{ github.repository_owner }} 58 | password: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Build and push 61 | id: docker_build 62 | uses: docker/build-push-action@v2 63 | with: 64 | platforms: linux/arm64,linux/amd64 65 | push: ${{ github.event_name != 'pull_request' }} 66 | tags: ${{ steps.docker_meta.outputs.tags }} 67 | labels: ${{ steps.docker_meta.outputs.labels }} 68 | cache-from: type=local,src=/tmp/.buildx-cache 69 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache 70 | -------------------------------------------------------------------------------- /src/disabled_users.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::UserCard; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | user_cards: Vec, 19 | page_num: u32, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | flash: Option<(String, String)>, 25 | mut db: Connection, 26 | maybe_page_num: Option, 27 | user: User, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let page_num = maybe_page_num.unwrap_or(1); 34 | let user_cards = UserCard::all_disabled(&mut db, PAGE_SIZE, page_num) 35 | .await 36 | .map_err(|_| "failed to get disabled users.")?; 37 | Ok(Context { 38 | base_context, 39 | flash, 40 | user_cards, 41 | page_num, 42 | }) 43 | } 44 | } 45 | 46 | #[get("/?")] 47 | async fn index( 48 | flash: Option>, 49 | db: Connection, 50 | page_num: Option, 51 | user: User, 52 | admin_user: AdminUser, 53 | ) -> Result { 54 | let flash = flash.map(FlashMessage::into_inner); 55 | let context = Context::raw(flash, db, page_num, user, Some(admin_user)) 56 | .await 57 | .map_err(|_| "failed to get template context.")?; 58 | Ok(Template::render("disabledusers", context)) 59 | } 60 | 61 | pub fn disabled_users_stage() -> AdHoc { 62 | AdHoc::on_ignite("Disabled Users Stage", |rocket| async { 63 | rocket.mount("/disabled_users", routes![index]) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /templates/prepareorder.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | {% if listing_display %} 5 | 6 | 7 |
    8 |

    9 | 10 |
    11 | 12 | 13 | {% if flash %} 14 | 15 | {{ flash.1 }} 16 | 17 | {% endif %} 18 | 19 |

    Prepare Order

    20 | 21 |

    Back to Listing

    22 | 23 |

    Listing: {{ listing_display.listing.title }} ({{ listing_display.listing.price_sat }} sats)

    24 |

    Quantity: {{ quantity }}

    25 |

    Shipping Option: {{ selected_shipping_option.title }} ({{ selected_shipping_option.price_sat }} sats)

    26 |

    Shipping option description: {{ selected_shipping_option.description }}

    27 | 28 |

    Total Price: {{ quantity }} x ({{ listing_display.listing.price_sat }} sats + {{ selected_shipping_option.price_sat }} sats) = {{ quantity * (selected_shipping_option.price_sat + listing_display.listing.price_sat) }} sats

    29 | 30 |

    Seller PGP Key: {% if seller_user_settings.pgp_key %}

    {{ seller_user_settings.pgp_key }}
    {% else %}Not set{% endif %}

    31 | 32 |
    33 | 34 | 36 | 37 | 40 | 41 |
    42 | 43 |
    44 |
    45 | 46 | {% endif %} 47 | 48 | {% endblock body %} 49 | -------------------------------------------------------------------------------- /src/deactivated_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | listing_cards: Vec, 19 | page_num: u32, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | flash: Option<(String, String)>, 25 | mut db: Connection, 26 | maybe_page_num: Option, 27 | user: Option, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let page_num = maybe_page_num.unwrap_or(1); 34 | let listing_cards = ListingCardDisplay::all_deactivated(&mut db, PAGE_SIZE, page_num) 35 | .await 36 | .map_err(|_| "failed to update market name.")?; 37 | Ok(Context { 38 | base_context, 39 | flash, 40 | listing_cards, 41 | page_num, 42 | }) 43 | } 44 | } 45 | 46 | #[get("/?")] 47 | async fn index( 48 | flash: Option>, 49 | db: Connection, 50 | page_num: Option, 51 | user: Option, 52 | admin_user: AdminUser, 53 | ) -> Result { 54 | let flash = flash.map(FlashMessage::into_inner); 55 | let context = Context::raw(flash, db, page_num, user, Some(admin_user)) 56 | .await 57 | .map_err(|_| "failed to get template context.")?; 58 | Ok(Template::render("deactivatedlistingsindex", context)) 59 | } 60 | 61 | pub fn deactivated_listings_stage() -> AdHoc { 62 | AdHoc::on_ignite("Deactivated Listings Stage", |rocket| async { 63 | rocket.mount("/deactivated_listings", routes![index]) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/top_sellers.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{Order, SellerInfo}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | seller_infos: Vec, 19 | page_num: u32, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | flash: Option<(String, String)>, 25 | mut db: Connection, 26 | maybe_page_num: Option, 27 | user: Option, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let page_num = maybe_page_num.unwrap_or(1); 34 | let seller_infos = Order::seller_info_for_all_users(&mut db, PAGE_SIZE, page_num) 35 | .await 36 | .map_err(|_| "failed to get seller infos for top users.")?; 37 | Ok(Context { 38 | base_context, 39 | flash, 40 | seller_infos, 41 | page_num, 42 | }) 43 | } 44 | } 45 | 46 | #[get("/?")] 47 | async fn index( 48 | flash: Option>, 49 | db: Connection, 50 | page_num: Option, 51 | user: Option, 52 | admin_user: Option, 53 | ) -> Result { 54 | let flash = flash.map(FlashMessage::into_inner); 55 | let context = Context::raw(flash, db, page_num, user, admin_user) 56 | .await 57 | .map_err(|_| "failed to get template context.")?; 58 | Ok(Template::render("topsellers", context)) 59 | } 60 | 61 | pub fn top_sellers_stage() -> AdHoc { 62 | AdHoc::on_ignite("Top Sellers Stage", |rocket| async { 63 | rocket.mount("/top_sellers", routes![index]) 64 | // .mount("/listing", routes![new]) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/my_paid_orders.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::OrderCard; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | order_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let order_cards = OrderCard::all_paid_for_user(&mut db, user.id, PAGE_SIZE, page_num) 36 | .await 37 | .map_err(|_| "failed to get paid orders.")?; 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | order_cards, 42 | page_num, 43 | }) 44 | } 45 | } 46 | 47 | #[get("/?")] 48 | async fn index( 49 | flash: Option>, 50 | db: Connection, 51 | page_num: Option, 52 | active_user: ActiveUser, 53 | admin_user: Option, 54 | ) -> Result { 55 | let flash = flash.map(FlashMessage::into_inner); 56 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 57 | .await 58 | .map_err(|_| "failed to get template context.")?; 59 | Ok(Template::render("mypaidorders", context)) 60 | } 61 | 62 | pub fn my_paid_orders_stage() -> AdHoc { 63 | AdHoc::on_ignite("My Paid Orders Stage", |rocket| async { 64 | rocket.mount("/my_paid_orders", routes![index]) 65 | // .mount("/listing", routes![new]) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/review_pending_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | listing_cards: Vec, 19 | page_num: u32, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | flash: Option<(String, String)>, 25 | mut db: Connection, 26 | maybe_page_num: Option, 27 | user: Option, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let page_num = maybe_page_num.unwrap_or(1); 34 | let listing_cards = ListingCardDisplay::all_pending(&mut db, PAGE_SIZE, page_num) 35 | .await 36 | .map_err(|_| "failed to get pending listings.")?; 37 | Ok(Context { 38 | base_context, 39 | flash, 40 | listing_cards, 41 | page_num, 42 | }) 43 | } 44 | } 45 | 46 | #[get("/?")] 47 | async fn index( 48 | flash: Option>, 49 | db: Connection, 50 | page_num: Option, 51 | user: Option, 52 | admin_user: AdminUser, 53 | ) -> Result { 54 | let flash = flash.map(FlashMessage::into_inner); 55 | let context = Context::raw(flash, db, page_num, user, Some(admin_user)) 56 | .await 57 | .map_err(|_| "failed to get template context.")?; 58 | Ok(Template::render("reviewpendinglistings", context)) 59 | } 60 | 61 | pub fn review_pending_listings_stage() -> AdHoc { 62 | AdHoc::on_ignite("Pending Listings Stage", |rocket| async { 63 | rocket.mount("/review_pending_listings", routes![index]) 64 | // .mount("/listing", routes![new]) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/withdrawal.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{RocketAuthUser, Withdrawal}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::AdminUser; 8 | use rocket_auth::User; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | #[derive(Debug, Serialize)] 13 | #[serde(crate = "rocket::serde")] 14 | struct Context { 15 | base_context: BaseContext, 16 | flash: Option<(String, String)>, 17 | withdrawal: Withdrawal, 18 | maybe_withdrawal_user: Option, 19 | user: Option, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | mut db: Connection, 25 | withdrawal_id: &str, 26 | flash: Option<(String, String)>, 27 | user: Option, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let withdrawal = Withdrawal::single_by_public_id(&mut db, withdrawal_id) 34 | .await 35 | .map_err(|_| "failed to get withdrawal.")?; 36 | let maybe_withdrawal_user = RocketAuthUser::single(&mut db, withdrawal.user_id) 37 | .await 38 | .ok(); 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | withdrawal, 43 | maybe_withdrawal_user, 44 | user, 45 | }) 46 | } 47 | } 48 | 49 | #[get("/")] 50 | async fn index( 51 | flash: Option>, 52 | id: &str, 53 | db: Connection, 54 | user: Option, 55 | admin_user: Option, 56 | ) -> Result { 57 | let flash = flash.map(FlashMessage::into_inner); 58 | let context = Context::raw(db, id, flash, user, admin_user) 59 | .await 60 | .map_err(|_| "failed to get template context.")?; 61 | Ok(Template::render("withdrawal", context)) 62 | } 63 | 64 | pub fn withdrawal_stage() -> AdHoc { 65 | AdHoc::on_ignite("Withdrawal Stage", |rocket| async { 66 | rocket.mount("/withdrawal", routes![index]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/my_unpaid_orders.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::OrderCard; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | order_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let order_cards = OrderCard::all_unpaid_for_user(&mut db, user.id, PAGE_SIZE, page_num) 36 | .await 37 | .map_err(|_| "failed to get unpaid orders.")?; 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | order_cards, 42 | page_num, 43 | }) 44 | } 45 | } 46 | 47 | #[get("/?")] 48 | async fn index( 49 | flash: Option>, 50 | db: Connection, 51 | page_num: Option, 52 | active_user: ActiveUser, 53 | admin_user: Option, 54 | ) -> Result { 55 | let flash = flash.map(FlashMessage::into_inner); 56 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 57 | .await 58 | .map_err(|_| "failed to get template context.")?; 59 | Ok(Template::render("myunpaidorders", context)) 60 | } 61 | 62 | pub fn my_unpaid_orders_stage() -> AdHoc { 63 | AdHoc::on_ignite("My Unpaid Orders Stage", |rocket| async { 64 | rocket.mount("/my_unpaid_orders", routes![index]) 65 | // .mount("/listing", routes![new]) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/account.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AccountInfo, UserAccount}; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::AdminUser; 9 | use rocket_auth::User; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | user: User, 19 | account_info: AccountInfo, 20 | user_account: UserAccount, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | mut db: Connection, 26 | flash: Option<(String, String)>, 27 | user: User, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let account_info = AccountInfo::account_info_for_user(&mut db, user.id()) 34 | .await 35 | .map_err(|_| "failed to get account info.")?; 36 | let user_account = UserAccount::single(&mut db, user.id()) 37 | .await 38 | .map_err(|_| "failed to get user account.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | user, 43 | account_info, 44 | user_account, 45 | }) 46 | } 47 | } 48 | 49 | #[get("/")] 50 | async fn index( 51 | flash: Option>, 52 | db: Connection, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(db, flash, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("account", context)) 61 | } 62 | 63 | pub fn account_stage() -> AdHoc { 64 | AdHoc::on_ignite("Account Stage", |rocket| async { 65 | rocket.mount("/account", routes![index]) 66 | // .mount("/listing", routes![new]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/my_processing_orders.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::OrderCard; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | order_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let order_cards = OrderCard::all_processing_for_user(&mut db, user.id, PAGE_SIZE, page_num) 36 | .await 37 | .map_err(|_| "failed to get processing orders.")?; 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | order_cards, 42 | page_num, 43 | }) 44 | } 45 | } 46 | 47 | #[get("/?")] 48 | async fn index( 49 | flash: Option>, 50 | db: Connection, 51 | page_num: Option, 52 | active_user: ActiveUser, 53 | admin_user: Option, 54 | ) -> Result { 55 | let flash = flash.map(FlashMessage::into_inner); 56 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 57 | .await 58 | .map_err(|_| "failed to get template context.")?; 59 | Ok(Template::render("myprocessingorders", context)) 60 | } 61 | 62 | pub fn my_processing_orders_stage() -> AdHoc { 63 | AdHoc::on_ignite("My Processing Orders Stage", |rocket| async { 64 | rocket.mount("/my_processing_orders", routes![index]) 65 | // .mount("/listing", routes![new]) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/user_profile.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{RocketAuthUser, UserSettings}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::AdminUser; 8 | use rocket_auth::User; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | #[derive(Debug, Serialize)] 13 | #[serde(crate = "rocket::serde")] 14 | struct Context { 15 | base_context: BaseContext, 16 | flash: Option<(String, String)>, 17 | visited_user: RocketAuthUser, 18 | visited_user_settings: UserSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | username: String, 25 | flash: Option<(String, String)>, 26 | user: Option, 27 | admin_user: Option, 28 | ) -> Result { 29 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 30 | .await 31 | .map_err(|_| "failed to get base template.")?; 32 | let visited_user = RocketAuthUser::single_by_username(&mut db, username) 33 | .await 34 | .map_err(|_| "failed to get visited user.")?; 35 | let visited_user_settings = UserSettings::single(&mut db, visited_user.id.unwrap()) 36 | .await 37 | .map_err(|_| "failed to get visited user settings.")?; 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | visited_user, 42 | visited_user_settings, 43 | }) 44 | } 45 | } 46 | 47 | #[get("/")] 48 | async fn index( 49 | username: &str, 50 | flash: Option>, 51 | db: Connection, 52 | user: Option, 53 | admin_user: Option, 54 | ) -> Result { 55 | let flash = flash.map(FlashMessage::into_inner); 56 | let context = Context::raw(db, username.to_string(), flash, user, admin_user) 57 | .await 58 | .map_err(|_| "failed to get template context.")?; 59 | Ok(Template::render("userprofile", context)) 60 | } 61 | 62 | pub fn user_profile_stage() -> AdHoc { 63 | AdHoc::on_ignite("User Profile Stage", |rocket| async { 64 | rocket.mount("/user_profile", routes![index]) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | listing_cards: Vec, 19 | page_num: u32, 20 | user: Option, 21 | admin_user: Option, 22 | } 23 | 24 | impl Context { 25 | pub async fn raw( 26 | flash: Option<(String, String)>, 27 | mut db: Connection, 28 | maybe_page_num: Option, 29 | user: Option, 30 | admin_user: Option, 31 | ) -> Result { 32 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 33 | .await 34 | .map_err(|_| "failed to get base template.")?; 35 | 36 | let page_num = maybe_page_num.unwrap_or(1); 37 | let listing_cards = ListingCardDisplay::all_active(&mut db, PAGE_SIZE, page_num) 38 | .await 39 | .map_err(|_| "failed to update market name.")?; 40 | 41 | Ok(Context { 42 | base_context, 43 | flash, 44 | listing_cards, 45 | page_num, 46 | user, 47 | admin_user, 48 | }) 49 | } 50 | } 51 | 52 | #[get("/?")] 53 | async fn index( 54 | flash: Option>, 55 | db: Connection, 56 | page_num: Option, 57 | user: Option, 58 | admin_user: Option, 59 | ) -> Result { 60 | let flash = flash.map(FlashMessage::into_inner); 61 | let context = Context::raw(flash, db, page_num, user, admin_user) 62 | .await 63 | .map_err(|_| "failed to get template context.")?; 64 | Ok(Template::render("listingsindex", context)) 65 | } 66 | 67 | pub fn listings_stage() -> AdHoc { 68 | AdHoc::on_ignite("Listings Stage", |rocket| async { 69 | rocket.mount("/", routes![index]) 70 | // .mount("/listing", routes![new]) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/my_deactivated_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | listing_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let listing_cards = 36 | ListingCardDisplay::all_deactivated_for_user(&mut db, user.id, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get deactivated listings.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_cards, 43 | page_num, 44 | }) 45 | } 46 | } 47 | 48 | #[get("/?")] 49 | async fn index( 50 | flash: Option>, 51 | db: Connection, 52 | page_num: Option, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("mydeactivatedlistings", context)) 61 | } 62 | 63 | pub fn my_deactivated_listings_stage() -> AdHoc { 64 | AdHoc::on_ignite("My Deactivated Listings Stage", |rocket| async { 65 | rocket.mount("/my_deactivated_listings", routes![index]) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/order_expiry.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::lightning::get_lnd_invoices_client; 3 | use crate::models::Order; 4 | use crate::util; 5 | use sqlx::pool::PoolConnection; 6 | use sqlx::Sqlite; 7 | use tonic_openssl_lnd::LndInvoicesClient; 8 | 9 | const ORDER_EXPIRY_INTERVAL_MS: u64 = 86400000; 10 | 11 | pub async fn remove_expired_orders( 12 | config: Config, 13 | mut conn: PoolConnection, 14 | ) -> Result<(), String> { 15 | let mut lightning_invoices_client = get_lnd_invoices_client( 16 | config.lnd_host.clone(), 17 | config.lnd_port, 18 | config.lnd_tls_cert_path.clone(), 19 | config.lnd_macaroon_path.clone(), 20 | ) 21 | .await 22 | .map_err(|e| format!("failed to get lightning client: {:?}", e))?; 23 | 24 | // Get all orders older than expiry time limit. 25 | let now = util::current_time_millis(); 26 | let expiry_cutoff = now - ORDER_EXPIRY_INTERVAL_MS; 27 | let expired_orders = Order::all_older_than(&mut conn, expiry_cutoff) 28 | .await 29 | .map_err(|_| "failed to expired orders.")?; 30 | 31 | for order in expired_orders { 32 | remove_order(&mut conn, &order, &mut lightning_invoices_client) 33 | .await 34 | .ok(); 35 | } 36 | Ok(()) 37 | } 38 | 39 | async fn remove_order( 40 | conn: &mut PoolConnection, 41 | order: &Order, 42 | lightning_invoices_client: &mut LndInvoicesClient, 43 | ) -> Result<(), String> { 44 | println!("deleting expired order: {:?}", order); 45 | let cancel_order_invoice_ret = cancel_order_invoice( 46 | lightning_invoices_client, 47 | util::from_hex(&order.invoice_hash), 48 | ); 49 | Order::delete_expired_order(conn, order.id.unwrap(), cancel_order_invoice_ret) 50 | .await 51 | .expect("failed to delete expired user account."); 52 | Ok(()) 53 | } 54 | 55 | async fn cancel_order_invoice( 56 | lightning_invoices_client: &mut LndInvoicesClient, 57 | payment_hash: Vec, 58 | ) -> Result { 59 | let cancel_response = lightning_invoices_client 60 | .cancel_invoice(tonic_openssl_lnd::invoicesrpc::CancelInvoiceMsg { payment_hash }) 61 | .await 62 | .expect("failed to cancel invoice") 63 | .into_inner(); 64 | Ok(cancel_response) 65 | } 66 | -------------------------------------------------------------------------------- /src/my_active_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | listing_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let listing_cards = 36 | ListingCardDisplay::all_active_for_user(&mut db, user.id, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get approved listings.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_cards, 43 | page_num, 44 | }) 45 | } 46 | } 47 | 48 | #[get("/?")] 49 | async fn index( 50 | flash: Option>, 51 | db: Connection, 52 | page_num: Option, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("myactivelistings", context)) 61 | } 62 | 63 | pub fn my_active_listings_stage() -> AdHoc { 64 | AdHoc::on_ignite("My Active Listings Stage", |rocket| async { 65 | rocket.mount("/my_active_listings", routes![index]) 66 | // .mount("/listing", routes![new]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/my_pending_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | listing_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let listing_cards = 36 | ListingCardDisplay::all_pending_for_user(&mut db, user.id, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get pending listings.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_cards, 43 | page_num, 44 | }) 45 | } 46 | } 47 | 48 | #[get("/?")] 49 | async fn index( 50 | flash: Option>, 51 | db: Connection, 52 | page_num: Option, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("mypendinglistings", context)) 61 | } 62 | 63 | pub fn my_pending_listings_stage() -> AdHoc { 64 | AdHoc::on_ignite("My Pending Listings Stage", |rocket| async { 65 | rocket.mount("/my_pending_listings", routes![index]) 66 | // .mount("/listing", routes![new]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/my_rejected_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | listing_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let listing_cards = 36 | ListingCardDisplay::all_rejected_for_user(&mut db, user.id, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get rejected listings.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_cards, 43 | page_num, 44 | }) 45 | } 46 | } 47 | 48 | #[get("/?")] 49 | async fn index( 50 | flash: Option>, 51 | db: Connection, 52 | page_num: Option, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("myrejectedlistings", context)) 61 | } 62 | 63 | pub fn my_rejected_listings_stage() -> AdHoc { 64 | AdHoc::on_ignite("My Rejected Listings Stage", |rocket| async { 65 | rocket.mount("/my_rejected_listings", routes![index]) 66 | // .mount("/listing", routes![new]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/my_unsubmitted_listings.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | listing_cards: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let listing_cards = 36 | ListingCardDisplay::all_unsubmitted_for_user(&mut db, user.id, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get unsubmitted listings.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_cards, 43 | page_num, 44 | }) 45 | } 46 | } 47 | 48 | #[get("/?")] 49 | async fn index( 50 | flash: Option>, 51 | db: Connection, 52 | page_num: Option, 53 | active_user: ActiveUser, 54 | admin_user: Option, 55 | ) -> Result { 56 | let flash = flash.map(FlashMessage::into_inner); 57 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 58 | .await 59 | .map_err(|_| "failed to get template context.")?; 60 | Ok(Template::render("myunsubmittedlistings", context)) 61 | } 62 | 63 | pub fn my_unsubmitted_listings_stage() -> AdHoc { 64 | AdHoc::on_ignite("My Unsubmitted Listings Stage", |rocket| async { 65 | rocket.mount("/my_unsubmitted_listings", routes![index]) 66 | // .mount("/listing", routes![new]) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::ListingCardDisplay; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::AdminUser; 8 | use rocket_auth::User; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | search_text: String, 20 | listing_cards: Vec, 21 | page_num: u32, 22 | } 23 | 24 | impl Context { 25 | pub async fn raw( 26 | mut db: Connection, 27 | search_text: String, 28 | flash: Option<(String, String)>, 29 | maybe_page_num: Option, 30 | user: Option, 31 | admin_user: Option, 32 | ) -> Result { 33 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 34 | .await 35 | .map_err(|_| "failed to get base template.")?; 36 | let page_num = maybe_page_num.unwrap_or(1); 37 | let listing_cards = ListingCardDisplay::all_active_for_search_text( 38 | &mut db, 39 | &search_text, 40 | PAGE_SIZE, 41 | page_num, 42 | ) 43 | .await 44 | .map_err(|_| "failed to get approved listings.")?; 45 | Ok(Context { 46 | base_context, 47 | flash, 48 | search_text, 49 | listing_cards, 50 | page_num, 51 | }) 52 | } 53 | } 54 | 55 | #[get("/?&")] 56 | async fn index( 57 | search_text: &str, 58 | flash: Option>, 59 | db: Connection, 60 | page_num: Option, 61 | user: Option, 62 | admin_user: Option, 63 | ) -> Result { 64 | let flash = flash.map(FlashMessage::into_inner); 65 | let context = Context::raw( 66 | db, 67 | search_text.to_string(), 68 | flash, 69 | page_num, 70 | user, 71 | admin_user, 72 | ) 73 | .await 74 | .map_err(|_| "failed to get template context.")?; 75 | Ok(Template::render("search", context)) 76 | } 77 | 78 | pub fn search_stage() -> AdHoc { 79 | AdHoc::on_ignite("Search Stage", |rocket| async { 80 | rocket.mount("/search", routes![index]) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/market_liabilities.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AccountBalanceChange, AccountInfo}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | total_market_liabilities_sat: i64, 19 | account_balance_changes: Vec, 20 | page_num: u32, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | flash: Option<(String, String)>, 26 | mut db: Connection, 27 | maybe_page_num: Option, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let page_num = maybe_page_num.unwrap_or(1); 35 | let account_balance_changes = 36 | AccountInfo::all_account_balance_changes(&mut db, PAGE_SIZE, page_num) 37 | .await 38 | .map_err(|_| "failed to get account balance changes.")?; 39 | let total_market_liabilities_sat = AccountInfo::total_market_liabilities_sat(&mut db) 40 | .await 41 | .map_err(|_| "failed to get total market liabilies.")?; 42 | Ok(Context { 43 | base_context, 44 | flash, 45 | total_market_liabilities_sat, 46 | account_balance_changes, 47 | page_num, 48 | }) 49 | } 50 | } 51 | 52 | #[get("/?")] 53 | async fn index( 54 | flash: Option>, 55 | db: Connection, 56 | page_num: Option, 57 | user: User, 58 | admin_user: AdminUser, 59 | ) -> Result { 60 | let flash = flash.map(FlashMessage::into_inner); 61 | let context = Context::raw(flash, db, page_num, user, Some(admin_user)) 62 | .await 63 | .map_err(|_| "failed to get template context.")?; 64 | Ok(Template::render("marketliabilities", context)) 65 | } 66 | 67 | pub fn market_liabilities_stage() -> AdHoc { 68 | AdHoc::on_ignite("Market Liabilies Stage", |rocket| async { 69 | rocket.mount("/market_liabilities", routes![index]) 70 | // .mount("/listing", routes![new]) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /templates/updateshippingoptions.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | 5 | 6 |
    7 |

    8 | 9 |

    {{ listing_display.listing.title }}

    10 | 11 |
    Add Shipping Options
    12 |

    Back to listing

    13 | 14 |
    15 | 16 |
    17 | 18 |
    19 |

    20 | 21 |
    22 | 23 |
    24 | 25 | 28 | 29 | 32 | 33 | 35 | {% if flash %} 36 | 37 | {{ flash.1 }} 38 | 39 | {% endif %} 40 | 41 | 42 |
    43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {% for shipping_option in listing_display.shipping_options %} 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | {% endfor %} 70 |
    Shipping Option NameShipping Option DescriptionPrice
    {{ shipping_option.title }}{{ shipping_option.description }}{{ shipping_option.price_sat }} sats 62 |
    63 | 64 | 65 |
    66 |
    71 | 72 |
    73 |
    74 | 75 |
    76 | 77 |
    78 | 79 | 80 | {% endblock body %} 81 | -------------------------------------------------------------------------------- /src/my_account_balance.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AccountBalanceChange, AccountInfo}; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::request::FlashMessage; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | const PAGE_SIZE: u32 = 10; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | account_balance_sat: i64, 20 | account_balance_changes: Vec, 21 | page_num: u32, 22 | } 23 | 24 | impl Context { 25 | pub async fn raw( 26 | flash: Option<(String, String)>, 27 | mut db: Connection, 28 | maybe_page_num: Option, 29 | user: User, 30 | admin_user: Option, 31 | ) -> Result { 32 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 33 | .await 34 | .map_err(|_| "failed to get base template.")?; 35 | let page_num = maybe_page_num.unwrap_or(1); 36 | let account_balance_changes = AccountInfo::all_account_balance_changes_for_user( 37 | &mut db, user.id, PAGE_SIZE, page_num, 38 | ) 39 | .await 40 | .map_err(|_| "failed to get account balance changes.")?; 41 | let account_info = AccountInfo::account_info_for_user(&mut db, user.id) 42 | .await 43 | .map_err(|_| "failed to get account info.")?; 44 | let account_balance_sat = account_info.account_balance_sat; 45 | Ok(Context { 46 | base_context, 47 | flash, 48 | account_balance_sat, 49 | account_balance_changes, 50 | page_num, 51 | }) 52 | } 53 | } 54 | 55 | #[get("/?")] 56 | async fn index( 57 | flash: Option>, 58 | db: Connection, 59 | page_num: Option, 60 | active_user: ActiveUser, 61 | admin_user: Option, 62 | ) -> Result { 63 | let flash = flash.map(FlashMessage::into_inner); 64 | let context = Context::raw(flash, db, page_num, active_user.user, admin_user) 65 | .await 66 | .map_err(|_| "failed to get template context.")?; 67 | Ok(Template::render("myaccountbalance", context)) 68 | } 69 | 70 | pub fn my_account_balance_stage() -> AdHoc { 71 | AdHoc::on_ignite("My Account Balance Stage", |rocket| async { 72 | rocket.mount("/my_account_balance", routes![index]) 73 | // .mount("/listing", routes![new]) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/user_account_expiry.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::lightning::get_lnd_invoices_client; 3 | use crate::models::UserAccount; 4 | use crate::util; 5 | use sqlx::pool::PoolConnection; 6 | use sqlx::Sqlite; 7 | use tonic_openssl_lnd::LndInvoicesClient; 8 | 9 | const USER_ACCOUNT_EXPIRY_INTERVAL_MS: u64 = 600000; // 10 minutes 10 | 11 | pub async fn remove_expired_user_accounts( 12 | config: Config, 13 | mut conn: PoolConnection, 14 | ) -> Result<(), String> { 15 | let mut lightning_invoices_client = get_lnd_invoices_client( 16 | config.lnd_host.clone(), 17 | config.lnd_port, 18 | config.lnd_tls_cert_path.clone(), 19 | config.lnd_macaroon_path.clone(), 20 | ) 21 | .await 22 | .map_err(|e| format!("failed to get lightning client: {:?}", e))?; 23 | 24 | // Delete all users without a corresponding user account. 25 | UserAccount::delete_users_with_no_account(&mut conn) 26 | .await 27 | .map_err(|_| "failed to delete users with no account.")?; 28 | 29 | // Get all unactivated user accounts older than expiry time limit. 30 | let now = util::current_time_millis(); 31 | let expiry_cutoff = now - USER_ACCOUNT_EXPIRY_INTERVAL_MS; 32 | let expired_user_accounts = UserAccount::all_older_than(&mut conn, expiry_cutoff) 33 | .await 34 | .map_err(|_| "failed to get expired user accounts.")?; 35 | 36 | for user_account in expired_user_accounts { 37 | remove_user_account(&mut conn, &user_account, &mut lightning_invoices_client) 38 | .await 39 | .expect("failed to remove user account."); 40 | } 41 | Ok(()) 42 | } 43 | 44 | async fn remove_user_account( 45 | conn: &mut PoolConnection, 46 | user_account: &UserAccount, 47 | lightning_invoices_client: &mut LndInvoicesClient, 48 | ) -> Result<(), String> { 49 | println!("deleting expired user account: {:?}", user_account); 50 | let cancel_user_account_invoice_ret = cancel_user_account_invoice( 51 | lightning_invoices_client, 52 | util::from_hex(&user_account.invoice_hash), 53 | ); 54 | UserAccount::delete_expired_user_account( 55 | conn, 56 | user_account.user_id, 57 | cancel_user_account_invoice_ret, 58 | ) 59 | .await 60 | .expect("failed to delete expired user account."); 61 | 62 | Ok(()) 63 | } 64 | 65 | async fn cancel_user_account_invoice( 66 | lightning_invoices_client: &mut LndInvoicesClient, 67 | payment_hash: Vec, 68 | ) -> Result { 69 | let cancel_response = lightning_invoices_client 70 | .cancel_invoice(tonic_openssl_lnd::invoicesrpc::CancelInvoiceMsg { payment_hash }) 71 | .await 72 | .expect("failed to cancel invoice") 73 | .into_inner(); 74 | Ok(cancel_response) 75 | } 76 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use rocket::fairing::AdHoc; 3 | use rocket::http::uri::fmt::{Query, UriDisplay}; 4 | use rocket::http::ContentType; 5 | use rocket::local::blocking::Client; 6 | use rocket::serde::{Deserialize, Serialize}; 7 | use rocket::{Build, Rocket}; 8 | 9 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, UriDisplayQuery)] 10 | #[serde(crate = "rocket::serde")] 11 | struct LoginInfo { 12 | email: String, 13 | password: String, 14 | } 15 | 16 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, UriDisplayQuery)] 17 | #[serde(crate = "rocket::serde")] 18 | struct UpdateMarketNameInfo { 19 | market_name: String, 20 | } 21 | 22 | fn rocket_build(config: Config) -> Rocket { 23 | let figment = rocket::Config::figment().merge(( 24 | "databases.squeakroad", 25 | rocket_db_pools::Config { 26 | url: config.db_url, 27 | min_connections: None, 28 | max_connections: 1024, 29 | connect_timeout: 3, 30 | idle_timeout: None, 31 | }, 32 | )); 33 | 34 | rocket::custom(figment) 35 | } 36 | 37 | fn test_admin_settings(_base: &str, stage: AdHoc, config: Config) { 38 | // NOTE: If we had more than one test running concurently that dispatches 39 | // DB-accessing requests, we'd need transactions or to serialize all tests. 40 | let client = Client::tracked(rocket_build(config.clone()).attach(stage)).unwrap(); 41 | 42 | // Log in as admin user. 43 | let admin_login_info = LoginInfo { 44 | email: config.admin_username, 45 | password: config.admin_password, 46 | }; 47 | let login_response = client 48 | .post("/login") 49 | .header(ContentType::Form) 50 | .body((&admin_login_info as &dyn UriDisplay).to_string()) 51 | .dispatch(); 52 | println!("login_response: {:?}", login_response); 53 | 54 | // Update the market name. 55 | let update_market_name_info = UpdateMarketNameInfo { 56 | market_name: "test-market-name".to_string(), 57 | }; 58 | let update_market_name_response = client 59 | .post("/update_market_name/change") 60 | .header(ContentType::Form) 61 | .body((&update_market_name_info as &dyn UriDisplay).to_string()) 62 | .dispatch(); 63 | println!( 64 | "update_market_name_response: {:?}", 65 | update_market_name_response 66 | ); 67 | 68 | // Get the index page and check the market name. 69 | let index_page_response = client.get("/").dispatch(); 70 | let index_page_string = index_page_response.into_string().unwrap(); 71 | println!("{}", index_page_string); 72 | assert!(index_page_string.contains("test-market-name")); 73 | } 74 | 75 | #[test] 76 | fn test_routes() { 77 | let config_figment = Config::get_config().merge(("db_url", "sqlite://:memory:".to_string())); 78 | let config: Config = config_figment.extract().unwrap(); 79 | 80 | test_admin_settings("/", crate::routes::stage(config.clone()), config); 81 | } 82 | -------------------------------------------------------------------------------- /src/about.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::config::Config; 3 | use crate::db::Db; 4 | use crate::lightning; 5 | use crate::models::AdminSettings; 6 | use rocket::fairing::AdHoc; 7 | use rocket::request::FlashMessage; 8 | use rocket::serde::Serialize; 9 | use rocket::State; 10 | use rocket_auth::AdminUser; 11 | use rocket_auth::User; 12 | use rocket_db_pools::Connection; 13 | use rocket_dyn_templates::Template; 14 | 15 | #[derive(Debug, Serialize)] 16 | #[serde(crate = "rocket::serde")] 17 | struct Context { 18 | base_context: BaseContext, 19 | flash: Option<(String, String)>, 20 | admin_settings: AdminSettings, 21 | lightning_node_pubkey: String, 22 | } 23 | 24 | impl Context { 25 | pub async fn raw( 26 | mut db: Connection, 27 | flash: Option<(String, String)>, 28 | user: Option, 29 | admin_user: Option, 30 | config: &Config, 31 | ) -> Result { 32 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 33 | .await 34 | .map_err(|_| "failed to get base template.")?; 35 | let admin_settings = AdminSettings::single(&mut db) 36 | .await 37 | .map_err(|_| "failed to update market name.")?; 38 | let lightning_node_pubkey = get_lightning_node_pubkey(config) 39 | .await 40 | .unwrap_or_else(|_| "".to_string()); 41 | Ok(Context { 42 | base_context, 43 | flash, 44 | admin_settings, 45 | lightning_node_pubkey, 46 | }) 47 | } 48 | } 49 | 50 | async fn get_lightning_node_pubkey(config: &Config) -> Result { 51 | let mut lightning_client = lightning::get_lnd_lightning_client( 52 | config.lnd_host.clone(), 53 | config.lnd_port, 54 | config.lnd_tls_cert_path.clone(), 55 | config.lnd_macaroon_path.clone(), 56 | ) 57 | .await 58 | .expect("failed to get lightning client"); 59 | let get_info_resp = lightning_client 60 | // All calls require at least empty parameter 61 | .get_info(tonic_openssl_lnd::lnrpc::GetInfoRequest {}) 62 | .await 63 | .expect("failed to get lightning node info") 64 | .into_inner(); 65 | Ok(get_info_resp.identity_pubkey) 66 | } 67 | 68 | #[get("/")] 69 | async fn index( 70 | flash: Option>, 71 | db: Connection, 72 | user: Option, 73 | admin_user: Option, 74 | config: &State, 75 | ) -> Result { 76 | let flash = flash.map(FlashMessage::into_inner); 77 | let context = Context::raw(db, flash, user, admin_user, config.inner()) 78 | .await 79 | .map_err(|_| "failed to get template context.")?; 80 | Ok(Template::render("about", context)) 81 | } 82 | 83 | pub fn about_stage() -> AdHoc { 84 | AdHoc::on_ignite("About Stage", |rocket| async { 85 | rocket.mount("/about", routes![index]) 86 | // .mount("/listing", routes![new]) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /src/update_pgp_info.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, PGPInfoInput}; 4 | use pgp::Deserializable; 5 | use pgp::SignedPublicKey; 6 | use rocket::fairing::AdHoc; 7 | use rocket::form::Form; 8 | use rocket::request::FlashMessage; 9 | use rocket::response::{Flash, Redirect}; 10 | use rocket::serde::Serialize; 11 | use rocket_auth::{AdminUser, User}; 12 | use rocket_db_pools::Connection; 13 | use rocket_dyn_templates::Template; 14 | 15 | #[derive(Debug, Serialize)] 16 | #[serde(crate = "rocket::serde")] 17 | struct Context { 18 | base_context: BaseContext, 19 | flash: Option<(String, String)>, 20 | admin_settings: AdminSettings, 21 | } 22 | 23 | impl Context { 24 | pub async fn raw( 25 | mut db: Connection, 26 | flash: Option<(String, String)>, 27 | user: User, 28 | admin_user: Option, 29 | ) -> Result { 30 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 31 | .await 32 | .map_err(|_| "failed to get base template.")?; 33 | let admin_settings = AdminSettings::single(&mut db) 34 | .await 35 | .map_err(|_| "failed to get admin settings.")?; 36 | Ok(Context { 37 | base_context, 38 | flash, 39 | admin_settings, 40 | }) 41 | } 42 | } 43 | 44 | #[post("/change", data = "")] 45 | async fn update( 46 | pgp_info_form: Form, 47 | mut db: Connection, 48 | _user: User, 49 | _admin_user: AdminUser, 50 | ) -> Flash { 51 | let pgp_info = pgp_info_form.into_inner(); 52 | 53 | match change_pgp_info(pgp_info, &mut db).await { 54 | Ok(_) => Flash::success( 55 | Redirect::to(uri!("/update_pgp_info", index())), 56 | "PGP info successfully updated.", 57 | ), 58 | Err(e) => Flash::error(Redirect::to(uri!("/update_pgp_info", index())), e), 59 | } 60 | } 61 | 62 | async fn change_pgp_info(pgp_info: PGPInfoInput, db: &mut Connection) -> Result<(), String> { 63 | let new_pgp_key = pgp_info.pgp_key; 64 | let (key, _headers) = 65 | SignedPublicKey::from_string(&new_pgp_key).map_err(|_| "Invalid PGP key input.")?; 66 | let validated_pgp_key = key.to_armored_string(None).unwrap(); 67 | AdminSettings::set_pgp_key(db, &validated_pgp_key) 68 | .await 69 | .map_err(|_| "failed to update PGP key id.")?; 70 | Ok(()) 71 | } 72 | 73 | #[get("/")] 74 | async fn index( 75 | flash: Option>, 76 | db: Connection, 77 | user: User, 78 | admin_user: AdminUser, 79 | ) -> Result { 80 | let flash = flash.map(FlashMessage::into_inner); 81 | let context = Context::raw(db, flash, user, Some(admin_user)) 82 | .await 83 | .map_err(|_| "failed to get template context.")?; 84 | Ok(Template::render("updatepgpinfo", context)) 85 | } 86 | 87 | pub fn update_pgp_info_stage() -> AdHoc { 88 | AdHoc::on_ignite("Update PGP Stage", |rocket| async { 89 | rocket.mount("/update_pgp_info", routes![index, update]) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/seller_history.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{Order, OrderCard, RocketAuthUser}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::serde::Serialize; 7 | use rocket_auth::{AdminUser, User}; 8 | use rocket_db_pools::Connection; 9 | use rocket_dyn_templates::Template; 10 | 11 | const PAGE_SIZE: u32 = 10; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | visited_user: RocketAuthUser, 19 | amount_sold_sat: u64, 20 | weighted_average_rating: f32, 21 | order_cards: Vec, 22 | page_num: u32, 23 | } 24 | 25 | impl Context { 26 | pub async fn raw( 27 | flash: Option<(String, String)>, 28 | mut db: Connection, 29 | maybe_page_num: Option, 30 | visited_user_username: &str, 31 | user: Option, 32 | admin_user: Option, 33 | ) -> Result { 34 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 35 | .await 36 | .map_err(|_| "failed to get base template.")?; 37 | let visited_user = 38 | RocketAuthUser::single_by_username(&mut db, visited_user_username.to_string()) 39 | .await 40 | .map_err(|_| "failed to get visited user.")?; 41 | let page_num = maybe_page_num.unwrap_or(1); 42 | let order_cards = OrderCard::all_received_for_user( 43 | &mut db, 44 | visited_user.id.unwrap(), 45 | PAGE_SIZE, 46 | page_num, 47 | ) 48 | .await 49 | .map_err(|_| "failed to get received orders for user.")?; 50 | let seller_info = Order::seller_info_for_user(&mut db, visited_user.id.unwrap()) 51 | .await 52 | .map_err(|_| "failed to get weighted average rating for user.")?; 53 | let weighted_average_rating = seller_info.weighted_average_rating; 54 | let amount_sold_sat = seller_info.total_amount_sold_sat; 55 | Ok(Context { 56 | base_context, 57 | flash, 58 | visited_user, 59 | amount_sold_sat, 60 | weighted_average_rating, 61 | order_cards, 62 | page_num, 63 | }) 64 | } 65 | } 66 | 67 | #[get("/?")] 68 | async fn index( 69 | username: &str, 70 | flash: Option>, 71 | db: Connection, 72 | page_num: Option, 73 | user: Option, 74 | admin_user: Option, 75 | ) -> Result { 76 | let flash = flash.map(FlashMessage::into_inner); 77 | let context = Context::raw(flash, db, page_num, username, user, admin_user) 78 | .await 79 | .map_err(|_| "failed to get template context.")?; 80 | Ok(Template::render("sellerhistory", context)) 81 | } 82 | 83 | pub fn seller_history_stage() -> AdHoc { 84 | AdHoc::on_ignite("Seller History Stage", |rocket| async { 85 | rocket.mount("/seller_history", routes![index]) 86 | // .mount("/listing", routes![new]) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /src/update_max_allowed_users.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, MaxAllowedUsersInput}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::form::Form; 6 | use rocket::request::FlashMessage; 7 | use rocket::response::{Flash, Redirect}; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::{AdminUser, User}; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | admin_settings: AdminSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let admin_settings = AdminSettings::single(&mut db) 32 | .await 33 | .map_err(|_| "failed to get admin settings.")?; 34 | Ok(Context { 35 | base_context, 36 | flash, 37 | admin_settings, 38 | }) 39 | } 40 | } 41 | 42 | #[post("/change", data = "")] 43 | async fn update( 44 | max_allowed_users_form: Form, 45 | mut db: Connection, 46 | _user: User, 47 | _admin_user: AdminUser, 48 | ) -> Flash { 49 | let max_allowed_users_input = max_allowed_users_form.into_inner(); 50 | let new_max_allowed_users = max_allowed_users_input.max_allowed_users.unwrap_or(0); 51 | 52 | match change_max_allowed_users(new_max_allowed_users, &mut db).await { 53 | Ok(_) => Flash::success( 54 | Redirect::to(uri!("/update_max_allowed_users", index())), 55 | "max allowed users successfully updated.", 56 | ), 57 | Err(e) => Flash::error(Redirect::to(uri!("/update_max_allowed_users", index())), e), 58 | } 59 | } 60 | 61 | async fn change_max_allowed_users( 62 | new_max_allowed_users: u64, 63 | db: &mut Connection, 64 | ) -> Result<(), String> { 65 | AdminSettings::set_max_allowed_users(db, new_max_allowed_users) 66 | .await 67 | .map_err(|_| "failed to update max allowed users.")?; 68 | 69 | Ok(()) 70 | } 71 | 72 | #[get("/")] 73 | async fn index( 74 | flash: Option>, 75 | db: Connection, 76 | user: User, 77 | admin_user: AdminUser, 78 | ) -> Result { 79 | let flash = flash.map(FlashMessage::into_inner); 80 | let context = Context::raw(db, flash, user, Some(admin_user)) 81 | .await 82 | .map_err(|_| "failed to get template context.")?; 83 | Ok(Template::render("updatemaxallowedusers", context)) 84 | } 85 | 86 | pub fn update_max_allowed_users_stage() -> AdHoc { 87 | AdHoc::on_ignite("Update Max Allowed Users Stage", |rocket| async { 88 | rocket 89 | // .mount("/update_listing_images", routes![index, new]) 90 | .mount("/update_max_allowed_users", routes![index, update]) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /src/update_market_name.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, MarketNameInput}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::form::Form; 6 | use rocket::request::FlashMessage; 7 | use rocket::response::{Flash, Redirect}; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::{AdminUser, User}; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | admin_settings: AdminSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let admin_settings = AdminSettings::single(&mut db) 32 | .await 33 | .map_err(|_| "failed to get admin settings.")?; 34 | Ok(Context { 35 | base_context, 36 | flash, 37 | admin_settings, 38 | }) 39 | } 40 | } 41 | 42 | #[post("/change", data = "")] 43 | async fn update( 44 | market_name_form: Form, 45 | mut db: Connection, 46 | _user: User, 47 | _admin_user: AdminUser, 48 | ) -> Flash { 49 | let market_name_input = market_name_form.into_inner(); 50 | let new_market_name = market_name_input.market_name; 51 | 52 | match change_market_name(new_market_name, &mut db).await { 53 | Ok(_) => Flash::success( 54 | Redirect::to(uri!("/update_market_name", index())), 55 | "Market name successfully updated.", 56 | ), 57 | Err(e) => Flash::error(Redirect::to(uri!("/update_market_name", index())), e), 58 | } 59 | } 60 | 61 | async fn change_market_name( 62 | new_market_name: String, 63 | db: &mut Connection, 64 | ) -> Result<(), String> { 65 | if new_market_name.is_empty() { 66 | return Err("Market name cannot be empty.".to_string()); 67 | }; 68 | if new_market_name.len() >= 64 { 69 | return Err("Market name is too long.".to_string()); 70 | }; 71 | 72 | AdminSettings::set_market_name(db, &new_market_name) 73 | .await 74 | .map_err(|_| "failed to update market name.")?; 75 | 76 | Ok(()) 77 | } 78 | 79 | #[get("/")] 80 | async fn index( 81 | flash: Option>, 82 | db: Connection, 83 | user: User, 84 | admin_user: AdminUser, 85 | ) -> Result { 86 | let flash = flash.map(FlashMessage::into_inner); 87 | let context = Context::raw(db, flash, user, Some(admin_user)) 88 | .await 89 | .map_err(|_| "failed to get template context.")?; 90 | Ok(Template::render("updatemarketname", context)) 91 | } 92 | 93 | pub fn update_market_name_stage() -> AdHoc { 94 | AdHoc::on_ignite("Update Market Name Stage", |rocket| async { 95 | rocket.mount("/update_market_name", routes![index, update]) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /src/update_user_bond_price.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, UserBondPriceInput}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::form::Form; 6 | use rocket::request::FlashMessage; 7 | use rocket::response::{Flash, Redirect}; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::{AdminUser, User}; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | admin_settings: AdminSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let admin_settings = AdminSettings::single(&mut db) 32 | .await 33 | .map_err(|_| "failed to get admin settings.")?; 34 | Ok(Context { 35 | base_context, 36 | flash, 37 | admin_settings, 38 | }) 39 | } 40 | } 41 | 42 | #[post("/change", data = "")] 43 | async fn update( 44 | user_bond_price_form: Form, 45 | mut db: Connection, 46 | _user: User, 47 | _admin_user: AdminUser, 48 | ) -> Flash { 49 | let user_bond_price_input = user_bond_price_form.into_inner(); 50 | let new_user_bond_price_basis_points = user_bond_price_input.user_bond_price_sat.unwrap_or(1); 51 | 52 | match change_user_bond_price(new_user_bond_price_basis_points, &mut db).await { 53 | Ok(_) => Flash::success( 54 | Redirect::to(uri!("/update_user_bond_price", index())), 55 | "User bond price successfully updated.", 56 | ), 57 | Err(e) => Flash::error(Redirect::to(uri!("/update_user_bond_price", index())), e), 58 | } 59 | } 60 | 61 | async fn change_user_bond_price( 62 | new_user_bond_price_basis_points: u64, 63 | db: &mut Connection, 64 | ) -> Result<(), String> { 65 | if new_user_bond_price_basis_points == 0 { 66 | return Err("Use bond price must be positive.".to_string()); 67 | }; 68 | 69 | AdminSettings::set_user_bond_price(db, new_user_bond_price_basis_points) 70 | .await 71 | .map_err(|_| "failed to update user bond price.")?; 72 | 73 | Ok(()) 74 | } 75 | 76 | #[get("/")] 77 | async fn index( 78 | flash: Option>, 79 | db: Connection, 80 | user: User, 81 | admin_user: AdminUser, 82 | ) -> Result { 83 | let flash = flash.map(FlashMessage::into_inner); 84 | let context = Context::raw(db, flash, user, Some(admin_user)) 85 | .await 86 | .map_err(|_| "failed to get template context.")?; 87 | Ok(Template::render("updateuserbondprice", context)) 88 | } 89 | 90 | pub fn update_user_bond_price_stage() -> AdHoc { 91 | AdHoc::on_ignite("Update User Bond Price Stage", |rocket| async { 92 | rocket.mount("/update_user_bond_price", routes![index, update]) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/update_user_pgp_info.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{PGPInfoInput, UserSettings}; 4 | use crate::user_account::ActiveUser; 5 | use pgp::Deserializable; 6 | use pgp::SignedPublicKey; 7 | use rocket::fairing::AdHoc; 8 | use rocket::form::Form; 9 | use rocket::request::FlashMessage; 10 | use rocket::response::{Flash, Redirect}; 11 | use rocket::serde::Serialize; 12 | use rocket_auth::{AdminUser, User}; 13 | use rocket_db_pools::Connection; 14 | use rocket_dyn_templates::Template; 15 | 16 | #[derive(Debug, Serialize)] 17 | #[serde(crate = "rocket::serde")] 18 | struct Context { 19 | base_context: BaseContext, 20 | flash: Option<(String, String)>, 21 | user_settings: UserSettings, 22 | } 23 | 24 | impl Context { 25 | pub async fn raw( 26 | mut db: Connection, 27 | flash: Option<(String, String)>, 28 | user: User, 29 | admin_user: Option, 30 | ) -> Result { 31 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 32 | .await 33 | .map_err(|_| "failed to get base template.")?; 34 | let user_settings = UserSettings::single(&mut db, user.id()) 35 | .await 36 | .map_err(|_| "failed to get user settings.")?; 37 | 38 | Ok(Context { 39 | base_context, 40 | flash, 41 | user_settings, 42 | }) 43 | } 44 | } 45 | 46 | #[post("/change", data = "")] 47 | async fn update( 48 | pgp_info_form: Form, 49 | mut db: Connection, 50 | active_user: ActiveUser, 51 | _admin_user: Option, 52 | ) -> Flash { 53 | let pgp_info = pgp_info_form.into_inner(); 54 | 55 | match change_pgp_info(active_user.user, pgp_info, &mut db).await { 56 | Ok(_) => Flash::success( 57 | Redirect::to(uri!("/update_user_pgp_info", index())), 58 | "PGP info successfully updated.", 59 | ), 60 | Err(e) => Flash::error(Redirect::to(uri!("/update_user_pgp_info", index())), e), 61 | } 62 | } 63 | 64 | async fn change_pgp_info( 65 | user: User, 66 | pgp_info: PGPInfoInput, 67 | db: &mut Connection, 68 | ) -> Result<(), String> { 69 | let new_pgp_key = pgp_info.pgp_key; 70 | let (key, _headers) = 71 | SignedPublicKey::from_string(&new_pgp_key).map_err(|_| "Invalid PGP key input.")?; 72 | let validated_pgp_key = key.to_armored_string(None).unwrap(); 73 | UserSettings::set_pgp_key(db, user.id(), &validated_pgp_key) 74 | .await 75 | .map_err(|_| "failed to update pgp key id.")?; 76 | Ok(()) 77 | } 78 | 79 | #[get("/")] 80 | async fn index( 81 | flash: Option>, 82 | db: Connection, 83 | active_user: ActiveUser, 84 | admin_user: Option, 85 | ) -> Result { 86 | let flash = flash.map(FlashMessage::into_inner); 87 | let context = Context::raw(db, flash, active_user.user, admin_user) 88 | .await 89 | .map_err(|_| "failed to get template context.")?; 90 | Ok(Template::render("updateuserpgpinfo", context)) 91 | } 92 | 93 | pub fn update_user_pgp_info_stage() -> AdHoc { 94 | AdHoc::on_ignite("Update User PGP Stage", |rocket| async { 95 | rocket.mount("/update_user_pgp_info", routes![index, update]) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /src/update_fee_rate.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, FeeRateInput}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::form::Form; 6 | use rocket::request::FlashMessage; 7 | use rocket::response::{Flash, Redirect}; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::{AdminUser, User}; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | admin_settings: AdminSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let admin_settings = AdminSettings::single(&mut db) 32 | .await 33 | .map_err(|_| "failed to get admin settings.")?; 34 | Ok(Context { 35 | base_context, 36 | flash, 37 | admin_settings, 38 | }) 39 | } 40 | } 41 | 42 | #[post("/change", data = "")] 43 | async fn update( 44 | fee_rate_form: Form, 45 | mut db: Connection, 46 | _user: User, 47 | _admin_user: AdminUser, 48 | ) -> Flash { 49 | let fee_rate_input = fee_rate_form.into_inner(); 50 | let new_fee_rate_basis_points = fee_rate_input.fee_rate_basis_points.unwrap_or(0); 51 | 52 | match change_fee_rate(new_fee_rate_basis_points, &mut db).await { 53 | Ok(_) => Flash::success( 54 | Redirect::to(uri!("/update_fee_rate", index())), 55 | "Fee rate successfully updated.", 56 | ), 57 | Err(e) => Flash::error(Redirect::to(uri!("/update_fee_rate", index())), e), 58 | } 59 | } 60 | 61 | async fn change_fee_rate( 62 | new_fee_rate_basis_points: i32, 63 | db: &mut Connection, 64 | ) -> Result<(), String> { 65 | if new_fee_rate_basis_points < 0 { 66 | return Err("Fee rate cannot be negative.".to_string()); 67 | }; 68 | if new_fee_rate_basis_points > 10000 { 69 | return Err("Fee rate basis points cannot be > 10000.".to_string()); 70 | }; 71 | 72 | AdminSettings::set_fee_rate(db, new_fee_rate_basis_points) 73 | .await 74 | .map_err(|_| "failed to update fee rate.")?; 75 | 76 | Ok(()) 77 | } 78 | 79 | #[get("/")] 80 | async fn index( 81 | flash: Option>, 82 | db: Connection, 83 | user: User, 84 | admin_user: AdminUser, 85 | ) -> Result { 86 | let flash = flash.map(FlashMessage::into_inner); 87 | let context = Context::raw(db, flash, user, Some(admin_user)) 88 | .await 89 | .map_err(|_| "failed to get template context.")?; 90 | Ok(Template::render("updatefeerate", context)) 91 | } 92 | 93 | pub fn update_fee_rate_stage() -> AdHoc { 94 | AdHoc::on_ignite("Update Fee Rate Stage", |rocket| async { 95 | rocket 96 | // .mount("/update_listing_images", routes![index, new]) 97 | .mount("/update_fee_rate", routes![index, update]) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /src/payment_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::lightning::get_lnd_lightning_client; 3 | use crate::models::{Order, UserAccount}; 4 | use crate::util; 5 | use sqlx::pool::PoolConnection; 6 | use sqlx::Sqlite; 7 | use tonic_openssl_lnd::LndLightningClient; 8 | 9 | pub async fn handle_received_payments( 10 | config: Config, 11 | mut conn: PoolConnection, 12 | ) -> Result<(), String> { 13 | let mut lightning_client = get_lnd_lightning_client( 14 | config.lnd_host.clone(), 15 | config.lnd_port, 16 | config.lnd_tls_cert_path.clone(), 17 | config.lnd_macaroon_path.clone(), 18 | ) 19 | .await 20 | .map_err(|e| format!("failed to get lightning client: {:?}", e))?; 21 | 22 | let latest_settle_index = get_latest_settle_index(&mut conn, &mut lightning_client).await?; 23 | 24 | println!( 25 | "Starting subscribe invoices with latest settle index: {:?}", 26 | latest_settle_index 27 | ); 28 | let invoice_subscription = tonic_openssl_lnd::lnrpc::InvoiceSubscription { 29 | settle_index: latest_settle_index, 30 | ..Default::default() 31 | }; 32 | let mut update_stream = lightning_client 33 | .subscribe_invoices(invoice_subscription) 34 | .await 35 | .map_err(|_| "Failed to call subscribe invoices.")? 36 | .into_inner(); 37 | while let Ok(Some(invoice)) = update_stream.message().await { 38 | #[allow(deprecated)] 39 | if invoice.settled { 40 | println!("Handling settled invoice: {:?}", invoice); 41 | let invoice_hash = util::to_hex(&invoice.r_hash); 42 | handle_payment(&mut conn, &invoice_hash).await?; 43 | } 44 | } 45 | Ok(()) 46 | } 47 | 48 | async fn handle_payment( 49 | conn: &mut PoolConnection, 50 | invoice_hash: &str, 51 | ) -> Result<(), String> { 52 | let now = util::current_time_millis(); 53 | 54 | let maybe_order = Order::single_by_invoice_hash(conn, invoice_hash).await.ok(); 55 | if let Some(order) = maybe_order { 56 | Order::mark_as_paid(conn, order.id.unwrap(), now) 57 | .await 58 | .map_err(|_| "failed to mark order as paid.")?; 59 | } 60 | 61 | let maybe_user_account = UserAccount::single_by_invoice_hash(conn, invoice_hash) 62 | .await 63 | .ok(); 64 | if let Some(user_account) = maybe_user_account { 65 | UserAccount::mark_as_paid(conn, user_account.id.unwrap(), now) 66 | .await 67 | .map_err(|_| "failed to mark user account as paid.")?; 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | async fn get_latest_settle_index( 74 | conn: &mut PoolConnection, 75 | lightning_client: &mut LndLightningClient, 76 | ) -> Result { 77 | // Get latest paid invoice if exists. 78 | let latest_paid_order = Order::most_recent_paid_order(conn) 79 | .await 80 | .map_err(|_| "failed to latest paid order.")?; 81 | 82 | let settle_index: u64 = if let Some(latest_invoice_hash) = latest_paid_order { 83 | let latest_paid_order_invoice = lightning_client 84 | .lookup_invoice(tonic_openssl_lnd::lnrpc::PaymentHash { 85 | r_hash: util::from_hex(&latest_invoice_hash), 86 | ..Default::default() 87 | }) 88 | .await 89 | .map_err(|e| format!("Failed to lookup invoice: {:?}", e))? 90 | .into_inner(); 91 | latest_paid_order_invoice.settle_index 92 | } else { 93 | 0 94 | }; 95 | 96 | Ok(settle_index) 97 | } 98 | -------------------------------------------------------------------------------- /db/migrations/20220729073857_initial_migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE listings ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | public_id VARCHAR NOT NULL, 4 | user_id INTEGER NOT NULL, 5 | title VARCHAR NOT NULL, 6 | description VARCHAR NOT NULL, 7 | price_sat UNSIGNED BIG INT NOT NULL, 8 | fee_rate_basis_points INTEGER NOT NULL, 9 | reviewed BOOLEAN NOT NULL, 10 | submitted BOOLEAN NOT NULL, 11 | approved BOOLEAN NOT NULL, 12 | deactivated_by_seller boolean NOT NULL, 13 | deactivated_by_admin boolean NOT NULL, 14 | created_time_ms UNSIGNED BIG INT NOT NULL 15 | ); 16 | 17 | CREATE TABLE listingimages ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT, 19 | public_id VARCHAR NOT NULL, 20 | listing_id INTEGER NOT NULL, 21 | image_data BLOB NOT NULL, 22 | is_primary BOOLEAN NOT NULL 23 | ); 24 | 25 | CREATE TABLE shippingoptions ( 26 | id INTEGER PRIMARY KEY AUTOINCREMENT, 27 | public_id VARCHAR NOT NULL, 28 | listing_id INTEGER NOT NULL, 29 | title VARCHAR NOT NULL, 30 | description VARCHAR NOT NULL, 31 | price_sat UNSIGNED BIG INT NOT NULL 32 | ); 33 | 34 | CREATE TABLE orders ( 35 | id INTEGER PRIMARY KEY AUTOINCREMENT, 36 | public_id VARCHAR NOT NULL, 37 | buyer_user_id INTEGER NOT NULL, 38 | seller_user_id INTEGER NOT NULL, 39 | quantity INTEGER NOT NULL, 40 | listing_id INTEGER NOT NULL, 41 | shipping_option_id INTEGER NOT NULL, 42 | shipping_instructions VARCHAR NOT NULL, 43 | amount_owed_sat UNSIGNED BIG INT NOT NULL, 44 | seller_credit_sat UNSIGNED BIG INT NOT NULL, 45 | paid BOOLEAN NOT NULL, 46 | shipped BOOLEAN NOT NULL, 47 | canceled_by_seller boolean NOT NULL, 48 | canceled_by_buyer boolean NOT NULL, 49 | invoice_payment_request VARCHAR NOT NULL, 50 | invoice_hash VARCHAR NOT NULL, 51 | review_text VARCHAR NOT NULL, 52 | review_rating INTEGER NOT NULL, 53 | reviewed BOOLEAN NOT NULL, 54 | created_time_ms UNSIGNED BIG INT NOT NULL, 55 | review_time_ms UNSIGNED BIG INT NOT NULL, 56 | payment_time_ms UNSIGNED BIG INT NOT NULL 57 | ); 58 | 59 | CREATE TABLE withdrawals ( 60 | id INTEGER PRIMARY KEY AUTOINCREMENT, 61 | public_id VARCHAR NOT NULL, 62 | user_id INTEGER NOT NULL, 63 | amount_sat UNSIGNED BIG INT NOT NULL, 64 | created_time_ms UNSIGNED BIG INT NOT NULL, 65 | invoice_hash VARCHAR NOT NULL, 66 | invoice_payment_request VARCHAR NOT NULL 67 | ); 68 | 69 | CREATE TABLE useraccounts ( 70 | id INTEGER PRIMARY KEY AUTOINCREMENT, 71 | public_id VARCHAR NOT NULL, 72 | user_id Integer NOT NULL, 73 | amount_owed_sat UNSIGNED BIG INT NOT NULL, 74 | paid BOOLEAN NOT NULL, 75 | disabled boolean NOT NULL, 76 | invoice_payment_request VARCHAR NOT NULL, 77 | invoice_hash VARCHAR NOT NULL, 78 | created_time_ms UNSIGNED BIG INT NOT NULL, 79 | payment_time_ms UNSIGNED BIG INT NOT NULL 80 | ); 81 | 82 | CREATE TABLE usersettings ( 83 | id INTEGER PRIMARY KEY AUTOINCREMENT, 84 | user_id INTEGER NOT NULL, 85 | pgp_key VARCHAR NOT NULL, 86 | squeaknode_pubkey VARCHAR NOT NULL, 87 | squeaknode_address VARCHAR NOT NULL 88 | ); 89 | 90 | CREATE TABLE adminsettings ( 91 | id INTEGER PRIMARY KEY AUTOINCREMENT, 92 | market_name VARCHAR NOT NULL, 93 | fee_rate_basis_points INTEGER NOT NULL, 94 | user_bond_price_sat UNSIGNED BIG INT NOT NULL, 95 | pgp_key VARCHAR NOT NULL, 96 | squeaknode_address VARCHAR NOT NULL, 97 | squeaknode_pubkey VARCHAR NOT NULL, 98 | max_allowed_users UNSIGNED BIG INT NOT NULL 99 | ); 100 | 101 | -------------------------------------------------------------------------------- /src/update_squeaknode_info.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{AdminSettings, SqueaknodeInfoInput}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::form::Form; 6 | use rocket::request::FlashMessage; 7 | use rocket::response::{Flash, Redirect}; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::{AdminUser, User}; 10 | use rocket_db_pools::Connection; 11 | use rocket_dyn_templates::Template; 12 | 13 | #[derive(Debug, Serialize)] 14 | #[serde(crate = "rocket::serde")] 15 | struct Context { 16 | base_context: BaseContext, 17 | flash: Option<(String, String)>, 18 | admin_settings: AdminSettings, 19 | } 20 | 21 | impl Context { 22 | pub async fn raw( 23 | mut db: Connection, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let admin_settings = AdminSettings::single(&mut db) 32 | .await 33 | .map_err(|_| "failed to get admin settings.")?; 34 | Ok(Context { 35 | base_context, 36 | flash, 37 | admin_settings, 38 | }) 39 | } 40 | } 41 | 42 | #[post("/change", data = "")] 43 | async fn update( 44 | squeaknode_info_form: Form, 45 | mut db: Connection, 46 | _user: User, 47 | _admin_user: AdminUser, 48 | ) -> Flash { 49 | let squeaknode_info = squeaknode_info_form.into_inner(); 50 | 51 | match change_squeaknode_info(squeaknode_info, &mut db).await { 52 | Ok(_) => Flash::success( 53 | Redirect::to(uri!("/update_squeaknode_info", index())), 54 | "Squeaknode info successfully updated.", 55 | ), 56 | Err(e) => Flash::error(Redirect::to(uri!("/update_squeaknode_info", index())), e), 57 | } 58 | } 59 | 60 | async fn change_squeaknode_info( 61 | squeaknode_info: SqueaknodeInfoInput, 62 | db: &mut Connection, 63 | ) -> Result<(), String> { 64 | let new_squeaknode_pubkey = squeaknode_info.squeaknode_pubkey; 65 | let new_squeaknode_address = squeaknode_info.squeaknode_address; 66 | 67 | if new_squeaknode_pubkey.len() != 64 { 68 | return Err("Pubkey is not valid.".to_string()); 69 | }; 70 | if new_squeaknode_address.len() > 128 { 71 | return Err("Address is too long.".to_string()); 72 | }; 73 | 74 | AdminSettings::set_squeaknode_pubkey(db, &new_squeaknode_pubkey) 75 | .await 76 | .map_err(|_| "failed to update squeaknode pubkey.")?; 77 | AdminSettings::set_squeaknode_address(db, &new_squeaknode_address) 78 | .await 79 | .map_err(|_| "failed to update squeaknode address.")?; 80 | 81 | Ok(()) 82 | } 83 | 84 | #[get("/")] 85 | async fn index( 86 | flash: Option>, 87 | db: Connection, 88 | user: User, 89 | admin_user: AdminUser, 90 | ) -> Result { 91 | let flash = flash.map(FlashMessage::into_inner); 92 | let context = Context::raw(db, flash, user, Some(admin_user)) 93 | .await 94 | .map_err(|_| "failed to get template context.")?; 95 | Ok(Template::render("updatesqueaknodeinfo", context)) 96 | } 97 | 98 | pub fn update_squeaknode_info_stage() -> AdHoc { 99 | AdHoc::on_ignite("Update Squeaknode Stage", |rocket| async { 100 | rocket.mount("/update_squeaknode_info", routes![index, update]) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /src/delete_listing.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{Listing, ListingDisplay}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::response::{Flash, Redirect}; 7 | use rocket::serde::Serialize; 8 | use rocket_auth::{AdminUser, User}; 9 | use rocket_db_pools::Connection; 10 | use rocket_dyn_templates::Template; 11 | 12 | #[derive(Debug, Serialize)] 13 | #[serde(crate = "rocket::serde")] 14 | struct Context { 15 | base_context: BaseContext, 16 | flash: Option<(String, String)>, 17 | listing_display: Option, 18 | } 19 | 20 | impl Context { 21 | pub async fn raw( 22 | mut db: Connection, 23 | listing_id: &str, 24 | flash: Option<(String, String)>, 25 | user: User, 26 | admin_user: Option, 27 | ) -> Result { 28 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 29 | .await 30 | .map_err(|_| "failed to get base template.")?; 31 | let listing_display = ListingDisplay::single_by_public_id(&mut db, listing_id) 32 | .await 33 | .map_err(|_| "failed to get listing display.")?; 34 | 35 | if listing_display.listing.user_id != user.id() && admin_user.is_none() { 36 | return Err("User does not have permission to delete listing.".to_string()); 37 | }; 38 | 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | listing_display: Some(listing_display), 43 | }) 44 | } 45 | } 46 | 47 | #[delete("/")] 48 | async fn delete( 49 | id: &str, 50 | mut db: Connection, 51 | user: User, 52 | admin_user: Option, 53 | ) -> Result, Flash> { 54 | match delete_listing(id, &mut db, user.clone(), admin_user.clone()).await { 55 | Ok(_) => Ok(Flash::success( 56 | Redirect::to(uri!("/")), 57 | "Listing was deleted.", 58 | )), 59 | Err(e) => { 60 | error_!("DB deletion({}) error: {}", id, e); 61 | Err(Flash::error( 62 | Redirect::to(uri!("/update_listing_images", index(id))), 63 | "Failed to delete listing image.", 64 | )) 65 | } 66 | } 67 | } 68 | 69 | async fn delete_listing( 70 | listing_id: &str, 71 | db: &mut Connection, 72 | user: User, 73 | admin_user: Option, 74 | ) -> Result<(), String> { 75 | let listing = Listing::single_by_public_id(&mut *db, listing_id) 76 | .await 77 | .map_err(|_| "failed to get listing")?; 78 | 79 | if listing.user_id != user.id() && admin_user.is_none() { 80 | return Err("User does not have permission to delete listing.".to_string()); 81 | }; 82 | 83 | Listing::delete(listing.id.unwrap(), &mut *db) 84 | .await 85 | .map_err(|_| "failed to delete listing.".to_string())?; 86 | 87 | Ok(()) 88 | } 89 | 90 | #[get("/")] 91 | async fn index( 92 | flash: Option>, 93 | id: &str, 94 | db: Connection, 95 | user: User, 96 | admin_user: Option, 97 | ) -> Result { 98 | let flash = flash.map(FlashMessage::into_inner); 99 | let context = Context::raw(db, id, flash, user, admin_user) 100 | .await 101 | .map_err(|_| "failed to get template context.")?; 102 | Ok(Template::render("deletelisting", context)) 103 | } 104 | 105 | pub fn delete_listing_stage() -> AdHoc { 106 | AdHoc::on_ignite("Delete Listing Stage", |rocket| async { 107 | rocket.mount("/delete_listing", routes![index, delete]) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /templates/base.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 42 | 43 | {{ base_context.admin_settings.market_name }} - Squeak Road 44 | 45 | 46 | 47 | 48 |
    49 | 84 | 85 | 86 |
    87 | 89 |
    90 | 93 |
    94 |
    95 |
    96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/update_user_squeaknode_info.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{SqueaknodeInfoInput, UserSettings}; 4 | use crate::user_account::ActiveUser; 5 | use rocket::fairing::AdHoc; 6 | use rocket::form::Form; 7 | use rocket::request::FlashMessage; 8 | use rocket::response::{Flash, Redirect}; 9 | use rocket::serde::Serialize; 10 | use rocket_auth::{AdminUser, User}; 11 | use rocket_db_pools::Connection; 12 | use rocket_dyn_templates::Template; 13 | 14 | #[derive(Debug, Serialize)] 15 | #[serde(crate = "rocket::serde")] 16 | struct Context { 17 | base_context: BaseContext, 18 | flash: Option<(String, String)>, 19 | user_settings: UserSettings, 20 | } 21 | 22 | impl Context { 23 | pub async fn raw( 24 | mut db: Connection, 25 | flash: Option<(String, String)>, 26 | user: User, 27 | admin_user: Option, 28 | ) -> Result { 29 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 30 | .await 31 | .map_err(|_| "failed to get base template.")?; 32 | let user_settings = UserSettings::single(&mut db, user.id()) 33 | .await 34 | .map_err(|_| "failed to get user settings.")?; 35 | 36 | Ok(Context { 37 | base_context, 38 | flash, 39 | user_settings, 40 | }) 41 | } 42 | } 43 | 44 | #[post("/change", data = "")] 45 | async fn update( 46 | squeaknode_info_form: Form, 47 | mut db: Connection, 48 | active_user: ActiveUser, 49 | _admin_user: Option, 50 | ) -> Flash { 51 | let squeaknode_info = squeaknode_info_form.into_inner(); 52 | 53 | match change_squeaknode_info(active_user.user, squeaknode_info, &mut db).await { 54 | Ok(_) => Flash::success( 55 | Redirect::to(uri!("/update_user_squeaknode_info", index())), 56 | "Squeaknode info successfully updated.", 57 | ), 58 | Err(e) => Flash::error( 59 | Redirect::to(uri!("/update_user_squeaknode_info", index())), 60 | e, 61 | ), 62 | } 63 | } 64 | 65 | async fn change_squeaknode_info( 66 | user: User, 67 | squeaknode_info: SqueaknodeInfoInput, 68 | db: &mut Connection, 69 | ) -> Result<(), String> { 70 | let new_squeaknode_pubkey = squeaknode_info.squeaknode_pubkey; 71 | let new_squeaknode_address = squeaknode_info.squeaknode_address; 72 | 73 | if new_squeaknode_pubkey.len() != 64 { 74 | return Err("Pubkey is not valid.".to_string()); 75 | }; 76 | if new_squeaknode_address.len() > 128 { 77 | return Err("Address is too long.".to_string()); 78 | }; 79 | 80 | UserSettings::set_squeaknode_pubkey(db, user.id(), &new_squeaknode_pubkey) 81 | .await 82 | .map_err(|_| "failed to update squeaknode pubkey.")?; 83 | UserSettings::set_squeaknode_address(db, user.id(), &new_squeaknode_address) 84 | .await 85 | .map_err(|_| "failed to update squeaknode address.")?; 86 | 87 | Ok(()) 88 | } 89 | 90 | #[get("/")] 91 | async fn index( 92 | flash: Option>, 93 | db: Connection, 94 | active_user: ActiveUser, 95 | admin_user: Option, 96 | ) -> Result { 97 | let flash = flash.map(FlashMessage::into_inner); 98 | let context = Context::raw(db, flash, active_user.user, admin_user) 99 | .await 100 | .map_err(|_| "failed to get template context.")?; 101 | Ok(Template::render("updateusersqueaknodeinfo", context)) 102 | } 103 | 104 | pub fn update_user_squeaknode_info_stage() -> AdHoc { 105 | AdHoc::on_ignite("Update User Squeaknode Stage", |rocket| async { 106 | rocket.mount("/update_user_squeaknode_info", routes![index, update]) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /src/account_activation.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::config::Config; 3 | use crate::db::Db; 4 | use crate::lightning; 5 | use crate::models::UserAccount; 6 | use crate::util; 7 | use rocket::fairing::AdHoc; 8 | use rocket::request::FlashMessage; 9 | use rocket::response::Redirect; 10 | use rocket::serde::Serialize; 11 | use rocket::State; 12 | use rocket_auth::AdminUser; 13 | use rocket_auth::User; 14 | use rocket_auth::Users; 15 | use rocket_db_pools::Connection; 16 | use rocket_dyn_templates::Template; 17 | 18 | #[derive(Debug, Serialize)] 19 | #[serde(crate = "rocket::serde")] 20 | struct Context { 21 | base_context: BaseContext, 22 | flash: Option<(String, String)>, 23 | user_account: UserAccount, 24 | maybe_account_user: Option, 25 | user: User, 26 | admin_user: Option, 27 | qr_svg_base64: String, 28 | lightning_node_pubkey: String, 29 | } 30 | 31 | impl Context { 32 | pub async fn raw( 33 | mut db: Connection, 34 | flash: Option<(String, String)>, 35 | id: &str, 36 | user: User, 37 | admin_user: Option, 38 | config: &Config, 39 | users: &Users, 40 | ) -> Result { 41 | let base_context = BaseContext::raw(&mut db, Some(user.clone()), admin_user.clone()) 42 | .await 43 | .map_err(|_| "failed to get base template.")?; 44 | let user_account = UserAccount::single_by_public_id(&mut db, id) 45 | .await 46 | .map_err(|_| "failed to get user account.")?; 47 | let maybe_account_user = users.get_by_id(user_account.user_id).await.ok(); 48 | let qr_svg_bytes = util::generate_qr(&user_account.invoice_payment_request); 49 | let qr_svg_base64 = util::to_base64(&qr_svg_bytes); 50 | let lightning_node_pubkey = get_lightning_node_pubkey(config) 51 | .await 52 | .unwrap_or_else(|_| "".to_string()); 53 | Ok(Context { 54 | base_context, 55 | flash, 56 | user_account, 57 | maybe_account_user, 58 | user, 59 | admin_user, 60 | qr_svg_base64, 61 | lightning_node_pubkey, 62 | }) 63 | } 64 | } 65 | 66 | async fn get_lightning_node_pubkey(config: &Config) -> Result { 67 | let mut lightning_client = lightning::get_lnd_lightning_client( 68 | config.lnd_host.clone(), 69 | config.lnd_port, 70 | config.lnd_tls_cert_path.clone(), 71 | config.lnd_macaroon_path.clone(), 72 | ) 73 | .await 74 | .expect("failed to get lightning client"); 75 | let get_info_resp = lightning_client 76 | // All calls require at least empty parameter 77 | .get_info(tonic_openssl_lnd::lnrpc::GetInfoRequest {}) 78 | .await 79 | .expect("failed to get lightning node info") 80 | .into_inner(); 81 | Ok(get_info_resp.identity_pubkey) 82 | } 83 | 84 | #[get("/")] 85 | async fn index( 86 | flash: Option>, 87 | id: &str, 88 | db: Connection, 89 | user: Option, 90 | admin_user: Option, 91 | config: &State, 92 | users: &State, 93 | ) -> Result { 94 | let flash = flash.map(FlashMessage::into_inner); 95 | match user { 96 | Some(user) => { 97 | let context = Context::raw(db, flash, id, user, admin_user, config, users) 98 | .await 99 | .map_err(|_| Redirect::to(uri!("/login")))?; 100 | Ok(Template::render("accountactivation", context)) 101 | } 102 | None => Err(Redirect::to(uri!("/login"))), 103 | } 104 | } 105 | 106 | pub fn account_activation_stage() -> AdHoc { 107 | AdHoc::on_ignite("Account Activation Stage", |rocket| async { 108 | rocket.mount("/account_activation", routes![index]) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /static/css/fonts.css: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Raleway'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: url(./fonts/1Ptug8zYS_SKggPNyCAIT5lu.woff2) format('woff2'); 7 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 8 | } 9 | /* cyrillic */ 10 | @font-face { 11 | font-family: 'Raleway'; 12 | font-style: normal; 13 | font-weight: 300; 14 | src: url(./fonts/1Ptug8zYS_SKggPNyCkIT5lu.woff2) format('woff2'); 15 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 16 | } 17 | /* vietnamese */ 18 | @font-face { 19 | font-family: 'Raleway'; 20 | font-style: normal; 21 | font-weight: 300; 22 | src: url(./fonts/1Ptug8zYS_SKggPNyCIIT5lu.woff2) format('woff2'); 23 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 24 | } 25 | /* latin-ext */ 26 | @font-face { 27 | font-family: 'Raleway'; 28 | font-style: normal; 29 | font-weight: 300; 30 | src: url(./fonts/1Ptug8zYS_SKggPNyCMIT5lu.woff2) format('woff2'); 31 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 32 | } 33 | /* latin */ 34 | @font-face { 35 | font-family: 'Raleway'; 36 | font-style: normal; 37 | font-weight: 300; 38 | src: url(./fonts/1Ptug8zYS_SKggPNyC0ITw.woff2) format('woff2'); 39 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 40 | } 41 | /* cyrillic-ext */ 42 | @font-face { 43 | font-family: 'Raleway'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: url(./fonts/1Ptug8zYS_SKggPNyCAIT5lu.woff2) format('woff2'); 47 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 48 | } 49 | /* cyrillic */ 50 | @font-face { 51 | font-family: 'Raleway'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: url(./fonts/1Ptug8zYS_SKggPNyCkIT5lu.woff2) format('woff2'); 55 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 56 | } 57 | /* vietnamese */ 58 | @font-face { 59 | font-family: 'Raleway'; 60 | font-style: normal; 61 | font-weight: 400; 62 | src: url(./fonts/1Ptug8zYS_SKggPNyCIIT5lu.woff2) format('woff2'); 63 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 64 | } 65 | /* latin-ext */ 66 | @font-face { 67 | font-family: 'Raleway'; 68 | font-style: normal; 69 | font-weight: 400; 70 | src: url(./fonts/1Ptug8zYS_SKggPNyCMIT5lu.woff2) format('woff2'); 71 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 72 | } 73 | /* latin */ 74 | @font-face { 75 | font-family: 'Raleway'; 76 | font-style: normal; 77 | font-weight: 400; 78 | src: url(./fonts/1Ptug8zYS_SKggPNyC0ITw.woff2) format('woff2'); 79 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 80 | } 81 | /* cyrillic-ext */ 82 | @font-face { 83 | font-family: 'Raleway'; 84 | font-style: normal; 85 | font-weight: 600; 86 | src: url(./fonts/1Ptug8zYS_SKggPNyCAIT5lu.woff2) format('woff2'); 87 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 88 | } 89 | /* cyrillic */ 90 | @font-face { 91 | font-family: 'Raleway'; 92 | font-style: normal; 93 | font-weight: 600; 94 | src: url(./fonts/1Ptug8zYS_SKggPNyCkIT5lu.woff2) format('woff2'); 95 | unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 96 | } 97 | /* vietnamese */ 98 | @font-face { 99 | font-family: 'Raleway'; 100 | font-style: normal; 101 | font-weight: 600; 102 | src: url(./fonts/1Ptug8zYS_SKggPNyCIIT5lu.woff2) format('woff2'); 103 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 104 | } 105 | /* latin-ext */ 106 | @font-face { 107 | font-family: 'Raleway'; 108 | font-style: normal; 109 | font-weight: 600; 110 | src: url(./fonts/1Ptug8zYS_SKggPNyCMIT5lu.woff2) format('woff2'); 111 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 112 | } 113 | /* latin */ 114 | @font-face { 115 | font-family: 'Raleway'; 116 | font-style: normal; 117 | font-weight: 600; 118 | src: url(./fonts/1Ptug8zYS_SKggPNyC0ITw.woff2) format('woff2'); 119 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 120 | } 121 | -------------------------------------------------------------------------------- /src/new_listing.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::AdminSettings; 4 | use crate::models::{InitialListingInfo, Listing}; 5 | use crate::user_account::ActiveUser; 6 | use crate::util; 7 | use rocket::fairing::AdHoc; 8 | use rocket::form::Form; 9 | use rocket::request::FlashMessage; 10 | use rocket::response::{Flash, Redirect}; 11 | use rocket::serde::Serialize; 12 | use rocket_auth::{AdminUser, User}; 13 | use rocket_db_pools::Connection; 14 | use rocket_dyn_templates::Template; 15 | 16 | const MAX_UNAPPROVED_LISTINGS: u32 = 5; 17 | 18 | #[derive(Debug, Serialize)] 19 | #[serde(crate = "rocket::serde")] 20 | struct Context { 21 | base_context: BaseContext, 22 | flash: Option<(String, String)>, 23 | admin_settings: AdminSettings, 24 | } 25 | 26 | impl Context { 27 | pub async fn raw( 28 | flash: Option<(String, String)>, 29 | mut db: Connection, 30 | user: Option, 31 | admin_user: Option, 32 | ) -> Result { 33 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 34 | .await 35 | .map_err(|_| "failed to get base template.")?; 36 | let admin_settings = AdminSettings::single(&mut db) 37 | .await 38 | .map_err(|_| "failed to update market name.")?; 39 | Ok(Context { 40 | base_context, 41 | flash, 42 | admin_settings, 43 | }) 44 | } 45 | } 46 | 47 | #[post("/", data = "")] 48 | async fn new( 49 | listing_form: Form, 50 | mut db: Connection, 51 | active_user: ActiveUser, 52 | ) -> Result, Flash> { 53 | let listing_info = listing_form.into_inner(); 54 | 55 | match create_listing(listing_info, &mut db, active_user.user.clone()).await { 56 | Ok(listing_id) => Ok(Flash::success( 57 | Redirect::to(format!("/{}/{}", "listing", listing_id)), 58 | "Listing successfully added.", 59 | )), 60 | Err(e) => { 61 | error_!("DB insertion error: {}", e); 62 | Err(Flash::error(Redirect::to(uri!("/new_listing", index())), e)) 63 | } 64 | } 65 | } 66 | 67 | async fn create_listing( 68 | listing_info: InitialListingInfo, 69 | db: &mut Connection, 70 | user: User, 71 | ) -> Result { 72 | let admin_settings = AdminSettings::single(db) 73 | .await 74 | .map_err(|_| "failed to update market name.")?; 75 | let now = util::current_time_millis(); 76 | 77 | let price_sat = listing_info.price_sat.unwrap_or(0); 78 | 79 | if listing_info.title.is_empty() { 80 | return Err("Title cannot be empty.".to_string()); 81 | }; 82 | if listing_info.description.is_empty() { 83 | return Err("Description cannot be empty.".to_string()); 84 | }; 85 | if listing_info.title.len() > 64 { 86 | return Err("Title length is too long.".to_string()); 87 | }; 88 | if listing_info.description.len() > 4096 { 89 | return Err("Description length is too long.".to_string()); 90 | }; 91 | if price_sat == 0 { 92 | return Err("Price must be a positive number.".to_string()); 93 | }; 94 | if user.is_admin { 95 | return Err("Admin user cannot create a listing.".to_string()); 96 | }; 97 | 98 | let listing = Listing { 99 | id: None, 100 | public_id: util::create_uuid(), 101 | user_id: user.id(), 102 | title: listing_info.title, 103 | description: listing_info.description, 104 | price_sat, 105 | fee_rate_basis_points: admin_settings.fee_rate_basis_points, 106 | submitted: false, 107 | reviewed: false, 108 | approved: false, 109 | deactivated_by_seller: false, 110 | deactivated_by_admin: false, 111 | created_time_ms: now, 112 | }; 113 | match Listing::insert(listing, MAX_UNAPPROVED_LISTINGS, db).await { 114 | Ok(listing_id) => match Listing::single(db, listing_id).await { 115 | Ok(new_listing) => Ok(new_listing.public_id), 116 | Err(e) => { 117 | error_!("DB insertion error: {}", e); 118 | Err("New listing could not be found after inserting.".to_string()) 119 | } 120 | }, 121 | Err(e) => { 122 | error_!("DB insertion error: {}", e); 123 | Err(e) 124 | } 125 | } 126 | } 127 | 128 | #[get("/")] 129 | async fn index( 130 | flash: Option>, 131 | db: Connection, 132 | active_user: ActiveUser, 133 | admin_user: Option, 134 | ) -> Result { 135 | let flash = flash.map(FlashMessage::into_inner); 136 | let context = Context::raw(flash, db, Some(active_user.user), admin_user) 137 | .await 138 | .map_err(|_| "failed to get template context.")?; 139 | Ok(Template::render("newlisting", context)) 140 | } 141 | 142 | pub fn new_listing_stage() -> AdHoc { 143 | AdHoc::on_ignite("New Listing Stage", |rocket| async { 144 | rocket.mount("/new_listing", routes![index, new]) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /static/css/skeleton.min.css: -------------------------------------------------------------------------------- 1 | .container{position:relative;width:100%;max-width:960px;margin:0 auto;padding:0 20px;box-sizing:border-box}.column,.columns{width:100%;float:left;box-sizing:border-box}@media (min-width:400px){.container{width:85%;padding:0}}@media (min-width:550px){.container{width:80%}.column,.columns{margin-left:4%}.column:first-child,.columns:first-child{margin-left:0}.one.column,.one.columns{width:4.66666666667%}.two.columns{width:13.3333333333%}.three.columns{width:22%}.four.columns{width:30.6666666667%}.five.columns{width:39.3333333333%}.six.columns{width:48%}.seven.columns{width:56.6666666667%}.eight.columns{width:65.3333333333%}.nine.columns{width:74%}.ten.columns{width:82.6666666667%}.eleven.columns{width:91.3333333333%}.twelve.columns{width:100%;margin-left:0}.one-third.column{width:30.6666666667%}.two-thirds.column{width:65.3333333333%}.one-half.column{width:48%}.offset-by-one.column,.offset-by-one.columns{margin-left:8.66666666667%}.offset-by-two.column,.offset-by-two.columns{margin-left:17.3333333333%}.offset-by-three.column,.offset-by-three.columns{margin-left:26%}.offset-by-four.column,.offset-by-four.columns{margin-left:34.6666666667%}.offset-by-five.column,.offset-by-five.columns{margin-left:43.3333333333%}.offset-by-six.column,.offset-by-six.columns{margin-left:52%}.offset-by-seven.column,.offset-by-seven.columns{margin-left:60.6666666667%}.offset-by-eight.column,.offset-by-eight.columns{margin-left:69.3333333333%}.offset-by-nine.column,.offset-by-nine.columns{margin-left:78%}.offset-by-ten.column,.offset-by-ten.columns{margin-left:86.6666666667%}.offset-by-eleven.column,.offset-by-eleven.columns{margin-left:95.3333333333%}.offset-by-one-third.column,.offset-by-one-third.columns{margin-left:34.6666666667%}.offset-by-two-thirds.column,.offset-by-two-thirds.columns{margin-left:69.3333333333%}.offset-by-one-half.column,.offset-by-one-half.columns{margin-left:52%}}html{font-size:62.5%}body{font-size:1.5em;line-height:1.6;font-weight:400;font-family:Raleway,HelveticaNeue,"Helvetica Neue",Helvetica,Arial,sans-serif;color:#222}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:2rem;font-weight:300}h1{font-size:4rem;line-height:1.2;letter-spacing:-.1rem}h2{font-size:3.6rem;line-height:1.25;letter-spacing:-.1rem}h3{font-size:3rem;line-height:1.3;letter-spacing:-.1rem}h4{font-size:2.4rem;line-height:1.35;letter-spacing:-.08rem}h5{font-size:1.8rem;line-height:1.5;letter-spacing:-.05rem}h6{font-size:1.5rem;line-height:1.6;letter-spacing:0}@media (min-width:550px){h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}p{margin-top:0}a{color:#1EAEDB}a:hover{color:#0FA0CE}.button,button,input[type=button],input[type=reset],input[type=submit]{display:inline-block;height:38px;padding:0 30px;color:#555;text-align:center;font-size:11px;font-weight:600;line-height:38px;letter-spacing:.1rem;text-transform:uppercase;text-decoration:none;white-space:nowrap;background-color:transparent;border-radius:4px;border:1px solid #bbb;cursor:pointer;box-sizing:border-box}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{color:#333;border-color:#888;outline:0}.button.button-primary,button.button-primary,input[type=button].button-primary,input[type=reset].button-primary,input[type=submit].button-primary{color:#FFF;background-color:#33C3F0;border-color:#33C3F0}.button.button-primary:focus,.button.button-primary:hover,button.button-primary:focus,button.button-primary:hover,input[type=button].button-primary:focus,input[type=button].button-primary:hover,input[type=reset].button-primary:focus,input[type=reset].button-primary:hover,input[type=submit].button-primary:focus,input[type=submit].button-primary:hover{color:#FFF;background-color:#1EAEDB;border-color:#1EAEDB}input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],select,textarea{height:38px;padding:6px 10px;background-color:#fff;border:1px solid #D1D1D1;border-radius:4px;box-shadow:none;box-sizing:border-box}input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=url],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none}textarea{min-height:65px;padding-top:6px;padding-bottom:6px}input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,select:focus,textarea:focus{border:1px solid #33C3F0;outline:0}label,legend{display:block;margin-bottom:.5rem;font-weight:600}fieldset{padding:0;border-width:0}input[type=checkbox],input[type=radio]{display:inline}label>.label-body{display:inline-block;margin-left:.5rem;font-weight:400}ul{list-style:circle inside}ol{list-style:decimal inside}ol,ul{padding-left:0;margin-top:0}ol ol,ol ul,ul ol,ul ul{margin:1.5rem 0 1.5rem 3rem;font-size:90%}li{margin-bottom:1rem}code{padding:.2rem .5rem;margin:0 .2rem;font-size:90%;white-space:nowrap;background:#F1F1F1;border:1px solid #E1E1E1;border-radius:4px}pre>code{display:block;padding:1rem 1.5rem;white-space:pre}td,th{padding:12px 15px;text-align:left;border-bottom:1px solid #E1E1E1}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}.button,button{margin-bottom:1rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}.u-full-width{width:100%;box-sizing:border-box}.u-max-full-width{max-width:100%;box-sizing:border-box}.u-pull-right{float:right}.u-pull-left{float:left}hr{margin-top:3rem;margin-bottom:3.5rem;border-width:0;border-top:1px solid #E1E1E1}.container:after,.row:after,.u-cf{content:"";display:table;clear:both} -------------------------------------------------------------------------------- /src/user.rs: -------------------------------------------------------------------------------- 1 | use crate::base::BaseContext; 2 | use crate::db::Db; 3 | use crate::models::{ListingCardDisplay, Order, RocketAuthUser, UserAccount}; 4 | use rocket::fairing::AdHoc; 5 | use rocket::request::FlashMessage; 6 | use rocket::response::Flash; 7 | use rocket::response::Redirect; 8 | use rocket::serde::Serialize; 9 | use rocket_auth::AdminUser; 10 | use rocket_auth::User; 11 | use rocket_db_pools::Connection; 12 | use rocket_dyn_templates::Template; 13 | 14 | const PAGE_SIZE: u32 = 10; 15 | 16 | #[derive(Debug, Serialize)] 17 | #[serde(crate = "rocket::serde")] 18 | struct Context { 19 | base_context: BaseContext, 20 | flash: Option<(String, String)>, 21 | visited_user: RocketAuthUser, 22 | visited_user_account: UserAccount, 23 | weighted_average_rating: f32, 24 | listing_cards: Vec, 25 | admin_user: Option, 26 | page_num: u32, 27 | } 28 | 29 | impl Context { 30 | pub async fn raw( 31 | mut db: Connection, 32 | username: String, 33 | flash: Option<(String, String)>, 34 | maybe_page_num: Option, 35 | user: Option, 36 | admin_user: Option, 37 | ) -> Result { 38 | let base_context = BaseContext::raw(&mut db, user.clone(), admin_user.clone()) 39 | .await 40 | .map_err(|_| "failed to get base template.")?; 41 | let visited_user = RocketAuthUser::single_by_username(&mut db, username) 42 | .await 43 | .map_err(|_| "failed to get visited user.")?; 44 | let visited_user_account = UserAccount::single(&mut db, visited_user.id.unwrap()) 45 | .await 46 | .map_err(|_| "failed to get user account.")?; 47 | let page_num = maybe_page_num.unwrap_or(1); 48 | let listing_cards = ListingCardDisplay::all_active_for_user( 49 | &mut db, 50 | visited_user.id.unwrap(), 51 | PAGE_SIZE, 52 | page_num, 53 | ) 54 | .await 55 | .map_err(|_| "failed to get approved listings.")?; 56 | let seller_info = Order::seller_info_for_user(&mut db, visited_user.id.unwrap()) 57 | .await 58 | .map_err(|_| "failed to get weighted average rating for user.")?; 59 | let weighted_average_rating = seller_info.weighted_average_rating; 60 | Ok(Context { 61 | base_context, 62 | flash, 63 | visited_user, 64 | visited_user_account, 65 | weighted_average_rating, 66 | listing_cards, 67 | admin_user, 68 | page_num, 69 | }) 70 | } 71 | } 72 | 73 | #[put("//disable")] 74 | async fn disable( 75 | username: &str, 76 | mut db: Connection, 77 | _user: User, 78 | _admin_user: AdminUser, 79 | ) -> Result, Flash> { 80 | match disable_user(&mut db, username).await { 81 | Ok(_) => Ok(Flash::success( 82 | Redirect::to(format!("/{}/{}", "user", username)), 83 | "User disabled by admin".to_string(), 84 | )), 85 | Err(e) => { 86 | error_!("Mark as disabled error({}) error: {}", username, e); 87 | Err(Flash::error( 88 | Redirect::to(format!("/{}/{}", "user", username)), 89 | e, 90 | )) 91 | } 92 | } 93 | } 94 | 95 | async fn disable_user(db: &mut Connection, username: &str) -> Result<(), String> { 96 | let rocket_auth_user = RocketAuthUser::single_by_username(db, username.to_string()) 97 | .await 98 | .map_err(|_| "failed to get user")?; 99 | UserAccount::mark_as_disabled(db, rocket_auth_user.id.unwrap()) 100 | .await 101 | .map_err(|_| "failed to disable user account.")?; 102 | Ok(()) 103 | } 104 | 105 | #[put("//enable")] 106 | async fn enable( 107 | username: &str, 108 | mut db: Connection, 109 | _user: User, 110 | _admin_user: AdminUser, 111 | ) -> Result, Flash> { 112 | match enable_user(&mut db, username).await { 113 | Ok(_) => Ok(Flash::success( 114 | Redirect::to(format!("/{}/{}", "user", username)), 115 | "User enabled by admin".to_string(), 116 | )), 117 | Err(e) => { 118 | error_!("Mark as enabled error({}) error: {}", username, e); 119 | Err(Flash::error( 120 | Redirect::to(format!("/{}/{}", "user", username)), 121 | e, 122 | )) 123 | } 124 | } 125 | } 126 | 127 | async fn enable_user(db: &mut Connection, username: &str) -> Result<(), String> { 128 | let rocket_auth_user = RocketAuthUser::single_by_username(db, username.to_string()) 129 | .await 130 | .map_err(|_| "failed to get user")?; 131 | UserAccount::mark_as_enabled(db, rocket_auth_user.id.unwrap()) 132 | .await 133 | .map_err(|_| "failed to enable user account.")?; 134 | Ok(()) 135 | } 136 | 137 | #[get("/?")] 138 | async fn index( 139 | username: &str, 140 | flash: Option>, 141 | db: Connection, 142 | page_num: Option, 143 | user: Option, 144 | admin_user: Option, 145 | ) -> Result { 146 | let flash = flash.map(FlashMessage::into_inner); 147 | let context = Context::raw(db, username.to_string(), flash, page_num, user, admin_user) 148 | .await 149 | .map_err(|_| "failed to get template context.")?; 150 | Ok(Template::render("user", context)) 151 | } 152 | 153 | pub fn user_stage() -> AdHoc { 154 | AdHoc::on_ignite("User Stage", |rocket| async { 155 | rocket.mount("/user", routes![index, disable, enable]) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /templates/listing.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 | 4 | {% if listing_display %} 5 | 6 |
    7 |

    8 | 9 | {% if flash %} 10 | 11 | {{ flash.1 }} 12 | 13 | {% endif %} 14 | 15 | {% if user %} 16 | {% if admin_user or listing_display.listing.user_id == user.id %} 17 | 18 | {% if listing_display.listing.deactivated_by_seller %} 19 |
    Deactivated By Seller
    20 | {% elif listing_display.listing.deactivated_by_admin %} 21 |
    Deactivated By Admin
    22 | {% elif listing_display.listing.approved %} 23 |
    Active Listing
    24 | {% if user and listing_display.listing.user_id == user.id %} 25 |
    26 | 27 | 28 |
    29 | {% endif %} 30 | {% if admin_user %} 31 |
    32 | 33 | 34 |
    35 | {% endif %} 36 | {% elif listing_display.listing.reviewed and not listing_display.listing.approved %} 37 |
    Rejected Listing
    38 | {% elif listing_display.listing.submitted and not listing_display.listing.reviewed %} 39 |
    Pending Listing (waiting for admin approval)
    40 | {% if admin_user %} 41 |
    42 | 43 | 44 |
    45 |
    46 | 47 | 48 |
    49 | {% endif %} 50 | {% elif not listing_display.listing.submitted %} 51 |
    Unsubmitted Listing
    52 | {% if user and listing_display.listing.user_id == user.id %} 53 |

    Update images

    54 |

    Update shipping options

    55 |
    56 | 57 | 58 |
    59 | {% endif %} 60 | {% else %} 61 | {% endif %} 62 | {% if user and listing_display.listing.user_id == user.id %} 63 |
    64 | 65 |
    66 | {% endif %} 67 | {% if admin_user %} 68 |
    69 | 70 |
    71 | {% endif %} 72 | {% endif %} 73 | {% endif %} 74 | 75 | 76 | 77 |
    78 |
    79 |
    80 | {% for image in listing_display.images %} 81 | 82 | {% endfor %} 83 |
    84 | 85 |
    86 |
    87 |
    88 |

    {{ listing_display.listing.title }}

    89 |

    Seller: {% if listing_display.user %}{{ listing_display.user.username }}{% else %}Not found{% endif %}

    90 |

    Price: {{ listing_display.listing.price_sat }} sats

    91 | 92 | {% if admin_user or user and listing_display.listing.user_id == user.id %} 93 |

    Fee Rate: Market will collect a {{ listing_display.listing.fee_rate_basis_points / 100 }}% fee rate

    94 | {% endif %} 95 | 96 |

    Description: {{ listing_display.listing.description }}

    97 | 98 | {% if listing_display.listing.approved %} 99 | {% if not admin_user %} 100 |
    101 | 102 | 103 | 109 | 110 | 112 | 113 | 114 | 115 |
    116 | {% endif %} 117 | {% endif %} 118 | 119 |
    120 |
    121 |
    122 | 123 |
    124 | 125 | {% endif %} 126 | 127 | {% endblock body %} 128 | --------------------------------------------------------------------------------