├── .dockerignore ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── PACKAGING.md ├── README.md ├── config.cfg ├── debian ├── changelog ├── compat ├── control ├── copyright ├── raider.install ├── raider.postinst ├── raider.service ├── rules └── source │ └── format ├── dev ├── designs │ └── dashboard.sketch └── workspaces │ ├── account.paw │ └── track.paw ├── doc └── fixtures │ └── raider.sql ├── res └── assets │ ├── fonts │ └── open_sans │ │ ├── open_sans_bold.woff │ │ ├── open_sans_bold.woff2 │ │ ├── open_sans_light.woff │ │ ├── open_sans_light.woff2 │ │ ├── open_sans_regular.woff │ │ ├── open_sans_regular.woff2 │ │ ├── open_sans_semibold.woff │ │ └── open_sans_semibold.woff2 │ ├── images │ ├── checkbox │ │ └── checked.svg │ ├── common │ │ ├── close.svg │ │ ├── external.svg │ │ └── next.svg │ ├── dashboard │ │ ├── action_remove.svg │ │ └── expand.svg │ └── toast │ │ ├── close.svg │ │ ├── critical.svg │ │ ├── information.svg │ │ └── success.svg │ ├── javascripts │ ├── dashboard.js │ ├── dashboard_account.js │ ├── dashboard_payouts.js │ └── dashboard_trackers.js │ ├── public │ └── robots.txt │ ├── stylesheets │ ├── common.css │ ├── dashboard.css │ └── initiate.css │ └── templates │ ├── __base.tera │ ├── __partial.tera │ ├── _dashboard_header.tera │ ├── _dashboard_menu.tera │ ├── _dashboard_payouts.tera │ ├── _dashboard_toast.tera │ ├── dashboard_account.tera │ ├── dashboard_payouts.tera │ ├── dashboard_payouts_partial_payouts.tera │ ├── dashboard_trackers.tera │ ├── dashboard_welcome.tera │ ├── initiate_login.tera │ ├── initiate_recover.tera │ └── initiate_signup.tera ├── scripts ├── build_packages.sh ├── release_binaries.sh └── sign_binaries.sh └── src ├── config ├── config.rs ├── defaults.rs ├── logger.rs ├── mod.rs └── reader.rs ├── exchange ├── manager.rs └── mod.rs ├── main.rs ├── management ├── account.rs └── mod.rs ├── notifier ├── email.rs └── mod.rs ├── responder ├── asset_file.rs ├── auth_guard.rs ├── catchers.rs ├── context.rs ├── macros.rs ├── management_guard.rs ├── manager.rs ├── mod.rs ├── routes.rs ├── track_guard.rs └── utilities.rs ├── storage ├── choices.rs ├── db.rs ├── mod.rs ├── models.rs └── schemas.rs └── track ├── mod.rs └── payment.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target/* 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | name: Build and Release 7 | 8 | jobs: 9 | build-releases: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Cache build artifacts 17 | id: cache-cargo 18 | uses: actions/cache@v4 19 | with: 20 | path: | 21 | ~/.cargo/bin 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: build-${{ runner.os }}-cargo-any 26 | 27 | - name: Install Rust toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: nightly-2021-01-07 31 | components: rustfmt 32 | override: true 33 | 34 | - name: Install build dependencies 35 | run: sudo apt-get install -y libmariadb3 libmariadb-dev 36 | 37 | - name: Alias build dependencies 38 | run: | 39 | cd /lib/x86_64-linux-gnu/ 40 | sudo ln -s libmariadb.a libmysqlclient.a 41 | sudo ln -s libmariadb.a libmysqlclient_r.a 42 | sudo ln -s libmariadb.so.3 libmysqlclient.so 43 | sudo ln -s libmariadb.so.3 libmysqlclient_r.so 44 | 45 | - name: Verify versions 46 | run: rustc --version && rustup --version && cargo --version 47 | 48 | - name: Get current tag 49 | id: current_tag 50 | uses: WyriHaximus/github-action-get-previous-tag@v1 51 | 52 | - name: Release package 53 | run: cargo publish --no-verify --token ${CRATES_TOKEN} 54 | env: 55 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 56 | 57 | - name: Release binaries 58 | run: ./scripts/release_binaries.sh --version=${{ steps.current_tag.outputs.tag }} 59 | 60 | - name: Release new version 61 | uses: softprops/action-gh-release@v1 62 | with: 63 | tag_name: ${{ steps.current_tag.outputs.tag }} 64 | name: Raider ${{ steps.current_tag.outputs.tag }} 65 | body: "⚠️ Changelog not yet provided." 66 | files: ./${{ steps.current_tag.outputs.tag }}-*.tar.gz 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | build-packages: 71 | needs: build-releases 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - name: Checkout code 76 | uses: actions/checkout@v2 77 | 78 | - name: Build packages 79 | run: ./scripts/build_packages.sh 80 | 81 | - name: Push packages to Packagecloud 82 | uses: faucetsdn/action-packagecloud-upload-debian-packages@v1 83 | with: 84 | path: ./packages 85 | repo: ${{ secrets.PACKAGECLOUD_REPO }} 86 | token: ${{ secrets.PACKAGECLOUD_TOKEN }} 87 | 88 | build-docker: 89 | runs-on: ubuntu-latest 90 | 91 | steps: 92 | - name: Checkout code 93 | uses: actions/checkout@v2 94 | 95 | - name: Acquire Docker image metadata 96 | id: metadata 97 | uses: docker/metadata-action@v4 98 | with: 99 | images: valeriansaliou/raider 100 | 101 | - name: Login to Docker Hub 102 | uses: docker/login-action@v2 103 | with: 104 | username: ${{ secrets.DOCKERHUB_USERNAME }} 105 | password: ${{ secrets.DOCKERHUB_TOKEN }} 106 | 107 | - name: Build and push Docker image 108 | uses: docker/build-push-action@v3 109 | with: 110 | context: . 111 | tags: ${{ steps.metadata.outputs.tags }} 112 | labels: ${{ steps.metadata.outputs.labels }} 113 | push: true 114 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Test and Build 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest] 10 | rust-toolchain: [nightly-2021-01-07] 11 | fail-fast: false 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Cache build artifacts 20 | id: cache-cargo 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cargo/registry 25 | ~/.cargo/git 26 | target 27 | key: test-${{ runner.os }}-cargo-${{ matrix.rust-toolchain }} 28 | 29 | - name: Install Rust toolchain 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: ${{ matrix.rust-toolchain }} 33 | components: rustfmt 34 | override: true 35 | 36 | - name: Verify versions 37 | run: rustc --version && rustup --version && cargo --version 38 | 39 | - name: Build code 40 | run: cargo build 41 | 42 | - name: Test code 43 | run: cargo test 44 | 45 | - name: Check code style 46 | run: cargo fmt -- --check 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | .DS_Store 3 | *~ 4 | *# 5 | .cargo 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raider-server" 3 | version = "1.2.3" 4 | description = "Affiliates dashboard. Used by affiliates to generate tracking codes and review their balance." 5 | readme = "README.md" 6 | license = "MPL-2.0" 7 | homepage = "https://github.com/valeriansaliou/raider" 8 | repository = "https://github.com/valeriansaliou/raider.git" 9 | keywords = ["affiliates", "dashboard", "sales", "tracker"] 10 | categories = ["web-programming"] 11 | authors = ["Valerian Saliou "] 12 | exclude = ["dev/*"] 13 | 14 | [[bin]] 15 | name = "raider" 16 | path = "src/main.rs" 17 | doc = false 18 | 19 | [dependencies] 20 | log = "0.3" 21 | clap = { version = "2.29", default-features = false } 22 | lazy_static = "1.3" 23 | sha2 = "0.7" 24 | time = "0.1" 25 | rand = "0.4" 26 | serde = "1.0" 27 | serde_derive = "1.0" 28 | toml = "0.4" 29 | base64 = "0.6" 30 | validate = "0.6" 31 | url_serde = { version = "0.2", default-features = false } 32 | chrono = { version = "0.4", default-features = false } 33 | native-tls = { version = "0.2", features = ["vendored"] } 34 | openssl-probe = "0.1" 35 | lettre = { version = "0.9", features = ["smtp-transport"] } 36 | lettre_email = "0.9" 37 | rocket = { version = "0.4", features = ["private-cookies"] } 38 | rocket_contrib = { version = "0.4", features = ["tera_templates"] } 39 | diesel = { version = "1.1", features = ["mysql", "chrono", "numeric"] } 40 | r2d2 = "0.8" 41 | r2d2-diesel = "1.0" 42 | reqwest = { version = "0.10", features = ["native-tls-vendored", "blocking", "gzip", "json"] } 43 | bigdecimal = "0.1" 44 | num-traits = "0.1" 45 | separator = "0.3" 46 | iso_country = "0.1" 47 | 48 | [profile.dev] 49 | opt-level = 0 50 | debug = true 51 | debug-assertions = true 52 | 53 | [profile.release] 54 | opt-level = "s" 55 | lto = true 56 | debug = false 57 | debug-assertions = false 58 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly-buster AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN rustup --version 8 | RUN rustup install nightly-2021-01-07 && \ 9 | rustup default nightly-2021-01-07 10 | 11 | RUN rustc --version && \ 12 | rustup --version && \ 13 | cargo --version 14 | 15 | RUN apt-get update 16 | RUN apt-get install -y libssl-dev default-libmysqlclient-dev 17 | 18 | RUN cargo clean && cargo build --release 19 | RUN strip ./target/release/raider 20 | 21 | FROM debian:buster-slim 22 | 23 | RUN apt-get update 24 | RUN apt-get install -y libssl1.1 libmariadb3 25 | 26 | WORKDIR /usr/src/raider 27 | 28 | COPY ./res/assets/ ./res/assets/ 29 | COPY --from=build /app/target/release/raider /usr/local/bin/raider 30 | 31 | CMD [ "raider", "-c", "/etc/raider.cfg" ] 32 | 33 | EXPOSE 8080 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | ------------------------- 263 | 264 | > Covered Software is provided under this License on an "as is" 265 | > basis, without warranty of any kind, either expressed, implied, or 266 | > statutory, including, without limitation, warranties that the 267 | > Covered Software is free of defects, merchantable, fit for a 268 | > particular purpose or non-infringing. The entire risk as to the 269 | > quality and performance of the Covered Software is with You. 270 | > Should any Covered Software prove defective in any respect, You 271 | > (not any Contributor) assume the cost of any necessary servicing, 272 | > repair, or correction. This disclaimer of warranty constitutes an 273 | > essential part of this License. No use of any Covered Software is 274 | > authorized under this License except under this disclaimer. 275 | 276 | 277 | 7. Limitation of Liability 278 | -------------------------- 279 | 280 | > Under no circumstances and under no legal theory, whether tort 281 | > (including negligence), contract, or otherwise, shall any 282 | > Contributor, or anyone who distributes Covered Software as 283 | > permitted above, be liable to You for any direct, indirect, 284 | > special, incidental, or consequential damages of any character 285 | > including, without limitation, damages for lost profits, loss of 286 | > goodwill, work stoppage, computer failure or malfunction, or any 287 | > and all other commercial damages or losses, even if such party 288 | > shall have been informed of the possibility of such damages. This 289 | > limitation of liability shall not apply to liability for death or 290 | > personal injury resulting from such party's negligence to the 291 | > extent applicable law prohibits such limitation. Some 292 | > jurisdictions do not allow the exclusion or limitation of 293 | > incidental or consequential damages, so this exclusion and 294 | > limitation may not apply to You. 295 | 296 | 8. Litigation 297 | ------------- 298 | 299 | Any litigation relating to this License may be brought only in the 300 | courts of a jurisdiction where the defendant maintains its principal 301 | place of business and such litigation shall be governed by laws of that 302 | jurisdiction, without reference to its conflict-of-law provisions. 303 | Nothing in this Section shall prevent a party's ability to bring 304 | cross-claims or counter-claims. 305 | 306 | 9. Miscellaneous 307 | ---------------- 308 | 309 | This License represents the complete agreement concerning the subject 310 | matter hereof. If any provision of this License is held to be 311 | unenforceable, such provision shall be reformed only to the extent 312 | necessary to make it enforceable. Any law or regulation which provides 313 | that the language of a contract shall be construed against the drafter 314 | shall not be used to construe this License against a Contributor. 315 | 316 | 10. Versions of the License 317 | --------------------------- 318 | 319 | 10.1. New Versions 320 | 321 | Mozilla Foundation is the license steward. Except as provided in Section 322 | 10.3, no one other than the license steward has the right to modify or 323 | publish new versions of this License. Each version will be given a 324 | distinguishing version number. 325 | 326 | 10.2. Effect of New Versions 327 | 328 | You may distribute the Covered Software under the terms of the version 329 | of the License under which You originally received the Covered Software, 330 | or under the terms of any subsequent version published by the license 331 | steward. 332 | 333 | 10.3. Modified Versions 334 | 335 | If you create software not governed by this License, and you want to 336 | create a new license for such software, you may create and use a 337 | modified version of this License if you rename the license and remove 338 | any references to the name of the license steward (except to note that 339 | such modified license differs from this License). 340 | 341 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 342 | Licenses 343 | 344 | If You choose to distribute Source Code Form that is Incompatible With 345 | Secondary Licenses under the terms of this version of the License, the 346 | notice described in Exhibit B of this License must be attached. 347 | 348 | Exhibit A - Source Code Form License Notice 349 | ------------------------------------------- 350 | 351 | This Source Code Form is subject to the terms of the Mozilla Public 352 | License, v. 2.0. If a copy of the MPL was not distributed with this 353 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 354 | 355 | If it is not possible or desirable to put the notice in a particular 356 | file, then You may include the notice in a location (such as a LICENSE 357 | file in a relevant directory) where a recipient would be likely to look 358 | for such a notice. 359 | 360 | You may add additional accurate notices of copyright ownership. 361 | 362 | Exhibit B - "Incompatible With Secondary Licenses" Notice 363 | --------------------------------------------------------- 364 | 365 | This Source Code Form is "Incompatible With Secondary Licenses", as 366 | defined by the Mozilla Public License, v. 2.0. 367 | -------------------------------------------------------------------------------- /PACKAGING.md: -------------------------------------------------------------------------------- 1 | Packaging 2 | ========= 3 | 4 | This file contains quick reminders and notes on how to package Raider. 5 | 6 | We consider here the packaging flow of Raider version `1.0.0` for Linux. 7 | 8 | 1. **How to bump Raider version before a release:** 9 | 1. Bump version in `Cargo.toml` to `1.0.0` 10 | 2. Execute `cargo update` to bump `Cargo.lock` 11 | 3. Bump Debian package version in `debian/rules` to `1.0.0` 12 | 13 | 2. **How to build Raider, package it and release it on Crates, GitHub, Docker Hub and Packagecloud (multiple architectures):** 14 | 1. Tag the latest Git commit corresponding to the release with tag `v1.0.0`, and push the tag 15 | 2. Wait for all release jobs to complete on the [actions](https://github.com/valeriansaliou/raider/actions) page on GitHub 16 | 3. Download all release archives, and sign them locally using: `./scripts/sign_binaries.sh --version=1.0.0` 17 | 4. Publish a changelog and upload all the built archives, as well as their signatures on the [releases](https://github.com/valeriansaliou/raider/releases) page on GitHub 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Raider 2 | ====== 3 | 4 | [![Test and Build](https://github.com/valeriansaliou/raider/workflows/Test%20and%20Build/badge.svg?branch=master)](https://github.com/valeriansaliou/raider/actions?query=workflow%3A%22Test+and+Build%22) [![Build and Release](https://github.com/valeriansaliou/raider/workflows/Build%20and%20Release/badge.svg)](https://github.com/valeriansaliou/raider/actions?query=workflow%3A%22Build+and+Release%22) [![dependency status](https://deps.rs/repo/github/valeriansaliou/raider/status.svg)](https://deps.rs/repo/github/valeriansaliou/raider) [![Buy Me A Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://www.buymeacoffee.com/valeriansaliou) 5 | 6 | **Affiliates dashboard. Used by affiliates to generate tracking codes and review their balance.** 7 | 8 | Raider is easy to integrate in your existing system. You can also customize the dashboard look & feel with templates and styles. It can be used as a self-service affiliates system, for your affiliate users to manage their account, create tracking URLs, review their balance and request for payouts. 9 | 10 | _Tested at Rust version: `rustc 1.51.0-nightly (c8915eebe 2021-01-07)`_ 11 | 12 | **🇭🇺 Crafted in Budapest, Hungary.** 13 | 14 | ![Raider](https://valeriansaliou.github.io/raider/images/raider.png) 15 | 16 | ## Who uses it? 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Crisp
26 | 27 | _👋 You use Raider and you want to be listed there? [Contact me](https://valeriansaliou.name/)._ 28 | 29 | ## Features 30 | 31 | * **Self-service affiliates dashboard** 32 | * **Users can generate affiliates tracking codes** 33 | * **Users can see their affiliates statistics** (eg. how much money they made) 34 | * **Users can request for payouts** (you then receive a notification email) 35 | * **Your backend reports referred customer payments to Raider** 36 | 37 | ## How does it work? 38 | 39 | Raider provides a self-service affiliates dashboard on which users can sign up, login, and manage their account (eg. create tracking codes, request for payouts, etc.). Your backend can report referred customer payments to Raider, so that the affiliates can cash out their commission and request for a payout at any point. 40 | 41 | **Raider provides two services:** 42 | 43 | * **Self-service dashboard**: Used by your affiliates users 44 | * **Payment reporting API**: Called by your backend once a payment is made (ie. to credit due commission money to an affiliate) 45 | 46 | ## How to use it? 47 | 48 | ### Installation 49 | 50 | **Install from releases:** 51 | 52 | The best way to install Raider is to pull the latest release from the [Raider releases](https://github.com/valeriansaliou/raider/releases) page. 53 | 54 | Make sure to pick the correct server architecture (eg. Intel 32 bits). 55 | 56 | 👉 _Each release binary comes with an `.asc` signature file, which can be verified using [@valeriansaliou](https://github.com/valeriansaliou) GPG public key: [:key:valeriansaliou.gpg.pub.asc](https://valeriansaliou.name/files/keys/valeriansaliou.gpg.pub.asc)._ 57 | 58 | **Install from packages:** 59 | 60 | Raider provides [pre-built packages](https://packagecloud.io/valeriansaliou/raider) for Debian-based systems (Debian, Ubuntu, etc.). 61 | 62 | **Important: Raider only provides 64 bits packages targeting Debian 12 for now (codename: `bookworm`). You might still be able to use them on other Debian versions, as well as Ubuntu (although they rely on a specific `glibc` version that might not be available on older or newer systems).** 63 | 64 | First, add the Raider APT repository (eg. for Debian `bookworm`): 65 | 66 | ```bash 67 | echo "deb [signed-by=/usr/share/keyrings/valeriansaliou_raider.gpg] https://packagecloud.io/valeriansaliou/raider/debian/ bookworm main" > /etc/apt/sources.list.d/valeriansaliou_raider.list 68 | ``` 69 | 70 | ```bash 71 | curl -fsSL https://packagecloud.io/valeriansaliou/raider/gpgkey | gpg --dearmor -o /usr/share/keyrings/valeriansaliou_raider.gpg 72 | ``` 73 | 74 | ```bash 75 | apt-get update 76 | ``` 77 | 78 | Then, install the Raider package: 79 | 80 | ```bash 81 | apt-get install raider 82 | ``` 83 | 84 | Then, edit the pre-filled Raider configuration file: 85 | 86 | ```bash 87 | nano /etc/raider/raider.cfg 88 | ``` 89 | 90 | Finally, restart Raider: 91 | 92 | ``` 93 | service raider restart 94 | ``` 95 | 96 | **Install from Cargo:** 97 | 98 | If you prefer managing `raider` via Rust's Cargo, install it directly via `cargo install`: 99 | 100 | ```bash 101 | cargo install raider-server 102 | ``` 103 | 104 | Ensure that your `$PATH` is properly configured to source the Crates binaries, and then run Raider using the `raider` command. 105 | 106 | **Install from source:** 107 | 108 | The last option is to pull the source code from Git and compile Raider via `cargo`: 109 | 110 | ```bash 111 | cargo build --release 112 | ``` 113 | 114 | You can find the built binaries in the `./target/release` directory. 115 | 116 | _Install the `libssl-dev` (ie. OpenSSL headers) and `libmysqlclient-dev` (ie. MySQL client headers) before you compile Raider. SSL dependencies are required for email notifications, and MySQL dependencies are required to connect to your database._ 117 | 118 | **Install from Docker Hub:** 119 | 120 | You might find it convenient to run Raider via Docker. You can find the pre-built Raider image on Docker Hub as [valeriansaliou/raider](https://hub.docker.com/r/valeriansaliou/raider/). 121 | 122 | First, pull the `valeriansaliou/raider` image: 123 | 124 | ```bash 125 | docker pull valeriansaliou/raider:v1.2.3 126 | ``` 127 | 128 | Then, seed it a configuration file and run it (replace `/path/to/your/raider/config.cfg` with the path to your configuration file): 129 | 130 | ```bash 131 | docker run -p 8080:8080 -v /path/to/your/raider/config.cfg:/etc/raider.cfg valeriansaliou/raider:v1.2.3 132 | ``` 133 | 134 | In the configuration file, ensure that: 135 | 136 | * `server.inet` is set to `0.0.0.0:8080` (this lets Raider be reached from outside the container) 137 | * `assets.path` is set to `./res/assets/` (this refers to an internal path in the container, as the assets are contained there) 138 | 139 | Raider will be reachable from `http://localhost:8080`. 140 | 141 | ### Database 142 | 143 | Raider requires a MySQL to be running on your host (it is unfortunately not compatible with PostgreSQL and others, _at the moment_). 144 | 145 | The Raider SQL schema should be imported in the Raider database you created, which you can find at [raider.sql](https://github.com/valeriansaliou/raider/blob/master/doc/fixtures/raider.sql). 146 | 147 | ### Configuration 148 | 149 | Use the sample [config.cfg](https://github.com/valeriansaliou/raider/blob/master/config.cfg) configuration file and adjust it to your own environment. 150 | 151 | --- 152 | 153 | **⚠️ Important: Make sure to change the default `server.secret_key`, `server.track_token` and `server.management_token` configuration values with secret keys you generated. Also, generate a random arbitrary length string for `database.password_salt`. Failing to change any of those values will make your Raider instance insecure. You can easily create these tokens by running `openssl rand -base64 32`.** 154 | 155 | --- 156 | 157 | **Available configuration options are commented below, with allowed values:** 158 | 159 | **[server]** 160 | 161 | * `log_level` (type: _string_, allowed: `debug`, `info`, `warn`, `error`, default: `error`) — Verbosity of logging, set it to `error` in production 162 | * `inet` (type: _string_, allowed: IPv4 / IPv6 + port, default: `[::1]:8080`) — Host and TCP port the Raider service should listen on 163 | * `workers` (type: _integer_, allowed: any number, default: `4`) — Number of workers for the Raider service to run on 164 | * `track_token` (type: _string_, allowed: secret token, default: no default) — Track API secret token (ie. secret password) 165 | * `management_token` (type: _string_, allowed: secret token, default: no default) — Management API secret token (ie. secret password) 166 | * `secret_key` (type: _string_, allowed: 192-bit base64 encoded secret key, default: no default) — Secret key for cookie encryption (see [Rocket docs](https://api.rocket.rs/rocket/struct.Config.html#method.set_secret_key) for details) 167 | 168 | **[database]** 169 | 170 | * `url` (type: _string_, allowed: MySQL URL, no default) — URL of the MySQL database to connect to 171 | * `pool_size` (type: _integer_, allowed: any number, default: `4`) — Number of connections to maintain to MySQL 172 | * `idle_timeout` (type: _integer_, allowed: seconds, default: `300`) — Idle timeout in seconds to MySQL 173 | * `connection_timeout` (type: _integer_, allowed: seconds, default: `10`) — Connection timeout in seconds to MySQL 174 | * `password_salt` (type: _string_, allowed: any string, no default) — Password salt (preferably strong and long; do not change this after accounts got created as it will make them unusable) 175 | * `account_create_allow` (type: _boolean_, allowed: `true`, `false`, default: `true`) — Whether to allow accounts to be created or not 176 | 177 | **[exchange]** 178 | 179 | **[exchange.fixer]** 180 | 181 | * `endpoint` (type: _string_, allowed: any string, default: `https://api.apilayer.com/fixer`) — Fixer API endpoint (on APILayer) 182 | * `api_key` (type: _string_, allowed: any string, no default) — APILayer API key (for Fixer) 183 | 184 | **[email]** 185 | 186 | * `from` (type: _string_, allowed: email address, no default) — Email address from which to send emails 187 | * `smtp_host` (type: _string_, allowed: hostname, IPv4, IPv6, default: `localhost`) — SMTP host to connect to 188 | * `smtp_port` (type: _integer_, allowed: TCP port, default: `587`) — SMTP TCP port to connect to 189 | * `smtp_username` (type: _string_, allowed: any string, no default) — SMTP username to use for authentication (if any) 190 | * `smtp_password` (type: _string_, allowed: any string, no default) — SMTP password to use for authentication (if any) 191 | * `smtp_encrypt` (type: _boolean_, allowed: `true`, `false`, default: `true`) — Whether to encrypt SMTP connection with `STARTTLS` or not 192 | 193 | **[assets]** 194 | 195 | * `path` (type: _string_, allowed: UNIX path, default: `./res/assets/`) — Path to Raider assets directory 196 | 197 | **[branding]** 198 | 199 | * `page_title` (type: _string_, allowed: any string, default: `Affiliates`) — Affiliates system title 200 | * `page_url` (type: _string_, allowed: URL, no default) — Affiliates system URL 201 | * `help_url` (type: _string_, allowed: URL, no default) — Help URL to be used in dashboard (ie. knowledge base where users can search for help) 202 | * `support_url` (type: _string_, allowed: URL, no default) — Support URL to be used in dashboard (ie. where users can contact you if something is wrong) 203 | * `icon_color` (type: _string_, allowed: hexadecimal color code, no default) — Icon color (ie. your icon background color) 204 | * `icon_url` (type: _string_, allowed: URL, no default) — Icon URL, the icon should be your squared logo, used as favicon (PNG format recommended) 205 | * `logo_white_url` (type: _string_, allowed: URL, no default) — Logo URL, the logo should be your full-width logo, used as login, signup & account recover form logo (whiter logo, SVG format recommended) 206 | * `logo_dark_url` (type: _string_, allowed: URL, no default) — Logo URL, the logo should be your full-width logo, used as dashboard header logo (darker logo, SVG format recommended) 207 | * `custom_html` (type: _string_, allowed: HTML, default: empty) — Custom HTML to include in affiliates system `head` (optional) 208 | 209 | **[tracker]** 210 | 211 | * `track_url` (type: _string_, allowed: tracker URL, no default) — Tracker URL, to which tracker links will point to 212 | * `track_parameter` (type: _string_, allowed: tracker query parameter, default: `t`) — Tracker query parameter used in URL (eg. `?t=xDJSas10`) 213 | * `commission_default` (type: _float_, allowed: percentage from `0.00` to `1.00`, default: `0.20`) — Default commission percentage (for new accounts) 214 | 215 | **[[tracker.banner]]** 216 | 217 | * `banner_url` (type: _string_, allowed: image URL, no default) — URL to the banner image 218 | * `size_width` (type: _integer_, allowed: image size in pixels, no default) — Width of the banner (in pixels) 219 | * `size_height` (type: _integer_, allowed: image size in pixels, no default) — Height of the banner (in pixels) 220 | 221 | **[payout]** 222 | 223 | * `currency` (type: _string_, allowed: currency code, default: `EUR`) — Currency to be used for payouts (and balances in general) 224 | * `amount_minimum` (type: _float_, allowed: any number, default: `100.00`) — Minimum amount for payout requests 225 | * `administrator_email` (type: _string_, allowed: email address, no default) — Email address of the affiliates system administrator (payout request emails will be sent there) 226 | 227 | ### Run Raider 228 | 229 | Raider can be run as such: 230 | 231 | `./raider -c /path/to/config.cfg` 232 | 233 | ## How can I integrate Raider reporting in my code? 234 | 235 | When a payment for which you have a `tracking_id` is made on your platform (ie. a payment for a customer that was referred by an affiliate); your backend needs to submit this payment to the Raider tracking API. The full payment amount needs to be submitted, as the commission percentage is applied by Raider itself. 236 | 237 | ### Raider reporting libraries 238 | 239 | * **Python**: **[py-raider-reporter](https://pypi.org/project/py-raider-reporter/)** 240 | 241 | 👉 Cannot find the library for your programming language? Build your own and be referenced here! ([contact me](https://valeriansaliou.name/)) 242 | 243 | ## How can I use Raider HTTP APIs? 244 | 245 | ### 1️⃣ Track API 246 | 247 | #### Payment tracking 248 | 249 | In case you need to manually report tracked payments to the Raider endpoint, use the following HTTP configuration (adjust it to yours): 250 | 251 | **Endpoint URL:** 252 | 253 | `HTTP POST https://affiliates.example.com/track/payment//` 254 | 255 | Where: 256 | 257 | * `tracking_id`: The tracking identifier associated to customer who paid 258 | 259 | **Request headers:** 260 | 261 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `server.track_token`. 262 | 263 | **Request data:** 264 | 265 | Adjust the request data to your payment context and send it as `HTTP POST`: 266 | 267 | ```json 268 | { 269 | "amount": 95.00, 270 | "currency": "EUR", 271 | "trace": "Plan: Unlimited; Customer: valerian@crisp.chat; Website: crisp.chat" 272 | } 273 | ``` 274 | 275 | Where: 276 | 277 | * `amount`: The full amount of the payment (Raider process the commission amount itself, eg. with `20%` commission you send `100.00` and Raider processes it as `20.00`) 278 | * `currency`: The payment currency code (if the currency is different than the default currency configured with `payout.currency`, a conversion is applied using current day market rates) 279 | * `trace`: An optional trace value which is logged in the database (may be used for your own records; this is never visible to your affiliate users) 280 | 281 | #### Signup tracking 282 | 283 | In case you need to manually report tracked signups to the Raider endpoint, use the following HTTP configuration (adjust it to yours): 284 | 285 | **Endpoint URL:** 286 | 287 | `HTTP POST https://affiliates.example.com/track/signup//` 288 | 289 | Where: 290 | 291 | * `tracking_id`: The tracking identifier associated to customer who signed up 292 | 293 | **Request headers:** 294 | 295 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `server.track_token`. 296 | 297 | ### 2️⃣ Management API 298 | 299 | #### Account creation 300 | 301 | In case you need to create accounts in Raider database from a third-party system in your infrastructure (eg. if regular signups are disabled), you may us create new accounts via the Raider endpoint, use the following HTTP configuration (adjust it to yours): 302 | 303 | **Endpoint URL:** 304 | 305 | `HTTP POST https://affiliates.example.com/management/account/` 306 | 307 | **Request headers:** 308 | 309 | * Add an `Authorization` header with a `Basic` authentication where the password is your configured `server.management_token`. 310 | 311 | **Request data:** 312 | 313 | Adjust the request data to your payment context and send it as `HTTP POST`: 314 | 315 | ```json 316 | { 317 | "email": "john.doe@gmail.com", 318 | "full_name": "John Doe", 319 | "address": "1 Market Street, San Francisco, CA", 320 | "country": "US" 321 | } 322 | ``` 323 | 324 | Where: 325 | 326 | * `email`: The email address for the new account (an auto-generated password will be sent to this email) 327 | * `full_name`: An optional full name value to preconfigure in the created account 328 | * `address`: An optional address value to preconfigure in the created account 329 | * `country`: An optional country value to preconfigure in the created account 330 | 331 | ## :fire: Report A Vulnerability 332 | 333 | If you find a vulnerability in Raider, you are more than welcome to report it directly to [@valeriansaliou](https://github.com/valeriansaliou) by sending an encrypted email to [valerian@valeriansaliou.name](mailto:valerian@valeriansaliou.name). Do not report vulnerabilities in public GitHub issues, as they may be exploited by malicious people to target production servers running an unpatched Raider server. 334 | 335 | **:warning: You must encrypt your email using [@valeriansaliou](https://github.com/valeriansaliou) GPG public key: [:key:valeriansaliou.gpg.pub.asc](https://valeriansaliou.name/files/keys/valeriansaliou.gpg.pub.asc).** 336 | -------------------------------------------------------------------------------- /config.cfg: -------------------------------------------------------------------------------- 1 | # Raider 2 | # Affiliates dashboard 3 | # Configuration file 4 | # Example: https://github.com/valeriansaliou/raider/blob/master/config.cfg 5 | 6 | 7 | [server] 8 | 9 | log_level = "error" 10 | inet = "[::1]:8080" 11 | workers = 4 12 | track_token = "REPLACE_THIS_WITH_A_SECRET_TRACK_TOKEN" 13 | management_token = "REPLACE_THIS_WITH_A_SECRET_MANAGEMENT_TOKEN" 14 | secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" 15 | 16 | [database] 17 | 18 | url = "mysql://crisp_affiliates@127.0.0.1:3306/crisp_affiliates" 19 | pool_size = 4 20 | idle_timeout = 300 21 | connection_timeout = 10 22 | password_salt = "^96^ofjJDBYrbv9toqtZv3m}k9HNwB*TKVq>2xPhf3j6UQ^M)GV+NKhpME.4Q;W6" 23 | account_create_allow = true 24 | 25 | [exchange] 26 | 27 | [exchange.fixer] 28 | 29 | endpoint = "https://api.apilayer.com/fixer" 30 | api_key = "REPLACE_THIS_WITH_YOUR_APILAYER_FIXER_API_KEY" 31 | 32 | [email] 33 | 34 | from = "affiliates@crisp.chat" 35 | 36 | smtp_host = "localhost" 37 | smtp_port = 587 38 | smtp_username = "user-access" 39 | smtp_password = "user-password" 40 | smtp_encrypt = false 41 | 42 | [assets] 43 | 44 | path = "./res/assets/" 45 | 46 | [branding] 47 | 48 | page_title = "Crisp Affiliates" 49 | page_url = "https://affiliates.crisp.chat/" 50 | help_url = "https://help.crisp.chat/" 51 | support_url = "mailto:support@crisp.chat" 52 | icon_color = "#3C82E7" 53 | icon_url = "https://valeriansaliou.github.io/raider/images/crisp-icon.png" 54 | logo_white_url = "https://valeriansaliou.github.io/raider/images/crisp-logo-white.svg" 55 | logo_dark_url = "https://valeriansaliou.github.io/raider/images/crisp-logo-dark.svg" 56 | custom_html = "" 57 | 58 | [tracker] 59 | 60 | track_url = "https://crisp.chat/" 61 | track_parameter = "t" 62 | commission_default = 0.20 63 | 64 | [[tracker.banner]] 65 | 66 | banner_url = "https://valeriansaliou.github.io/raider/images/crisp-icon.png" 67 | size_width = 300 68 | size_height = 520 69 | 70 | [[tracker.banner]] 71 | 72 | banner_url = "https://valeriansaliou.github.io/raider/images/crisp-icon.png" 73 | size_width = 320 74 | size_height = 600 75 | 76 | [[tracker.banner]] 77 | 78 | banner_url = "https://valeriansaliou.github.io/raider/images/crisp-icon.png" 79 | size_width = 400 80 | size_height = 180 81 | 82 | [payout] 83 | 84 | currency = "EUR" 85 | amount_minimum = 100.00 86 | administrator_email = "affiliates+payouts@crisp.chat" 87 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | raider (0.0.0-1) UNRELEASED; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Valerian Saliou Tue, 31 Aug 2023 12:00:00 +0000 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: raider 2 | Section: net 3 | Priority: ext 4 | Maintainer: Valerian Saliou 5 | Standards-Version: 3.9.4 6 | Build-Depends: wget, ca-certificates, libmariadb3, libmariadb-dev 7 | Homepage: https://github.com/valeriansaliou/raider 8 | 9 | Package: raider 10 | Architecture: any 11 | Depends: adduser, libmariadb3 12 | Provides: raider 13 | Description: Affiliates dashboard. Used by affiliates to generate tracking codes and review their balance. 14 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: raider 3 | Upstream-Contact: Valerian Saliou 4 | Source: https://github.com/valeriansaliou/raider 5 | 6 | Files: * 7 | Copyright: 2023 Valerian Saliou 8 | License: MPL-2 9 | 10 | License: MPL-2 11 | This Source Code Form is subject to the terms of the Mozilla Public License, 12 | v. 2.0. If a copy of the MPL was not distributed with this file, 13 | You can obtain one at http://mozilla.org/MPL/2.0/. 14 | -------------------------------------------------------------------------------- /debian/raider.install: -------------------------------------------------------------------------------- 1 | raider/raider usr/bin/ 2 | raider/raider.cfg etc/raider/ 3 | raider/assets/ etc/raider/ 4 | -------------------------------------------------------------------------------- /debian/raider.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | case "$1" in 6 | configure) 7 | adduser --system --disabled-password --disabled-login --home /var/empty \ 8 | --no-create-home --quiet --group raider 9 | ;; 10 | esac 11 | 12 | #DEBHELPER# 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /debian/raider.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Affiliates dashboard 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=raider 8 | Group=raider 9 | ExecStart=/usr/bin/raider -c /etc/raider/raider.cfg 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DISTRIBUTION = $(shell lsb_release -sr) 4 | VERSION = 1.2.3 5 | PACKAGEVERSION = $(VERSION)-0~$(DISTRIBUTION)0 6 | URL = https://github.com/valeriansaliou/raider/releases/download/v$(VERSION)/ 7 | 8 | %: 9 | dh $@ --with systemd 10 | 11 | override_dh_auto_clean: 12 | override_dh_auto_test: 13 | override_dh_auto_build: 14 | override_dh_auto_install: 15 | $(eval ENV_ARCH := $(shell dpkg --print-architecture)) 16 | $(eval ENV_ISA := $(shell if [ "$(ENV_ARCH)" = "amd64" ]; then echo "x86_64"; else echo "$(ENV_ARCH)"; fi)) 17 | $(eval ENV_TARBALL := v$(VERSION)-$(ENV_ISA)-gnu.tar.gz) 18 | 19 | echo "Architecture: $(ENV_ARCH)" 20 | echo "Instruction Set: $(ENV_ISA)" 21 | echo "Target: $(URL)$(ENV_TARBALL)" 22 | 23 | wget -N --progress=dot:mega $(URL)$(ENV_TARBALL) 24 | tar -xf $(ENV_TARBALL) 25 | strip raider/raider 26 | mv raider/config.cfg raider/raider.cfg 27 | mv raider/res/assets/ raider/assets/ 28 | rm -r raider/res/ 29 | sed -i 's/path = ".\/res\/assets\/"/path = "\/etc\/raider\/assets\/"/g' raider/raider.cfg 30 | 31 | override_dh_gencontrol: 32 | dh_gencontrol -- -v$(PACKAGEVERSION) 33 | 34 | override_dh_shlibdeps: 35 | sudo ln -s /lib/x86_64-linux-gnu/libmariadb.a /lib/x86_64-linux-gnu/libmysqlclient.a 36 | sudo ln -s /lib/x86_64-linux-gnu/libmariadb.a /lib/x86_64-linux-gnu/libmysqlclient_r.a 37 | sudo ln -s /lib/x86_64-linux-gnu/libmariadb.so.3 /lib/x86_64-linux-gnu/libmysqlclient.so 38 | sudo ln -s /lib/x86_64-linux-gnu/libmariadb.so.3 /lib/x86_64-linux-gnu/libmysqlclient_r.so 39 | 40 | dh_shlibdeps 41 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /dev/designs/dashboard.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/dev/designs/dashboard.sketch -------------------------------------------------------------------------------- /dev/workspaces/account.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/dev/workspaces/account.paw -------------------------------------------------------------------------------- /dev/workspaces/track.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/dev/workspaces/track.paw -------------------------------------------------------------------------------- /doc/fixtures/raider.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `account` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | `email` varchar(191) NOT NULL DEFAULT '', 4 | `password` binary(32) NOT NULL DEFAULT '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', 5 | `recovery` binary(32) DEFAULT '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', 6 | `commission` decimal(3,2) NOT NULL DEFAULT '0.00', 7 | `full_name` varchar(191) DEFAULT NULL, 8 | `address` varchar(191) DEFAULT NULL, 9 | `country` enum('AF','AX','AL','DZ','AS','AD','AO','AI','AQ','AG','AR','AM','AW','AU','AT','AZ','BS','BH','BD','BB','BY','BE','BZ','BJ','BM','BT','BO','BQ','BA','BW','BV','BR','IO','BN','BG','BF','BI','KH','CM','CA','CV','KY','CF','TD','CL','CN','CX','CC','CO','KM','CG','CD','CK','CR','CI','HR','CU','CW','CY','CZ','DK','DJ','DM','DO','EC','EG','SV','GQ','ER','EE','ET','FK','FO','FJ','FI','FR','GF','PF','TF','GA','GM','GE','DE','GH','GI','GR','GL','GD','GP','GU','GT','GG','GN','GW','GY','HT','HM','VA','HN','HK','HU','IS','IN','ID','IR','IQ','IE','IM','IL','IT','JM','JP','JE','JO','KZ','KE','KI','KP','KR','KW','KG','LA','LV','LB','LS','LR','LY','LI','LT','LU','MO','MK','MG','MW','MY','MV','ML','MT','MH','MQ','MR','MU','YT','MX','FM','MD','MC','MN','ME','MS','MA','MZ','MM','NA','NR','NP','NL','NC','NZ','NI','NE','NG','NU','NF','MP','NO','OM','PK','PW','PS','PA','PG','PY','PE','PH','PN','PL','PT','PR','QA','RE','RO','RU','RW','BL','SH','KN','LC','MF','PM','VC','WS','SM','ST','SA','SN','RS','SC','SL','SG','SX','SK','SI','SB','SO','ZA','GS','SS','ES','LK','SD','SR','SJ','SZ','SE','CH','SY','TW','TJ','TZ','TH','TL','TG','TK','TO','TT','TN','TR','TM','TC','TV','UG','UA','AE','GB','US','UM','UY','UZ','VU','VE','VN','VG','VI','WF','EH','YE','ZM','ZW') DEFAULT NULL, 10 | `payout_method` enum('bank','paypal','bitcoin','other') DEFAULT NULL, 11 | `payout_instructions` text, 12 | `notify_balance` char(1) NOT NULL DEFAULT '1', 13 | `created_at` datetime NOT NULL, 14 | `updated_at` datetime NOT NULL, 15 | PRIMARY KEY (`id`), 16 | UNIQUE KEY `email` (`email`) 17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 18 | 19 | CREATE TABLE `tracker` ( 20 | `id` char(10) NOT NULL DEFAULT '', 21 | `label` varchar(191) NOT NULL, 22 | `statistics_signups` int(11) unsigned NOT NULL DEFAULT '0', 23 | `account_id` int(11) unsigned NOT NULL, 24 | `created_at` datetime NOT NULL, 25 | `updated_at` datetime NOT NULL, 26 | PRIMARY KEY (`id`), 27 | KEY `fk_tracker_account_id` (`account_id`), 28 | CONSTRAINT `fk_tracker_account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 30 | 31 | CREATE TABLE `balance` ( 32 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 33 | `amount` decimal(9,2) NOT NULL DEFAULT '0.00', 34 | `currency` enum('AFN','EUR','ALL','DZD','USD','AOA','XCD','ARS','AMD','AWG','AUD','AZN','BSD','BHD','BDT','BBD','BYN','BZD','XOF','BMD','INR','BTN','BOB','BOV','BAM','BWP','NOK','BRL','BND','BGN','BIF','CVE','KHR','XAF','CAD','KYD','CLP','CLF','CNY','COP','COU','KMF','CDF','NZD','CRC','HRK','CUP','CUC','ANG','CZK','DKK','DJF','DOP','EGP','SVC','ERN','ETB','FKP','FJD','XPF','GMD','GEL','GHS','GIP','GTQ','GBP','GNF','GYD','HTG','HNL','HKD','HUF','ISK','IDR','XDR','IRR','IQD','ILS','JMD','JPY','JOD','KZT','KES','KPW','KRW','KWD','KGS','LAK','LBP','LSL','ZAR','LRD','LYD','CHF','MOP','MKD','MGA','MWK','MYR','MVR','MRO','MUR','XUA','MXN','MXV','MDL','MNT','MAD','MZN','MMK','NAD','NPR','NIO','NGN','OMR','PKR','PAB','PGK','PYG','PEN','PHP','PLN','QAR','RON','RUB','RWF','SHP','WST','STD','SAR','RSD','SCR','SLL','SGD','XSU','SBD','SOS','SSP','LKR','SDG','SRD','SZL','SEK','CHE','CHW','SYP','TWD','TJS','TZS','THB','TOP','TTD','TND','TRY','TMT','UGX','UAH','AED','USN','UYU','UYI','UZS','VUV','VEF','VND','YER','ZMW','ZWL','XBA','XBB','XBC','XBD','XTS','XXX','XAU','XPD','XPT','XAG','AFA','FIM','ALK','ADP','ESP','FRF','AOK','AON','AOR','ARA','ARP','ARY','RUR','ATS','AYM','AZM','BYR','BYB','BEC','BEF','BEL','BOP','BAD','BRB','BRC','BRE','BRN','BRR','BGJ','BGK','BGL','BUK','CNX','HRD','CYP','CSJ','CSK','ECS','ECV','GQE','EEK','XEU','GEK','DDM','DEM','GHC','GHP','GRD','GNE','GNS','GWE','GWP','ITL','ISJ','IEP','ILP','ILR','LAJ','LVL','LVR','LSM','ZAL','LTL','LTT','LUC','LUF','LUL','MGF','MVQ','MLF','MTL','MTP','MXP','MZE','MZM','NLG','NIC','PEH','PEI','PES','PLZ','PTE','ROK','ROL','CSD','SKK','SIT','RHD','ESA','ESB','SDD','SDP','SRG','CHC','TJR','TPE','TRL','TMM','UGS','UGW','UAK','SUR','USS','UYN','UYP','VEB','VNC','YDD','YUD','YUM','YUN','ZRN','ZRZ','ZMK','ZWC','ZWD','ZWN','ZWR','XFO','XRE','XFU') NOT NULL DEFAULT 'USD', 35 | `released` char(1) NOT NULL DEFAULT '0', 36 | `trace` text, 37 | `account_id` int(11) unsigned NOT NULL, 38 | `tracker_id` char(10) DEFAULT NULL, 39 | `created_at` datetime NOT NULL, 40 | `updated_at` datetime NOT NULL, 41 | PRIMARY KEY (`id`), 42 | KEY `fk_balance_account_id` (`account_id`), 43 | KEY `fk_balance_tracker_id` (`tracker_id`), 44 | CONSTRAINT `fk_balance_account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 45 | CONSTRAINT `fk_balance_tracker_id` FOREIGN KEY (`tracker_id`) REFERENCES `tracker` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 46 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 47 | 48 | CREATE TABLE `payout` ( 49 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 50 | `number` int(11) unsigned NOT NULL DEFAULT '0', 51 | `amount` decimal(9,2) NOT NULL DEFAULT '0.00', 52 | `currency` enum('AFN','EUR','ALL','DZD','USD','AOA','XCD','ARS','AMD','AWG','AUD','AZN','BSD','BHD','BDT','BBD','BYN','BZD','XOF','BMD','INR','BTN','BOB','BOV','BAM','BWP','NOK','BRL','BND','BGN','BIF','CVE','KHR','XAF','CAD','KYD','CLP','CLF','CNY','COP','COU','KMF','CDF','NZD','CRC','HRK','CUP','CUC','ANG','CZK','DKK','DJF','DOP','EGP','SVC','ERN','ETB','FKP','FJD','XPF','GMD','GEL','GHS','GIP','GTQ','GBP','GNF','GYD','HTG','HNL','HKD','HUF','ISK','IDR','XDR','IRR','IQD','ILS','JMD','JPY','JOD','KZT','KES','KPW','KRW','KWD','KGS','LAK','LBP','LSL','ZAR','LRD','LYD','CHF','MOP','MKD','MGA','MWK','MYR','MVR','MRO','MUR','XUA','MXN','MXV','MDL','MNT','MAD','MZN','MMK','NAD','NPR','NIO','NGN','OMR','PKR','PAB','PGK','PYG','PEN','PHP','PLN','QAR','RON','RUB','RWF','SHP','WST','STD','SAR','RSD','SCR','SLL','SGD','XSU','SBD','SOS','SSP','LKR','SDG','SRD','SZL','SEK','CHE','CHW','SYP','TWD','TJS','TZS','THB','TOP','TTD','TND','TRY','TMT','UGX','UAH','AED','USN','UYU','UYI','UZS','VUV','VEF','VND','YER','ZMW','ZWL','XBA','XBB','XBC','XBD','XTS','XXX','XAU','XPD','XPT','XAG','AFA','FIM','ALK','ADP','ESP','FRF','AOK','AON','AOR','ARA','ARP','ARY','RUR','ATS','AYM','AZM','BYR','BYB','BEC','BEF','BEL','BOP','BAD','BRB','BRC','BRE','BRN','BRR','BGJ','BGK','BGL','BUK','CNX','HRD','CYP','CSJ','CSK','ECS','ECV','GQE','EEK','XEU','GEK','DDM','DEM','GHC','GHP','GRD','GNE','GNS','GWE','GWP','ITL','ISJ','IEP','ILP','ILR','LAJ','LVL','LVR','LSM','ZAL','LTL','LTT','LUC','LUF','LUL','MGF','MVQ','MLF','MTL','MTP','MXP','MZE','MZM','NLG','NIC','PEH','PEI','PES','PLZ','PTE','ROK','ROL','CSD','SKK','SIT','RHD','ESA','ESB','SDD','SDP','SRG','CHC','TJR','TPE','TRL','TMM','UGS','UGW','UAK','SUR','USS','UYN','UYP','VEB','VNC','YDD','YUD','YUM','YUN','ZRN','ZRZ','ZMK','ZWC','ZWD','ZWN','ZWR','XFO','XRE','XFU') NOT NULL DEFAULT 'USD', 53 | `status` enum('pending','accepted','rejected','processed') NOT NULL DEFAULT 'pending', 54 | `account` varchar(191) DEFAULT NULL, 55 | `invoice_url` varchar(191) DEFAULT NULL, 56 | `account_id` int(11) unsigned NOT NULL, 57 | `created_at` datetime NOT NULL, 58 | `updated_at` datetime NOT NULL, 59 | PRIMARY KEY (`id`), 60 | KEY `fk_payout_account_id` (`account_id`), 61 | CONSTRAINT `fk_payout_account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 63 | 64 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 65 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 66 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 67 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 68 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 69 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 70 | -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_bold.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_bold.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_light.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_light.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_regular.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_regular.woff2 -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_semibold.woff -------------------------------------------------------------------------------- /res/assets/fonts/open_sans/open_sans_semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valeriansaliou/raider/d0d23a67275145c71672cc2e731c4f7cb162d67c/res/assets/fonts/open_sans/open_sans_semibold.woff2 -------------------------------------------------------------------------------- /res/assets/images/checkbox/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/assets/images/common/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/assets/images/common/external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /res/assets/images/common/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /res/assets/images/dashboard/action_remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /res/assets/images/dashboard/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /res/assets/images/toast/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/assets/images/toast/critical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/assets/images/toast/information.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/assets/images/toast/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/assets/javascripts/dashboard.js: -------------------------------------------------------------------------------- 1 | var ToastManager = (function() { 2 | return { 3 | _toast : {}, 4 | _selectors : {}, 5 | 6 | _TOAST_CLOSE_DELAY : 250, 7 | _TOAST_OPEN_DELAY : 350, 8 | 9 | _TOAST_AUTO_CLOSE_DELAY_DEFAULT : 8000, 10 | _TOAST_AUTO_CLOSE_DELAY_SHORT : 2000, 11 | 12 | _TOAST_MESSAGE_FORMAT_SPACE_REGEX : /[_-]+/g, 13 | 14 | _toast_close_timeout : null, 15 | _toast_open_timeout : null, 16 | _toast_auto_close_timeout : null, 17 | 18 | success : function(label, message) { 19 | ToastManager.__open("success", label, message); 20 | }, 21 | 22 | info : function(label, message) { 23 | ToastManager.__open("info", label, message); 24 | }, 25 | 26 | warning : function(label, message) { 27 | ToastManager.__open("warning", label, message); 28 | }, 29 | 30 | error : function(label, message) { 31 | ToastManager.__open("error", label, message); 32 | }, 33 | 34 | close : function() { 35 | if (ToastManager._toast.active === true) { 36 | // Cancel previous toast close? 37 | if (ToastManager._toast_close_timeout !== null) { 38 | clearTimeout(ToastManager._toast_close_timeout); 39 | 40 | ToastManager._toast_close_timeout = null; 41 | } 42 | 43 | // Mark toast as inactive 44 | ToastManager._toast.active = false; 45 | 46 | // Hide toast 47 | ToastManager.__select(".toast").setAttribute("data-active", "false"); 48 | 49 | // Close toast 50 | ToastManager._toast_close_timeout = setTimeout(function() { 51 | // Unschedule close (eg. if still scheduled) 52 | ToastManager.__unschedule_close(); 53 | }, ToastManager._TOAST_CLOSE_DELAY); 54 | } 55 | 56 | return false; 57 | }, 58 | 59 | _mouse_over : function() { 60 | // Unschedules active closes 61 | ToastManager.__unschedule_close(); 62 | }, 63 | 64 | _mouse_leave : function() { 65 | // Re-schedule toast auto-close 66 | ToastManager.__schedule_close(true); 67 | }, 68 | 69 | __open : function(level, label, message) { 70 | if (!label) { 71 | throw new Error("No label provided for toast"); 72 | } 73 | if (!message) { 74 | throw new Error("No message provided for toast"); 75 | } 76 | 77 | // Cancel previous toast open? 78 | if (ToastManager._toast_open_timeout !== null) { 79 | clearTimeout(ToastManager._toast_open_timeout); 80 | 81 | ToastManager._toast_open_timeout = null; 82 | } 83 | 84 | // Close previous toast? 85 | ToastManager.close(); 86 | 87 | // Open toast 88 | ToastManager._toast_open_timeout = setTimeout(function() { 89 | ToastManager._toast_open_timeout = null; 90 | 91 | // Mark toast as active 92 | ToastManager._toast.active = true; 93 | 94 | // Assign toast state 95 | ToastManager.__select(".toast-view-wrapped-message-main").innerText = ( 96 | label 97 | ); 98 | ToastManager.__select(".toast-view-wrapped-message-sub").innerText = ( 99 | message 100 | ); 101 | 102 | // Show toast 103 | var toast_sel = ToastManager.__select(".toast"); 104 | 105 | toast_sel.setAttribute("data-active", "true"); 106 | toast_sel.setAttribute("data-level", level); 107 | 108 | // Schedule toast auto-close 109 | ToastManager.__schedule_close(); 110 | }, ToastManager._TOAST_OPEN_DELAY); 111 | 112 | return false; 113 | }, 114 | 115 | __schedule_close : function(is_short) { 116 | // Unschedule any previous close 117 | ToastManager.__unschedule_close(); 118 | 119 | // Schedule auto close 120 | ToastManager._toast_auto_close_timeout = setTimeout(function() { 121 | ToastManager._toast_auto_close_timeout = null; 122 | 123 | ToastManager.close(); 124 | }, ( 125 | (is_short === true) ? ToastManager._TOAST_AUTO_CLOSE_DELAY_SHORT : 126 | ToastManager._TOAST_AUTO_CLOSE_DELAY_DEFAULT 127 | )); 128 | }, 129 | 130 | __unschedule_close : function() { 131 | if (ToastManager._toast_auto_close_timeout !== null) { 132 | clearTimeout(ToastManager._toast_auto_close_timeout); 133 | 134 | ToastManager._toast_auto_close_timeout = null; 135 | } 136 | }, 137 | 138 | __format_message : function(message) { 139 | // Convert all space-like chars to actual spaces 140 | message = ( 141 | message.replace( 142 | ToastManager._TOAST_MESSAGE_FORMAT_SPACE_REGEX, " " 143 | ).trim() 144 | ); 145 | 146 | // Capitalize first letter from message 147 | message = (message.charAt(0).toUpperCase() + message.slice(1)); 148 | 149 | return message; 150 | }, 151 | 152 | __select : function(target) { 153 | ToastManager._selectors[target] = ( 154 | ToastManager._selectors[target] || document.querySelector(target) 155 | ); 156 | 157 | return ToastManager._selectors[target]; 158 | } 159 | }; 160 | })(); 161 | 162 | 163 | var IntentManager = (function() { 164 | return { 165 | show : function(type, name, return_selector) { 166 | var target_selector = this.__visibility(type, name, true); 167 | 168 | return ( 169 | (return_selector === true) ? target_selector : null 170 | ); 171 | }, 172 | 173 | hide : function(type, name, return_selector) { 174 | var target_selector = this.__visibility(type, name, false); 175 | 176 | return ( 177 | (return_selector === true) ? target_selector : null 178 | ); 179 | }, 180 | 181 | __visibility : function(type, name, is_visible) { 182 | var target_selector = ( 183 | document.querySelector("." + type + "-lock[data-name=\"" + name + "\"]") 184 | ); 185 | 186 | if (target_selector) { 187 | target_selector.setAttribute( 188 | "data-visible", ((is_visible === true) ? "true" : "false") 189 | ); 190 | 191 | return target_selector; 192 | } 193 | 194 | return null; 195 | }, 196 | }; 197 | })(); 198 | 199 | 200 | var FormManager = (function() { 201 | return { 202 | submit : function() { 203 | FormManager.__toggle(true); 204 | }, 205 | 206 | unsubmit : function() { 207 | FormManager.__toggle(false); 208 | }, 209 | 210 | __toggle : function(is_pending) { 211 | document.querySelector("main").setAttribute("data-pending", ( 212 | (is_pending === true) ? "true" : "false" 213 | )); 214 | } 215 | }; 216 | })(); 217 | 218 | 219 | var PartialManager = (function() { 220 | return { 221 | load : function(path, fn_handle_done, fn_handle_error) { 222 | var request = new XMLHttpRequest(); 223 | 224 | request.open("GET", path, true); 225 | 226 | request.responseType = "document"; 227 | 228 | request.onreadystatechange = function() { 229 | // Request finished. 230 | if (request.readyState === 4) { 231 | if (request.status === 200) { 232 | if (typeof fn_handle_done === "function") { 233 | fn_handle_done(request); 234 | } 235 | } else { 236 | if (typeof fn_handle_error === "function") { 237 | fn_handle_error(request); 238 | } 239 | } 240 | } 241 | }; 242 | 243 | request.send(); 244 | } 245 | }; 246 | })(); 247 | -------------------------------------------------------------------------------- /res/assets/javascripts/dashboard_account.js: -------------------------------------------------------------------------------- 1 | var PasswordAccountManager = (function() { 2 | return { 3 | change : function() { 4 | var field_selector = document.getElementById("account-password-field"), 5 | link_selector = document.getElementById("account-password-link"); 6 | 7 | link_selector.style.display = "none"; 8 | 9 | field_selector.style.display = "block"; 10 | field_selector.focus(); 11 | } 12 | }; 13 | })(); 14 | -------------------------------------------------------------------------------- /res/assets/javascripts/dashboard_payouts.js: -------------------------------------------------------------------------------- 1 | var PayoutsManager = (function() { 2 | return { 3 | _last_page : 1, 4 | _is_loading : false, 5 | 6 | load_more : function() { 7 | if (PayoutsManager._is_loading === false) { 8 | PayoutsManager._is_loading = true; 9 | 10 | FormManager.submit(); 11 | 12 | PayoutsManager._last_page++; 13 | 14 | PartialManager.load( 15 | ("/dashboard/payouts/partial/payouts/" + 16 | PayoutsManager._last_page + "/"), 17 | 18 | PayoutsManager.__handle_load_more_success, 19 | PayoutsManager.__handle_load_more_error 20 | ); 21 | } 22 | }, 23 | 24 | __handle_load_more_success : function(request) { 25 | var list_container = document.querySelector(".section-list"); 26 | 27 | var result_list_items = ( 28 | request.response.body.querySelectorAll(".section-list li") 29 | ); 30 | var result_load_more = ( 31 | request.response.body.querySelector(".section-box-more-wrap") 32 | ); 33 | 34 | // Append results? 35 | if (result_list_items.length > 0) { 36 | for (var i = (result_list_items.length - 1); i >= 0; i--) { 37 | list_container.appendChild(result_list_items[i]); 38 | } 39 | } 40 | 41 | // Nuke load more? 42 | if (!result_load_more) { 43 | var load_more_container = ( 44 | document.querySelector(".section-box-more-wrap") 45 | ); 46 | 47 | if (load_more_container) { 48 | load_more_container.parentNode.removeChild(load_more_container); 49 | } 50 | } 51 | 52 | FormManager.unsubmit(); 53 | 54 | // Not loading anymore 55 | PayoutsManager._is_loading = false; 56 | }, 57 | 58 | __handle_load_more_error : function() { 59 | ToastManager.error( 60 | "Error loading more payouts.", 61 | "Older payouts could not be loaded. Try again." 62 | ); 63 | 64 | FormManager.unsubmit(); 65 | 66 | // Not loading anymore 67 | PayoutsManager._is_loading = false; 68 | } 69 | }; 70 | })(); 71 | -------------------------------------------------------------------------------- /res/assets/javascripts/dashboard_trackers.js: -------------------------------------------------------------------------------- 1 | var FormTrackersManager = (function() { 2 | return { 3 | _count_selected : 0, 4 | 5 | remove_trackers_confirm : function() { 6 | IntentManager.show("modal", "remove"); 7 | }, 8 | 9 | remove_trackers_submit : function() { 10 | IntentManager.hide("modal", "remove"); 11 | 12 | FormManager.submit(); 13 | 14 | document.getElementById("trackers-remove-form").submit(); 15 | }, 16 | 17 | create_tracker : function() { 18 | var modal_selector = IntentManager.show("modal", "create", true); 19 | 20 | if (modal_selector) { 21 | modal_selector.querySelector("input").focus(); 22 | } 23 | }, 24 | 25 | checkbox_change : function(checkbox) { 26 | FormTrackersManager._count_selected = Math.max( 27 | 0, 28 | 29 | ( 30 | FormTrackersManager._count_selected + 31 | ((checkbox.checked === true) ? 1 : -1) 32 | ) 33 | ); 34 | 35 | checkbox.parentElement.parentElement.setAttribute( 36 | "data-selected", ((checkbox.checked === true) ? "true" : "false") 37 | ); 38 | 39 | document.getElementById("trackers-remove-button").setAttribute( 40 | "data-locked", 41 | ((FormTrackersManager._count_selected > 0) ? "false" : "true") 42 | ); 43 | }, 44 | 45 | copy_tracker_link : function(button) { 46 | var link_selector = button.parentElement.parentElement.querySelector( 47 | "input.item-identity-sub" 48 | ); 49 | 50 | if (link_selector) { 51 | link_selector.select(); 52 | 53 | document.execCommand("copy"); 54 | 55 | ToastManager.info( 56 | "Tracker link copied.", 57 | "The tracker link has been copied to your clipboard." 58 | ); 59 | } 60 | } 61 | }; 62 | })(); 63 | 64 | 65 | var FormBannersManager = (function() { 66 | return { 67 | _last_tracker_link : "", 68 | _attribute_escape_regex : /"/g, 69 | 70 | open : function(button) { 71 | var link_selector = button.parentElement.parentElement.querySelector( 72 | "input.item-identity-sub" 73 | ); 74 | 75 | if (link_selector) { 76 | FormBannersManager._last_tracker_link = link_selector.value; 77 | 78 | var popup_selector = document.querySelector(".popup-lock"); 79 | 80 | if (popup_selector) { 81 | var selected_picker_selector = popup_selector.querySelector( 82 | ".popup-banner-picker li[data-selected=\"true\"] a" 83 | ); 84 | 85 | if (selected_picker_selector) { 86 | // Re-generate banner HTML code 87 | FormBannersManager.__generate_html(selected_picker_selector); 88 | } 89 | 90 | IntentManager.show("popup", "banner"); 91 | } 92 | } 93 | }, 94 | 95 | pick_banner : function(banner) { 96 | var popup_selector = document.querySelector(".popup-lock"); 97 | 98 | if (popup_selector) { 99 | // Select target elements 100 | var pickers_selector = ( 101 | popup_selector.querySelectorAll(".popup-banner-picker li") 102 | ); 103 | var next_selector = ( 104 | popup_selector.querySelector(".popup-actions .button-next") 105 | ); 106 | 107 | // Select picked banner 108 | for (var i = 0; i < pickers_selector.length; i++) { 109 | if (pickers_selector[i].getAttribute("data-selected") !== "false") { 110 | pickers_selector[i].setAttribute("data-selected", "false"); 111 | } 112 | } 113 | 114 | banner.parentElement.setAttribute("data-selected", "true"); 115 | 116 | // Generate banner HTML code 117 | FormBannersManager.__generate_html(banner); 118 | 119 | // Activate copy button 120 | next_selector.removeAttribute("data-locked"); 121 | } 122 | }, 123 | 124 | copy_selected_banner : function() { 125 | document.getElementById("trackers-banner-code").select(); 126 | document.execCommand("copy"); 127 | 128 | IntentManager.hide("popup", "banner"); 129 | 130 | ToastManager.info( 131 | "Banner HTML code copied.", 132 | "The banner HTML code has been copied to your clipboard for tracker." 133 | ); 134 | }, 135 | 136 | __generate_html : function(banner) { 137 | // Generate banner HTML code 138 | var image_selector = banner.querySelector("img"); 139 | var code_selector = document.getElementById("trackers-banner-code"); 140 | 141 | var html_data = { 142 | href : FormBannersManager.__attribute( 143 | FormBannersManager._last_tracker_link 144 | ), 145 | src : FormBannersManager.__attribute( 146 | image_selector.getAttribute("src") 147 | ), 148 | width : FormBannersManager.__attribute( 149 | image_selector.getAttribute("data-width") 150 | ), 151 | height : FormBannersManager.__attribute( 152 | image_selector.getAttribute("data-height") 153 | ) 154 | }; 155 | 156 | code_selector.value = ( 157 | "" + 158 | "\"\"" + 162 | "" 163 | ); 164 | }, 165 | 166 | __attribute : function(value) { 167 | return (value || "").replace( 168 | FormBannersManager._attribute_escape_regex, """ 169 | ); 170 | } 171 | }; 172 | })(); 173 | -------------------------------------------------------------------------------- /res/assets/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /res/assets/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: "Raider Open Sans Light"; 5 | src: url("/assets/fonts/open_sans/open_sans_light.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_light.woff") format("woff"); 6 | font-weight: 100; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: "Raider Open Sans Regular"; 12 | src: url("/assets/fonts/open_sans/open_sans_regular.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_regular.woff") format("woff"); 13 | font-weight: 400; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: "Raider Open Sans Semibold"; 19 | src: url("/assets/fonts/open_sans/open_sans_semibold.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_semibold.woff") format("woff"); 20 | font-weight: 600; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: "Raider Open Sans Bold"; 26 | src: url("/assets/fonts/open_sans/open_sans_bold.woff2") format("woff2"), url("/assets/fonts/open_sans/open_sans_bold.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | .font-sans-light { 32 | font-family: "Raider Open Sans Light", sans-serif; 33 | } 34 | 35 | .font-sans-regular { 36 | font-family: "Raider Open Sans Regular", sans-serif; 37 | } 38 | 39 | .font-sans-semibold { 40 | font-family: "Raider Open Sans Semibold", sans-serif; 41 | } 42 | 43 | .font-sans-bold { 44 | font-family: "Raider Open Sans Bold", sans-serif; 45 | } 46 | 47 | body { 48 | font-family: "Raider Open Sans Regular", sans-serif; 49 | font-size: 14px; 50 | background: #EFF3F6 !important; 51 | color: #000000; 52 | } 53 | 54 | * { 55 | font-weight: normal !important; 56 | margin: 0; 57 | padding: 0; 58 | } 59 | 60 | ::-moz-selection { 61 | background-color: rgba(182, 212, 255, 0.5); 62 | } 63 | 64 | ::selection { 65 | background-color: rgba(182, 212, 255, 0.5); 66 | } 67 | 68 | a, 69 | button { 70 | outline: 0 none !important; 71 | } 72 | 73 | a { 74 | text-decoration: none; 75 | cursor: pointer; 76 | } 77 | 78 | a:hover { 79 | cursor: pointer; 80 | } 81 | 82 | .clear { 83 | display: block !important; 84 | clear: both !important; 85 | } 86 | 87 | .hidden { 88 | display: none !important; 89 | } 90 | 91 | .underlined { 92 | text-decoration: underline !important; 93 | } 94 | 95 | .capitalize { 96 | text-transform: capitalize !important; 97 | } 98 | 99 | .uppercase { 100 | text-transform: uppercase !important; 101 | } 102 | 103 | .text-overflow-ellipsis { 104 | white-space: nowrap !important; 105 | overflow: hidden !important; 106 | text-overflow: ellipsis !important; 107 | } 108 | 109 | .color-green { 110 | color: #37C234 !important; 111 | } 112 | 113 | .color-blue { 114 | color: #1972F5 !important; 115 | } 116 | 117 | .color-orange { 118 | color: #FF8C25 !important; 119 | } 120 | 121 | .color-red { 122 | color: #F14040 !important; 123 | } 124 | 125 | .color-grey { 126 | color: #878889 !important; 127 | } 128 | 129 | .lock { 130 | background-color: rgba(15, 21, 29, 0.60); 131 | position: fixed; 132 | top: 0; 133 | bottom: 0; 134 | left: 0; 135 | right: 0; 136 | z-index: 100; 137 | animation-duration: 0.25s; 138 | animation-name: lock-fade; 139 | } 140 | 141 | .close { 142 | background-image: url("/assets/images/common/close.svg"); 143 | background-size: contain; 144 | background-repeat: no-repeat; 145 | background-position: center; 146 | } 147 | 148 | .modal { 149 | position: absolute; 150 | left: 50%; 151 | top: 50%; 152 | transform: translate(-50%, -50%); 153 | } 154 | 155 | .modal-lock, 156 | .popup-lock { 157 | display: none; 158 | } 159 | 160 | .modal-lock[data-visible="true"], 161 | .popup-lock[data-visible="true"] { 162 | display: block; 163 | } 164 | 165 | .modal-inner { 166 | background-color: #FFFFFF; 167 | width: 460px; 168 | padding: 28px 34px 24px; 169 | box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.16); 170 | border-radius: 2px; 171 | animation-duration: 0.6s; 172 | animation-name: modal-appear; 173 | } 174 | 175 | .modal-title { 176 | margin-bottom: 20px; 177 | } 178 | 179 | .modal-title h1 { 180 | color: #000000; 181 | font-size: 16px; 182 | float: left; 183 | } 184 | 185 | .modal-title a { 186 | width: 14px; 187 | height: 14px; 188 | margin-top: 3px; 189 | opacity: 0.5; 190 | float: right; 191 | transition: opacity 0.2s linear; 192 | } 193 | 194 | .modal-title a:hover { 195 | opacity: 0.75; 196 | } 197 | 198 | .modal-title a:active { 199 | opacity: 0.85; 200 | } 201 | 202 | .modal p.modal-main { 203 | color: #000000; 204 | margin-bottom: 10px; 205 | } 206 | 207 | .modal p { 208 | font-size: 13px; 209 | color: rgba(0, 0, 0, 0.5); 210 | line-height: 18px; 211 | text-align: justify; 212 | } 213 | 214 | .modal-field { 215 | text-indent: 20px; 216 | width: 100%; 217 | margin-top: 18px; 218 | padding-left: 0 !important; 219 | } 220 | 221 | .modal-actions { 222 | margin-top: 24px; 223 | } 224 | 225 | .modal-actions .button { 226 | float: right; 227 | } 228 | 229 | .popup { 230 | position: absolute; 231 | left: 50%; 232 | top: 80px; 233 | bottom: 0; 234 | transform: translateX(-50%); 235 | } 236 | 237 | .popup-inner { 238 | background-color: #FFFFFF; 239 | width: 680px; 240 | height: 100%; 241 | box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.16); 242 | border-top-left-radius: 2px; 243 | border-top-right-radius: 2px; 244 | display: flex; 245 | flex-direction: column; 246 | animation-duration: 0.6s; 247 | animation-name: popup-appear; 248 | } 249 | 250 | .popup-scroll { 251 | padding: 28px 34px 24px; 252 | flex: 1; 253 | overflow-x: hidden; 254 | overflow-y: auto; 255 | } 256 | 257 | .popup-title { 258 | margin-bottom: 34px; 259 | padding-left: 22px; 260 | padding-right: 8px; 261 | } 262 | 263 | .popup-title h1 { 264 | color: #000000; 265 | font-size: 17px; 266 | float: left; 267 | } 268 | 269 | .popup-title a { 270 | width: 14px; 271 | height: 14px; 272 | margin-top: 3px; 273 | opacity: 0.5; 274 | float: right; 275 | transition: opacity 0.2s linear; 276 | } 277 | 278 | .popup-title a:hover { 279 | opacity: 0.75; 280 | } 281 | 282 | .popup-title a:active { 283 | opacity: 0.85; 284 | } 285 | 286 | .popup-actions { 287 | border-top: 1px solid rgba(0, 0, 0, 0.05); 288 | padding: 14px 34px; 289 | box-shadow: 0 -1px 3px 0 rgba(15, 31, 64, 0.025); 290 | } 291 | 292 | .popup-actions .button { 293 | float: right; 294 | } 295 | 296 | .button { 297 | background: #1972F5; 298 | color: #FFFFFF; 299 | border: 0 none; 300 | text-align: center; 301 | cursor: pointer; 302 | outline: none; 303 | display: block; 304 | box-shadow: 0 2px 3px 0 rgba(15, 31, 64, 0.06); 305 | border-radius: 2px; 306 | transition: all 0.2s linear; 307 | transition-property: background, box-shadow, transform; 308 | } 309 | 310 | .button:hover { 311 | box-shadow: 0 3px 8px 0 rgba(15, 31, 64, 0.125); 312 | } 313 | 314 | .button:active { 315 | background: #0F64E2; 316 | transform: translateY(1px); 317 | box-shadow: 0 1px 2px 0 rgba(15, 31, 64, 0.1); 318 | } 319 | 320 | .button.disabled { 321 | background: rgba(0, 0, 0, 0.1) !important; 322 | pointer-events: none !important; 323 | user-select: none !important; 324 | box-shadow: none !important; 325 | } 326 | 327 | .button[data-locked="true"] { 328 | opacity: 0.7 !important; 329 | pointer-events: none !important; 330 | user-select: none !important; 331 | } 332 | 333 | .button.button-danger { 334 | background: #EA2C2C; 335 | } 336 | 337 | .button.button-danger:active { 338 | background: #D42222; 339 | } 340 | 341 | .button.button-important { 342 | background: #FA5E13; 343 | } 344 | 345 | .button.button-important:active { 346 | background: #E3510A; 347 | } 348 | 349 | .button.button-dark { 350 | background: #383838; 351 | } 352 | 353 | .button.button-dark:active { 354 | background: #191919; 355 | } 356 | 357 | .button.button-large { 358 | font-size: 12.5px; 359 | letter-spacing: 0.3px; 360 | line-height: 50px; 361 | padding: 0 26px; 362 | } 363 | 364 | button.button.button-large { 365 | line-height: 16px; 366 | height: 50px; 367 | } 368 | 369 | .button.button-medium { 370 | font-size: 12px; 371 | letter-spacing: 0.2px; 372 | line-height: 38px; 373 | padding: 0 22px; 374 | } 375 | 376 | button.button.button-medium { 377 | line-height: 14px; 378 | height: 38px; 379 | } 380 | 381 | .button.button-small { 382 | font-size: 11.5px; 383 | letter-spacing: 0.1px; 384 | line-height: 34px; 385 | padding: 0 17px; 386 | } 387 | 388 | button.button.button-small { 389 | line-height: 13px; 390 | height: 34px; 391 | } 392 | 393 | .button.button-tiny { 394 | font-size: 11px; 395 | letter-spacing: 0.05px; 396 | line-height: 32px; 397 | padding: 0 16px; 398 | } 399 | 400 | button.button.button-tiny { 401 | line-height: 12px; 402 | height: 32px; 403 | } 404 | 405 | .button.button-action { 406 | background: #FFFFFF; 407 | color: #000000; 408 | } 409 | 410 | .button.button-next:after { 411 | content: ""; 412 | background-image: url("/assets/images/common/next.svg"); 413 | background-size: contain; 414 | background-repeat: no-repeat; 415 | background-position: center; 416 | height: 12px; 417 | width: 6px; 418 | margin-left: 20px; 419 | margin-right: -6px; 420 | margin-bottom: -1px; 421 | opacity: 0.5; 422 | display: inline-block; 423 | } 424 | 425 | .checkbox { 426 | width: 0; 427 | height: 0; 428 | opacity: 0; 429 | display: none; 430 | } 431 | 432 | .checkbox + .checkbox-label { 433 | margin: 0; 434 | display: inline-block; 435 | } 436 | 437 | .checkbox, 438 | .checkbox-image { 439 | width: 20px; 440 | height: 20px; 441 | } 442 | 443 | .checkbox-image { 444 | display: inline-block; 445 | cursor: pointer; 446 | background-color: #FFFFFF; 447 | border: 1px solid rgba(0, 0, 0, 0.125); 448 | position: relative; 449 | border-radius: 2px; 450 | transition: border-color 0.1s linear; 451 | } 452 | 453 | .checkbox-image:hover { 454 | border-color: rgba(0, 0, 0, 0.25); 455 | } 456 | 457 | .checkbox-image:active { 458 | border-color: rgba(0, 0, 0, 0.35); 459 | } 460 | 461 | .checkbox:checked + .checkbox-label .checkbox-image { 462 | background-color: #1972F5; 463 | border: 0 none; 464 | width: 22px; 465 | height: 22px; 466 | } 467 | 468 | .checkbox:checked + .checkbox-label .checkbox-image:before { 469 | content: ""; 470 | background-image: url("/assets/images/checkbox/checked.svg"); 471 | background-size: contain; 472 | background-repeat: no-repeat; 473 | background-position: center; 474 | width: 13px; 475 | height: 13px; 476 | display: block; 477 | position: absolute; 478 | top: 50%; 479 | left: 50%; 480 | transform: translate(-50%, -50%); 481 | } 482 | 483 | .toast { 484 | width: 100%; 485 | position: relative; 486 | z-index: 90; 487 | margin-bottom: -47px; 488 | visibility: hidden; 489 | transition: margin-bottom 0.25s linear; 490 | } 491 | 492 | .toast[data-active="true"] { 493 | margin-bottom: 0; 494 | } 495 | 496 | .toast[data-active="true"] .toast-view-progress { 497 | animation: toast-progress; 498 | animation-timing-function: linear; 499 | animation-delay: 1s; 500 | animation-duration: 8.25s; 501 | animation-fill-mode: forwards; 502 | } 503 | 504 | .toast[data-level="success"], 505 | .toast[data-level="info"], 506 | .toast[data-level="warning"], 507 | .toast[data-level="error"] { 508 | visibility: visible; 509 | } 510 | 511 | .toast[data-level="success"] .toast-view { 512 | background-color: #37C234; 513 | } 514 | 515 | .toast[data-level="success"] .toast-view-wrapped-icon:before { 516 | background-image: url("/assets/images/toast/success.svg"); 517 | } 518 | 519 | .toast[data-level="info"] .toast-view { 520 | background-color: #1972F5; 521 | } 522 | 523 | .toast[data-level="info"] .toast-view-wrapped-icon:before { 524 | background-image: url("/assets/images/toast/information.svg"); 525 | } 526 | 527 | .toast[data-level="warning"] .toast-view { 528 | background-color: #FF8C25; 529 | } 530 | 531 | .toast[data-level="warning"] .toast-view-wrapped-icon:before, 532 | .toast[data-level="error"] .toast-view-wrapped-icon:before { 533 | background-image: url("/assets/images/toast/critical.svg"); 534 | } 535 | 536 | .toast[data-level="error"] .toast-view { 537 | background-color: #F14040; 538 | } 539 | 540 | .toast-view { 541 | color: #FFFFFF; 542 | min-height: 46px; 543 | border-top: 1px solid rgba(255, 255, 255, 0.15); 544 | } 545 | 546 | .toast-view-wrapped { 547 | line-height: 21px; 548 | overflow: hidden; 549 | font-size: 12.5px; 550 | letter-spacing: 0.1px; 551 | padding: 12px 36px 12px 70px; 552 | position: relative; 553 | display: block; 554 | z-index: 1; 555 | } 556 | 557 | .toast-view-wrapped-icon { 558 | height: 20px; 559 | position: absolute; 560 | left: 40px; 561 | } 562 | 563 | .toast-view-wrapped-icon:before { 564 | content: ""; 565 | background-size: contain; 566 | background-repeat: no-repeat; 567 | background-position: center; 568 | width: 16px; 569 | height: 16px; 570 | position: absolute; 571 | top: 50%; 572 | transform: translateY(-50%); 573 | } 574 | 575 | .toast-view-wrapped-message-sub { 576 | margin-left: 6px; 577 | } 578 | 579 | .toast-view-wrapped-close { 580 | background-image: url("/assets/images/toast/close.svg"); 581 | background-size: contain; 582 | background-repeat: no-repeat; 583 | background-position: center; 584 | width: 10px; 585 | height: 10px; 586 | margin-top: -1px; 587 | opacity: 0.85; 588 | position: absolute; 589 | top: 50%; 590 | right: 20px; 591 | transform: translateY(-50%); 592 | } 593 | 594 | .toast-view-wrapped-close:hover { 595 | opacity: 0.95; 596 | } 597 | 598 | .toast-view-wrapped-close:active { 599 | opacity: 1; 600 | } 601 | 602 | .toast-view-progress { 603 | background-color: rgba(0, 0, 0, 0.05); 604 | width: 100%; 605 | position: absolute; 606 | left: 0; 607 | top: 0; 608 | bottom: 0; 609 | } 610 | 611 | @keyframes lock-fade { 612 | from { 613 | opacity: 0; 614 | } 615 | 616 | to { 617 | opacity: 1; 618 | } 619 | } 620 | 621 | @keyframes modal-appear { 622 | 0% { 623 | opacity: 0; 624 | } 625 | 626 | 50% { 627 | opacity: 0; 628 | transform: scale3d(0.925, 0.925, 0.925); 629 | } 630 | 631 | 100% { 632 | opacity: 1; 633 | transform: scale3d(1, 1, 1); 634 | } 635 | } 636 | 637 | @keyframes popup-appear { 638 | 0% { 639 | opacity: 0; 640 | } 641 | 642 | 50% { 643 | opacity: 0; 644 | transform: translate3d(0, 4%, 0); 645 | } 646 | 647 | 100% { 648 | opacity: 1; 649 | transform: none; 650 | } 651 | } 652 | 653 | @keyframes toast-progress { 654 | from { 655 | transform: translate3d(0, 0, 0); 656 | } 657 | 658 | to { 659 | visibility: hidden; 660 | transform: translate3d(-100%, 0, 0); 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /res/assets/stylesheets/initiate.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #3B3F4F !important; 3 | } 4 | 5 | mark { 6 | font-size: 12.5px; 7 | text-align: center; 8 | line-height: 16px; 9 | letter-spacing: 0.1px; 10 | padding: 13px 12px 15px; 11 | display: block; 12 | position: relative; 13 | z-index: 1; 14 | animation-duration: 0.5s; 15 | animation-name: mark-slide; 16 | } 17 | 18 | mark.information { 19 | background-color: #FFEA00; 20 | color: #000000; 21 | } 22 | 23 | mark.notice { 24 | background-color: #F37C00; 25 | color: #FFFFFF; 26 | } 27 | 28 | mark.success { 29 | background-color: #04BB00; 30 | color: #FFFFFF; 31 | } 32 | 33 | mark.failure { 34 | background-color: #E10000; 35 | color: #FFFFFF; 36 | } 37 | 38 | main { 39 | width: 100%; 40 | max-width: 400px; 41 | margin: 0 auto; 42 | position: absolute; 43 | top: 50%; 44 | left: 50%; 45 | transform: translate(-50%, -50%); 46 | animation-duration: 0.5s; 47 | animation-name: main-appear; 48 | } 49 | 50 | main .logo { 51 | text-align: center; 52 | margin-bottom: 34px; 53 | display: block; 54 | } 55 | 56 | main .logo .logo-inner { 57 | display: inline-block; 58 | } 59 | 60 | main .logo img, 61 | main .logo .logo-label { 62 | vertical-align: middle; 63 | display: inline-block; 64 | } 65 | 66 | main .logo img { 67 | height: 30px; 68 | max-width: 200px; 69 | } 70 | 71 | main .logo .logo-label { 72 | color: rgba(255, 255, 255, 0.80); 73 | border-left: 1px solid rgba(255, 255, 255, 0.1); 74 | font-size: 15.5px; 75 | text-transform: lowercase; 76 | line-height: 24px; 77 | letter-spacing: -0.15px; 78 | margin-top: -2px; 79 | margin-left: 11px; 80 | padding-left: 14px; 81 | } 82 | 83 | main .error { 84 | background-color: #E10000; 85 | color: #FFFFFF; 86 | font-size: 12px; 87 | text-align: center; 88 | line-height: 12px; 89 | letter-spacing: 0.1px; 90 | margin-top: -12px; 91 | margin-bottom: 20px; 92 | padding: 14px 10px; 93 | display: block; 94 | border-radius: 2px; 95 | } 96 | 97 | main form label input, 98 | main form button { 99 | width: 100%; 100 | display: block; 101 | } 102 | 103 | main form label input { 104 | background: #FFFFFF; 105 | border: 0 none; 106 | font-size: 13.5px; 107 | letter-spacing: 0.1px; 108 | text-indent: 30px; 109 | outline: none; 110 | height: 51px; 111 | margin: 0 0 12px; 112 | padding: 0; 113 | box-shadow: 0 2px 3px 0 rgba(6, 15, 34, 0.12); 114 | border-radius: 2px; 115 | transition: box-shadow 0.2s linear; 116 | } 117 | 118 | main form label input:focus { 119 | box-shadow: 0 3px 8px 0 rgba(15, 31, 64, 0.15); 120 | } 121 | 122 | main form label input::-webkit-input-placeholder { 123 | color: rgba(0, 0, 0, 0.65); 124 | } 125 | 126 | main form label input::-moz-placeholder { 127 | color: rgba(0, 0, 0, 0.65); 128 | } 129 | 130 | main form label input:-ms-input-placeholder { 131 | color: rgba(0, 0, 0, 0.65); 132 | } 133 | 134 | main form label input:-moz-placeholder { 135 | color: rgba(0, 0, 0, 0.65); 136 | } 137 | 138 | main form button { 139 | margin-top: 22px; 140 | } 141 | 142 | main form button.button { 143 | box-shadow: 0 2px 3px 0 rgba(6, 15, 34, 0.12); 144 | } 145 | 146 | main form button.button:hover { 147 | box-shadow: 0 3px 8px 0 rgba(6, 15, 34, 0.175); 148 | } 149 | 150 | main nav { 151 | border-top: 1px solid rgba(255, 255, 255, 0.08); 152 | margin-top: 42px; 153 | } 154 | 155 | main nav ul { 156 | list-style-type: none; 157 | padding-top: 10px; 158 | } 159 | 160 | main nav ul li:nth-of-type(1) { 161 | float: left; 162 | } 163 | 164 | main nav ul li:nth-of-type(2) { 165 | float: right; 166 | } 167 | 168 | main nav ul li a { 169 | font-size: 12.5px; 170 | letter-spacing: -0.18px; 171 | } 172 | 173 | main nav ul li a:hover { 174 | text-decoration: underline; 175 | } 176 | 177 | main nav ul li:nth-of-type(1) a { 178 | color: #FFFFFF; 179 | } 180 | 181 | main nav ul li:nth-of-type(2) a { 182 | color: rgba(255, 255, 255, 0.75); 183 | } 184 | 185 | @keyframes mark-slide { 186 | from { 187 | transform: translateY(-100%); 188 | opacity: 0; 189 | } 190 | 191 | to { 192 | transform: none; 193 | opacity: 1; 194 | } 195 | } 196 | 197 | @keyframes main-appear { 198 | 0% { 199 | opacity: 0; 200 | } 201 | 202 | 15% { 203 | opacity: 0; 204 | } 205 | 206 | 100% { 207 | opacity: 1; 208 | } 209 | } 210 | 211 | @media (max-width: 480px) { 212 | main { 213 | max-width: calc(100% - 30px); 214 | margin-top: 60px; 215 | position: static; 216 | transform: none; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /res/assets/templates/__base.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block title %}{% endblock title %} | {{ config.page_title | escape }} 16 | 17 | {% block stylesheets %} 18 | 19 | {% endblock stylesheets %} 20 | 21 | {% block javascripts %}{% endblock javascripts %} 22 | 23 | {% if config.custom_html %} 24 | {{ config.custom_html | safe }} 25 | {% endif %} 26 | 27 | 28 | 29 | {% block body %}{% endblock body %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /res/assets/templates/__partial.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block body %}{% endblock body %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /res/assets/templates/_dashboard_header.tera: -------------------------------------------------------------------------------- 1 | {% macro header(config) %} 2 |
3 | 8 | 9 | 14 | 15 |
16 |
17 | {% endmacro header %} 18 | -------------------------------------------------------------------------------- /res/assets/templates/_dashboard_menu.tera: -------------------------------------------------------------------------------- 1 | {% macro menu(config, common, sidebar, selected, infobox_title, infobox_label) %} 2 | 58 | {% endmacro menu %} 59 | -------------------------------------------------------------------------------- /res/assets/templates/_dashboard_payouts.tera: -------------------------------------------------------------------------------- 1 | {% macro payouts(payouts, has_more) %} 2 |
3 |
4 |
    5 | {% if payouts | length > 0 %} 6 | {% for payout in payouts %} 7 |
  • 8 |
    9 |
    10 |
    Payout #{{ payout.number }}
    11 |

    {{ payout.date | escape }}

    12 |
    13 | 14 |
    15 |
    16 |

    Status

    17 |
    {{ payout.status | escape }}
    18 |
    19 | 20 |
    21 |

    Payout amount

    22 |
    {{ payout.currency | escape }} {{ payout.amount | escape }}
    23 |
    24 | 25 | {% if payout.account %} 26 |
    27 |

    Payout account

    28 |
    {{ payout.account | escape }}
    29 |
    30 | {% endif %} 31 | 32 |
    33 |
    34 | 35 |
    36 |
    37 | 38 | 43 | 44 |
    45 |
  • 46 | {% endfor %} 47 | {% else %} 48 |

    No payout was found. Request your first payout now!

    49 | {% endif %} 50 |
51 |
52 |
53 | 54 | {% if has_more %} 55 |
56 | Load older payouts 57 |
58 | {% endif %} 59 | {% endmacro payouts %} 60 | -------------------------------------------------------------------------------- /res/assets/templates/_dashboard_toast.tera: -------------------------------------------------------------------------------- 1 | {% macro toast() %} 2 |
3 |
4 |
5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 | {% endmacro toast %} 19 | -------------------------------------------------------------------------------- /res/assets/templates/dashboard_account.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% import "_dashboard_toast" as dashboard_toast %} 4 | {% import "_dashboard_header" as dashboard_header %} 5 | {% import "_dashboard_menu" as dashboard_menu %} 6 | 7 | {% block title %}Trackers | Dashboard{% endblock title %} 8 | 9 | {% block stylesheets %} 10 | {{ super() }} 11 | 12 | 13 | {% endblock stylesheets %} 14 | 15 | {% block javascripts %} 16 | {{ super() }} 17 | 18 | 19 | 20 | 21 | 32 | {% endblock javascripts %} 33 | 34 | {% block body %} 35 | {{ dashboard_header::header(config=config) }} 36 | 37 |
38 |
39 | {{ dashboard_menu::menu(config=config, common=common, sidebar="", selected="account", infobox_title="Manage your account.", infobox_label="Configure your bank account and legal information for payouts.") }} 40 | 41 |
42 |
43 |
44 |
45 |
46 |

Account

47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 60 | 61 | 70 | 71 | 82 |
83 |
84 | 85 |
86 | 87 | 88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 |
96 |
97 |

Payout recipient

98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 |

As every affiliate country is different, we don’t have a strict way to collect recipient account details.

106 | 107 |

Thus, we ask you to select the recipient account type (eg. PayPal) and manually write the account details (eg. PayPal email).

108 |

We also ask you to enter your country, address and full name for invoicing purposes.

109 |
110 | 111 |
112 | 118 | 119 | 125 | 126 | 141 | 142 | 157 | 158 | 164 |
165 |
166 | 167 |
168 | 169 | 170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | 178 | {{ dashboard_toast::toast() }} 179 | {% endblock body %} 180 | -------------------------------------------------------------------------------- /res/assets/templates/dashboard_payouts.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% import "_dashboard_toast" as dashboard_toast %} 4 | {% import "_dashboard_header" as dashboard_header %} 5 | {% import "_dashboard_menu" as dashboard_menu %} 6 | {% import "_dashboard_payouts" as dashboard_payouts %} 7 | 8 | {% block title %}Trackers | Dashboard{% endblock title %} 9 | 10 | {% block stylesheets %} 11 | {{ super() }} 12 | 13 | 14 | {% endblock stylesheets %} 15 | 16 | {% block javascripts %} 17 | {{ super() }} 18 | 19 | 20 | 21 | 22 | 37 | {% endblock javascripts %} 38 | 39 | {% block body %} 40 | {{ dashboard_header::header(config=config) }} 41 | 42 |
43 |
44 | {{ dashboard_menu::menu(config=config, common=common, sidebar="", selected="payouts", infobox_title="Manage your payouts.", infobox_label="Get the money you earned wired to your bank and retrieve invoices.") }} 45 | 46 |
47 |
48 |
49 |
50 |
51 |

Payouts

52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 |
62 |
Your unpaid balance:
63 |

{{ config.payout_currency | escape }} {{ common.balance_pending | escape }}

64 |
65 | 66 | Request payout of {{ config.payout_currency | escape }} {{ common.balance_pending | escape }} 67 |
68 | 69 |
70 |
71 |
You earned in total:
72 |

{{ config.payout_currency | escape }} {{ balance_total | escape }}

73 | 74 |

This includes your paid and unpaid balance.

75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 |

Payout history

90 | 91 |
({{ payouts_total }} payout{{ payouts_total | pluralize }})
92 |
93 | 94 |
95 |
96 | 97 | {{ dashboard_payouts::payouts(payouts=payouts, has_more=has_more) }} 98 |
99 |
100 |
101 |
102 |
103 | 104 | 127 | 128 | {{ dashboard_toast::toast() }} 129 | {% endblock body %} 130 | -------------------------------------------------------------------------------- /res/assets/templates/dashboard_payouts_partial_payouts.tera: -------------------------------------------------------------------------------- 1 | {% extends "__partial" %} 2 | 3 | {% import "_dashboard_payouts" as dashboard_payouts %} 4 | 5 | {% block body %} 6 | {{ dashboard_payouts::payouts(payouts=payouts, has_more=has_more) }} 7 | {% endblock body %} 8 | -------------------------------------------------------------------------------- /res/assets/templates/dashboard_trackers.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% import "_dashboard_toast" as dashboard_toast %} 4 | {% import "_dashboard_header" as dashboard_header %} 5 | {% import "_dashboard_menu" as dashboard_menu %} 6 | 7 | {% block title %}Trackers | Dashboard{% endblock title %} 8 | 9 | {% block stylesheets %} 10 | {{ super() }} 11 | 12 | 13 | {% endblock stylesheets %} 14 | 15 | {% block javascripts %} 16 | {{ super() }} 17 | 18 | 19 | 20 | 21 | 36 | {% endblock javascripts %} 37 | 38 | {% block body %} 39 | {{ dashboard_header::header(config=config) }} 40 | 41 |
42 |
43 | {{ dashboard_menu::menu(config=config, common=common, sidebar="", selected="trackers", infobox_title="Manage your trackers.", infobox_label="Send those tracker links to people and start earning money.") }} 44 | 45 |
46 |
47 |
48 |
49 |
50 |
51 |

Trackers

52 | 53 | {% if trackers | length > 0 %} 54 |
({{ trackers | length }} tracker{{ trackers | length | pluralize }})
55 | {% endif %} 56 |
57 | 58 | 67 | 68 |
69 |
70 | 71 |
72 |
73 |
    74 | {% if trackers | length > 0 %} 75 | {% for tracker in trackers %} 76 |
  • 77 |
    78 | 79 | 80 | 83 |
    84 | 85 |
    86 |
    87 |
    {{ tracker.label | escape }}
    88 | 89 |
    90 | 91 |
    92 |
    93 |

    Signups

    94 |
    {{ tracker.statistics_signups | escape }}
    95 |
    96 | 97 |
    98 |

    Earned so far

    99 |
    {{ config.payout_currency | escape }} {{ tracker.total_earned | escape }}
    100 |
    101 | 102 |
    103 |
    104 | 105 |
    106 |
    107 | 108 |
    109 | Copy tracker link 110 | 111 | {% if config.banners | length > 0 %} 112 | View banners 113 | {% endif %} 114 | 115 |
    116 |
    117 | 118 |
    119 |
  • 120 | {% endfor %} 121 | {% else %} 122 |

    No tracker was found. Add your first tracker now!

    123 | {% endif %} 124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | 134 | 159 | 160 | 181 | 182 | 214 | 215 | {{ dashboard_toast::toast() }} 216 | {% endblock body %} 217 | -------------------------------------------------------------------------------- /res/assets/templates/dashboard_welcome.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% import "_dashboard_toast" as dashboard_toast %} 4 | {% import "_dashboard_header" as dashboard_header %} 5 | {% import "_dashboard_menu" as dashboard_menu %} 6 | 7 | {% block title %}Dashboard{% endblock title %} 8 | 9 | {% block stylesheets %} 10 | {{ super() }} 11 | 12 | 13 | {% endblock stylesheets %} 14 | 15 | {% block javascripts %} 16 | {{ super() }} 17 | 18 | 19 | 20 | 27 | {% endblock javascripts %} 28 | 29 | {% block body %} 30 | {{ dashboard_header::header(config=config) }} 31 | 32 |
33 |
34 | {{ dashboard_menu::menu(config=config, common=common, sidebar="animated", selected="", infobox_title="This is your affiliates dashboard.", infobox_label="Manage your trackers, payouts & account. Pick a category below.") }} 35 |
36 |
37 | 38 | 58 | 59 | {{ dashboard_toast::toast() }} 60 | {% endblock body %} 61 | -------------------------------------------------------------------------------- /res/assets/templates/initiate_login.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block stylesheets %} 6 | {{ super() }} 7 | 8 | 9 | {% endblock stylesheets %} 10 | 11 | {% block body %} 12 | {% if failure %} 13 | Oops. Your credentials are invalid. Please enter them again and retry. 14 | {% else %} 15 | Please sign in to access your {{ config.page_title | escape }} dashboard. 16 | {% endif %} 17 | 18 |
19 | 26 | 27 | {% if failure %} 28 |

Invalid credentials. Try again.

29 | {% endif %} 30 | 31 |
32 | 35 | 36 | 39 | 40 | 41 |
42 | 43 | 58 |
59 | {% endblock body %} 60 | -------------------------------------------------------------------------------- /res/assets/templates/initiate_recover.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% block title %}Recover{% endblock title %} 4 | 5 | {% block stylesheets %} 6 | {{ super() }} 7 | 8 | 9 | {% endblock stylesheets %} 10 | 11 | {% block body %} 12 | {% if failure %} 13 | No account was found for the email you entered. 14 | {% elif success %} 15 | An account recovery email is on its way to your mailbox. Follow the instructions there. 16 | {% else %} 17 | Enter your email address to recover your account. You will receive a recovery email. 18 | {% endif %} 19 | 20 |
21 | 28 | 29 | {% if failure %} 30 |

Could not recover account (email not found).

31 | {% endif %} 32 | 33 |
34 | 37 | 38 | 39 |
40 | 41 | 50 |
51 | {% endblock body %} 52 | -------------------------------------------------------------------------------- /res/assets/templates/initiate_signup.tera: -------------------------------------------------------------------------------- 1 | {% extends "__base" %} 2 | 3 | {% block title %}Signup{% endblock title %} 4 | 5 | {% block stylesheets %} 6 | {{ super() }} 7 | 8 | 9 | {% endblock stylesheets %} 10 | 11 | {% block body %} 12 | {% if failure %} 13 | Oops. The account could not be created. Please check your credentials and try again. 14 | {% else %} 15 | Signup for a new {{ config.page_title | escape }} account to access your dashboard. 16 | {% endif %} 17 | 18 |
19 | 26 | 27 | {% if failure %} 28 |

Could not create an account with this email.

29 | {% endif %} 30 | 31 |
32 | 35 | 36 | 39 | 40 | 43 | 44 | 45 |
46 | 47 | 56 |
57 | {% endblock body %} 58 | -------------------------------------------------------------------------------- /scripts/build_packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Raider 5 | # 6 | # Affiliates dashboard 7 | # Copyright: 2023, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Define build pipeline 12 | function build_for_target { 13 | OS="$2" DIST="$3" ARCH="$1" ./packpack/packpack 14 | release_result=$? 15 | 16 | if [ $release_result -eq 0 ]; then 17 | mkdir -p "./packages/$2_$3/" 18 | mv ./build/*$4 "./packages/$2_$3/" 19 | 20 | echo "Result: Packaged architecture: $1 for OS: $2:$3 (*$4)" 21 | fi 22 | 23 | return $release_result 24 | } 25 | 26 | # Run release tasks 27 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 28 | BASE_DIR="$ABSPATH/../" 29 | 30 | rc=0 31 | 32 | pushd "$BASE_DIR" > /dev/null 33 | echo "Executing packages build steps for Raider..." 34 | 35 | # Initialize `packpack` 36 | rm -rf ./packpack && \ 37 | git clone https://github.com/packpack/packpack.git packpack 38 | rc=$? 39 | 40 | # Proceed build for each target? 41 | if [ $rc -eq 0 ]; then 42 | build_for_target "x86_64" "debian" "bookworm" ".deb" 43 | rc=$? 44 | fi 45 | 46 | # Cleanup environment 47 | rm -rf ./build ./packpack 48 | 49 | if [ $rc -eq 0 ]; then 50 | echo "Success: Done executing packages build steps for Raider" 51 | else 52 | echo "Error: Failed executing packages build steps for Raider" 53 | fi 54 | popd > /dev/null 55 | 56 | exit $rc 57 | -------------------------------------------------------------------------------- /scripts/release_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Raider 5 | # 6 | # Affiliates dashboard 7 | # Copyright: 2020, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Read arguments 12 | while [ "$1" != "" ]; do 13 | argument_key=`echo $1 | awk -F= '{print $1}'` 14 | argument_value=`echo $1 | awk -F= '{print $2}'` 15 | 16 | case $argument_key in 17 | -v | --version) 18 | # Notice: strip any leading 'v' to the version number 19 | RAIDER_VERSION="${argument_value/v}" 20 | ;; 21 | *) 22 | echo "Unknown argument received: '$argument_key'" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | shift 28 | done 29 | 30 | # Ensure release version is provided 31 | if [ -z "$RAIDER_VERSION" ]; then 32 | echo "No Raider release version was provided, please provide it using '--version'" 33 | 34 | exit 1 35 | fi 36 | 37 | # Define release pipeline 38 | function release_for_architecture { 39 | final_tar="v$RAIDER_VERSION-$1-$2.tar.gz" 40 | 41 | rm -rf ./raider/ && \ 42 | cargo build --target "$3" --release && \ 43 | mkdir ./raider && \ 44 | cp -p "target/$3/release/raider" ./raider/ && \ 45 | cp -r ./config.cfg ./res raider/ && \ 46 | tar --owner=0 --group=0 -czvf "$final_tar" ./raider && \ 47 | rm -r ./raider/ 48 | release_result=$? 49 | 50 | if [ $release_result -eq 0 ]; then 51 | echo "Result: Packed architecture: $1 ($2) to file: $final_tar" 52 | fi 53 | 54 | return $release_result 55 | } 56 | 57 | # Run release tasks 58 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 59 | BASE_DIR="$ABSPATH/../" 60 | 61 | rc=0 62 | 63 | pushd "$BASE_DIR" > /dev/null 64 | echo "Executing release steps for Raider v$RAIDER_VERSION..." 65 | 66 | release_for_architecture "x86_64" "gnu" "x86_64-unknown-linux-gnu" 67 | rc=$? 68 | 69 | if [ $rc -eq 0 ]; then 70 | echo "Success: Done executing release steps for Raider v$RAIDER_VERSION" 71 | else 72 | echo "Error: Failed executing release steps for Raider v$RAIDER_VERSION" 73 | fi 74 | popd > /dev/null 75 | 76 | exit $rc 77 | -------------------------------------------------------------------------------- /scripts/sign_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # Raider 5 | # 6 | # Affiliates dashboard 7 | # Copyright: 2020, Valerian Saliou 8 | # License: Mozilla Public License v2.0 (MPL v2.0) 9 | ## 10 | 11 | # Read arguments 12 | while [ "$1" != "" ]; do 13 | argument_key=`echo $1 | awk -F= '{print $1}'` 14 | argument_value=`echo $1 | awk -F= '{print $2}'` 15 | 16 | case $argument_key in 17 | -v | --version) 18 | # Notice: strip any leading 'v' to the version number 19 | RAIDER_VERSION="${argument_value/v}" 20 | ;; 21 | *) 22 | echo "Unknown argument received: '$argument_key'" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | shift 28 | done 29 | 30 | # Ensure release version is provided 31 | if [ -z "$RAIDER_VERSION" ]; then 32 | echo "No Raider release version was provided, please provide it using '--version'" 33 | 34 | exit 1 35 | fi 36 | 37 | # Define sign pipeline 38 | function sign_for_architecture { 39 | final_tar="v$RAIDER_VERSION-$1-$2.tar.gz" 40 | gpg_signer="valerian@valeriansaliou.name" 41 | 42 | gpg -u "$gpg_signer" --armor --detach-sign "$final_tar" 43 | sign_result=$? 44 | 45 | if [ $sign_result -eq 0 ]; then 46 | echo "Result: Signed architecture: $1 ($2) for file: $final_tar" 47 | fi 48 | 49 | return $sign_result 50 | } 51 | 52 | # Run sign tasks 53 | ABSPATH=$(cd "$(dirname "$0")"; pwd) 54 | BASE_DIR="$ABSPATH/../" 55 | 56 | rc=0 57 | 58 | pushd "$BASE_DIR" > /dev/null 59 | echo "Executing sign steps for Raider v$RAIDER_VERSION..." 60 | 61 | sign_for_architecture "x86_64" "gnu" 62 | rc=$? 63 | 64 | if [ $rc -eq 0 ]; then 65 | echo "Success: Done executing sign steps for Raider v$RAIDER_VERSION" 66 | else 67 | echo "Error: Failed executing sign steps for Raider v$RAIDER_VERSION" 68 | fi 69 | popd > /dev/null 70 | 71 | exit $rc 72 | -------------------------------------------------------------------------------- /src/config/config.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::net::SocketAddr; 8 | use std::path::PathBuf; 9 | 10 | use url_serde::SerdeUrl; 11 | 12 | use super::defaults; 13 | 14 | #[derive(Deserialize)] 15 | pub struct Config { 16 | pub server: ConfigServer, 17 | pub database: ConfigDatabase, 18 | pub exchange: ConfigExchange, 19 | pub email: ConfigEmail, 20 | pub assets: ConfigAssets, 21 | pub branding: ConfigBranding, 22 | pub tracker: ConfigTracker, 23 | pub payout: ConfigPayout, 24 | } 25 | 26 | #[derive(Deserialize)] 27 | pub struct ConfigServer { 28 | #[serde(default = "defaults::server_log_level")] 29 | pub log_level: String, 30 | 31 | #[serde(default = "defaults::server_inet")] 32 | pub inet: SocketAddr, 33 | 34 | #[serde(default = "defaults::server_workers")] 35 | pub workers: u16, 36 | 37 | pub track_token: String, 38 | pub management_token: String, 39 | pub secret_key: String, 40 | } 41 | 42 | #[derive(Deserialize)] 43 | pub struct ConfigDatabase { 44 | pub url: SerdeUrl, 45 | 46 | #[serde(default = "defaults::database_pool_size")] 47 | pub pool_size: u32, 48 | 49 | #[serde(default = "defaults::database_idle_timeout")] 50 | pub idle_timeout: u64, 51 | 52 | #[serde(default = "defaults::database_connection_timeout")] 53 | pub connection_timeout: u64, 54 | 55 | pub password_salt: String, 56 | 57 | #[serde(default = "defaults::database_account_create_allow")] 58 | pub account_create_allow: bool, 59 | } 60 | 61 | #[derive(Deserialize)] 62 | pub struct ConfigExchange { 63 | pub fixer: ConfigExchangeFixer, 64 | } 65 | 66 | #[derive(Deserialize)] 67 | pub struct ConfigExchangeFixer { 68 | #[serde(default = "defaults::exchange_fixer_endpoint")] 69 | pub endpoint: String, 70 | 71 | pub api_key: String, 72 | } 73 | 74 | #[derive(Deserialize)] 75 | pub struct ConfigEmail { 76 | pub from: String, 77 | 78 | #[serde(default = "defaults::email_smtp_host")] 79 | pub smtp_host: String, 80 | 81 | #[serde(default = "defaults::email_smtp_port")] 82 | pub smtp_port: u16, 83 | 84 | pub smtp_username: Option, 85 | pub smtp_password: Option, 86 | 87 | #[serde(default = "defaults::email_smtp_encrypt")] 88 | pub smtp_encrypt: bool, 89 | } 90 | 91 | #[derive(Deserialize)] 92 | pub struct ConfigAssets { 93 | #[serde(default = "defaults::assets_path")] 94 | pub path: PathBuf, 95 | } 96 | 97 | #[derive(Deserialize)] 98 | pub struct ConfigBranding { 99 | #[serde(default = "defaults::branding_page_title")] 100 | pub page_title: String, 101 | 102 | pub page_url: SerdeUrl, 103 | pub help_url: SerdeUrl, 104 | pub support_url: SerdeUrl, 105 | pub icon_color: String, 106 | pub icon_url: SerdeUrl, 107 | pub logo_white_url: SerdeUrl, 108 | pub logo_dark_url: SerdeUrl, 109 | pub custom_html: Option, 110 | } 111 | 112 | #[derive(Deserialize)] 113 | pub struct ConfigTracker { 114 | pub track_url: String, 115 | 116 | #[serde(default = "defaults::tracker_track_parameter")] 117 | pub track_parameter: String, 118 | 119 | #[serde(default = "defaults::tracker_commission_default")] 120 | pub commission_default: f32, 121 | 122 | #[serde(default = "defaults::tracker_banner")] 123 | pub banner: Vec, 124 | } 125 | 126 | #[derive(Deserialize)] 127 | pub struct ConfigTrackerBanner { 128 | pub banner_url: SerdeUrl, 129 | pub size_width: u16, 130 | pub size_height: u16, 131 | } 132 | 133 | #[derive(Deserialize)] 134 | pub struct ConfigPayout { 135 | #[serde(default = "defaults::payout_currency")] 136 | pub currency: String, 137 | 138 | #[serde(default = "defaults::payout_amount_minimum")] 139 | pub amount_minimum: f32, 140 | 141 | pub administrator_email: String, 142 | } 143 | -------------------------------------------------------------------------------- /src/config/defaults.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::net::SocketAddr; 8 | use std::path::PathBuf; 9 | 10 | use super::config::ConfigTrackerBanner; 11 | 12 | pub fn server_log_level() -> String { 13 | "error".to_string() 14 | } 15 | 16 | pub fn server_inet() -> SocketAddr { 17 | "[::1]:8080".parse().unwrap() 18 | } 19 | 20 | pub fn server_workers() -> u16 { 21 | 4 22 | } 23 | 24 | pub fn database_pool_size() -> u32 { 25 | 4 26 | } 27 | 28 | pub fn database_idle_timeout() -> u64 { 29 | 300 30 | } 31 | 32 | pub fn database_connection_timeout() -> u64 { 33 | 10 34 | } 35 | 36 | pub fn database_account_create_allow() -> bool { 37 | true 38 | } 39 | 40 | pub fn exchange_fixer_endpoint() -> String { 41 | "https://api.apilayer.com/fixer".to_string() 42 | } 43 | 44 | pub fn email_smtp_host() -> String { 45 | "localhost".to_string() 46 | } 47 | 48 | pub fn email_smtp_port() -> u16 { 49 | 587 50 | } 51 | 52 | pub fn email_smtp_encrypt() -> bool { 53 | true 54 | } 55 | 56 | pub fn assets_path() -> PathBuf { 57 | PathBuf::from("./res/assets/") 58 | } 59 | 60 | pub fn branding_page_title() -> String { 61 | "Affiliates".to_string() 62 | } 63 | 64 | pub fn tracker_track_parameter() -> String { 65 | "t".to_string() 66 | } 67 | 68 | pub fn tracker_commission_default() -> f32 { 69 | 0.20 70 | } 71 | 72 | pub fn tracker_banner() -> Vec { 73 | Vec::new() 74 | } 75 | 76 | pub fn payout_currency() -> String { 77 | "EUR".to_string() 78 | } 79 | 80 | pub fn payout_amount_minimum() -> f32 { 81 | 100.00 82 | } 83 | -------------------------------------------------------------------------------- /src/config/logger.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use log; 8 | use log::{LogLevel, LogLevelFilter, LogMetadata, LogRecord, SetLoggerError}; 9 | 10 | pub struct ConfigLogger; 11 | 12 | impl log::Log for ConfigLogger { 13 | fn enabled(&self, metadata: &LogMetadata) -> bool { 14 | metadata.level() <= LogLevel::Debug 15 | } 16 | 17 | fn log(&self, record: &LogRecord) { 18 | if self.enabled(record.metadata()) { 19 | println!("({}) - {}", record.level(), record.args()); 20 | } 21 | } 22 | } 23 | 24 | impl ConfigLogger { 25 | pub fn init(level: LogLevelFilter) -> Result<(), SetLoggerError> { 26 | log::set_logger(|max_log_level| { 27 | max_log_level.set(level); 28 | Box::new(ConfigLogger) 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | mod defaults; 8 | 9 | pub mod config; 10 | pub mod logger; 11 | pub mod reader; 12 | -------------------------------------------------------------------------------- /src/config/reader.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use log; 8 | use std::fs::File; 9 | use std::io::Read; 10 | use toml; 11 | 12 | use super::config::*; 13 | use APP_ARGS; 14 | 15 | pub struct ConfigReader; 16 | 17 | impl ConfigReader { 18 | pub fn make() -> Config { 19 | log::debug!("reading config file: {}", &APP_ARGS.config); 20 | 21 | let mut file = File::open(&APP_ARGS.config).expect("cannot find config file"); 22 | let mut conf = String::new(); 23 | 24 | file.read_to_string(&mut conf) 25 | .expect("cannot read config file"); 26 | 27 | log::debug!("read config file: {}", &APP_ARGS.config); 28 | 29 | toml::from_str(&conf).expect("syntax error in config file") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/exchange/manager.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use log; 8 | use reqwest::blocking::Client; 9 | use reqwest::StatusCode; 10 | use std::collections::HashMap; 11 | use std::sync::Arc; 12 | use std::sync::RwLock; 13 | use std::thread; 14 | use std::time::Duration; 15 | 16 | use APP_CONF; 17 | 18 | const POLL_RATE_SECONDS: u64 = 259200; 19 | const RETRY_POLL_SECONDS: u64 = 60; 20 | const RETRY_POLL_ATTEMPTS_LIMIT: u16 = 2; 21 | 22 | lazy_static! { 23 | static ref RATES: Arc>> = Arc::new(RwLock::new(HashMap::new())); 24 | static ref HTTP_CLIENT: Client = Client::builder() 25 | .timeout(Duration::from_secs(20)) 26 | .gzip(true) 27 | .build() 28 | .unwrap(); 29 | } 30 | 31 | #[derive(Deserialize)] 32 | struct FixerLatestResponse { 33 | rates: HashMap, 34 | } 35 | 36 | fn store_rates(rates: HashMap) { 37 | let mut store = RATES.write().unwrap(); 38 | 39 | *store = rates; 40 | } 41 | 42 | fn update_rates(retry_count: u16) -> Result<(), ()> { 43 | log::debug!("acquiring updated exchange rates"); 44 | 45 | // Acquire latest rates from Fixer 46 | let response = HTTP_CLIENT 47 | .get(&format!( 48 | "{}/latest?base={}", 49 | &APP_CONF.exchange.fixer.endpoint, &APP_CONF.payout.currency 50 | )) 51 | .header("apikey", &APP_CONF.exchange.fixer.api_key) 52 | .send(); 53 | 54 | if let Ok(response_inner) = response { 55 | let status = response_inner.status(); 56 | 57 | log::debug!("received updated exchange rates"); 58 | 59 | if status == StatusCode::OK { 60 | if let Ok(response_json) = response_inner.json::() { 61 | log::debug!("got updated exchange rates: {:?}", &response_json.rates); 62 | 63 | store_rates(response_json.rates); 64 | 65 | return Ok(()); 66 | } else { 67 | log::error!("got invalid json when requesting updated exchange rates") 68 | } 69 | } else { 70 | log::error!("got bad status code when requesting updated exchange rates") 71 | } 72 | } else { 73 | log::error!("could not request updated exchange rates"); 74 | } 75 | 76 | // Re-schedule an update after a few seconds? (if retry count not over limit) 77 | if retry_count <= RETRY_POLL_ATTEMPTS_LIMIT { 78 | log::info!( 79 | "scheduled an exchange rates update retry in {} seconds", 80 | RETRY_POLL_SECONDS 81 | ); 82 | 83 | thread::sleep(Duration::from_secs(RETRY_POLL_SECONDS)); 84 | 85 | return update_rates(retry_count + 1); 86 | } 87 | 88 | log::error!( 89 | "exceeded exchange rates update retry limit of {} attempts", 90 | RETRY_POLL_ATTEMPTS_LIMIT 91 | ); 92 | 93 | // Failed to update rates (all retry attempts exceeded) 94 | return Err(()); 95 | } 96 | 97 | pub fn normalize(amount: f32, currency: &str) -> Result { 98 | if currency == APP_CONF.payout.currency { 99 | Ok(amount) 100 | } else { 101 | if let Ok(ref store) = RATES.read() { 102 | if let Some(rate) = store.get(currency) { 103 | if rate > &0.0 { 104 | Ok((1.0 / rate) * amount) 105 | } else { 106 | Err(()) 107 | } 108 | } else { 109 | Err(()) 110 | } 111 | } else { 112 | Err(()) 113 | } 114 | } 115 | } 116 | 117 | pub fn run() { 118 | loop { 119 | log::debug!("running an exchange poll operation..."); 120 | 121 | update_rates(0).ok(); 122 | 123 | log::info!("ran exchange poll operation"); 124 | 125 | // Hold for next poll run 126 | thread::sleep(Duration::from_secs(POLL_RATE_SECONDS)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/exchange/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod manager; 8 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #![feature(proc_macro_hygiene, decl_macro)] 8 | 9 | #[macro_use(log)] 10 | extern crate log; 11 | #[macro_use] 12 | extern crate clap; 13 | #[macro_use] 14 | extern crate lazy_static; 15 | #[macro_use] 16 | extern crate diesel; 17 | #[macro_use] 18 | extern crate serde_derive; 19 | #[macro_use] 20 | extern crate rocket; 21 | extern crate base64; 22 | extern crate bigdecimal; 23 | extern crate chrono; 24 | extern crate iso_country; 25 | extern crate lettre; 26 | extern crate lettre_email; 27 | extern crate native_tls; 28 | extern crate num_traits; 29 | extern crate openssl_probe; 30 | extern crate r2d2; 31 | extern crate r2d2_diesel; 32 | extern crate rand; 33 | extern crate reqwest; 34 | extern crate rocket_contrib; 35 | extern crate separator; 36 | extern crate sha2; 37 | extern crate time; 38 | extern crate toml; 39 | extern crate url_serde; 40 | extern crate validate; 41 | 42 | mod config; 43 | mod exchange; 44 | mod management; 45 | mod notifier; 46 | mod responder; 47 | mod storage; 48 | mod track; 49 | 50 | use std::ops::Deref; 51 | use std::str::FromStr; 52 | use std::thread; 53 | use std::time::Duration; 54 | 55 | use clap::{App, Arg}; 56 | use log::LogLevelFilter; 57 | 58 | use config::config::Config; 59 | use config::logger::ConfigLogger; 60 | use config::reader::ConfigReader; 61 | use exchange::manager::run as run_exchange; 62 | use responder::manager::run as run_responder; 63 | 64 | struct AppArgs { 65 | config: String, 66 | } 67 | 68 | pub static THREAD_NAME_EXCHANGE: &'static str = "raider-exchange"; 69 | pub static THREAD_NAME_RESPONDER: &'static str = "raider-responder"; 70 | 71 | macro_rules! gen_spawn_managed { 72 | ($name:expr, $method:ident, $thread_name:ident, $managed_fn:ident) => { 73 | fn $method() { 74 | log::debug!("spawn managed thread: {}", $name); 75 | 76 | let worker = thread::Builder::new() 77 | .name($thread_name.to_string()) 78 | .spawn($managed_fn); 79 | 80 | // Block on worker thread (join it) 81 | let has_error = if let Ok(worker_thread) = worker { 82 | worker_thread.join().is_err() 83 | } else { 84 | true 85 | }; 86 | 87 | // Worker thread crashed? 88 | if has_error == true { 89 | log::error!("managed thread crashed ({}), setting it up again", $name); 90 | 91 | // Prevents thread start loop floods 92 | thread::sleep(Duration::from_secs(1)); 93 | 94 | $method(); 95 | } 96 | } 97 | }; 98 | } 99 | 100 | lazy_static! { 101 | static ref APP_ARGS: AppArgs = make_app_args(); 102 | static ref APP_CONF: Config = ConfigReader::make(); 103 | } 104 | 105 | gen_spawn_managed!( 106 | "exchange", 107 | spawn_exchange, 108 | THREAD_NAME_EXCHANGE, 109 | run_exchange 110 | ); 111 | gen_spawn_managed!( 112 | "responder", 113 | spawn_responder, 114 | THREAD_NAME_RESPONDER, 115 | run_responder 116 | ); 117 | 118 | fn make_app_args() -> AppArgs { 119 | let matches = App::new(crate_name!()) 120 | .version(crate_version!()) 121 | .author(crate_authors!()) 122 | .about(crate_description!()) 123 | .arg( 124 | Arg::with_name("config") 125 | .short("c") 126 | .long("config") 127 | .help("Path to configuration file") 128 | .default_value("./config.cfg") 129 | .takes_value(true), 130 | ) 131 | .get_matches(); 132 | 133 | // Generate owned app arguments 134 | AppArgs { 135 | config: String::from(matches.value_of("config").expect("invalid config value")), 136 | } 137 | } 138 | 139 | fn ensure_states() { 140 | // Ensure all statics are valid (a `deref` is enough to lazily initialize them) 141 | let (_, _) = (APP_ARGS.deref(), APP_CONF.deref()); 142 | 143 | // Ensure assets path exists 144 | assert_eq!( 145 | APP_CONF.assets.path.exists(), 146 | true, 147 | "assets directory not found: {:?}", 148 | APP_CONF.assets.path 149 | ); 150 | } 151 | 152 | fn main() { 153 | // Ensure OpenSSL root chain is found on current environment 154 | openssl_probe::init_ssl_cert_env_vars(); 155 | 156 | // Initialize shared logger 157 | let _logger = ConfigLogger::init( 158 | LogLevelFilter::from_str(&APP_CONF.server.log_level).expect("invalid log level"), 159 | ); 160 | 161 | log::info!("starting up"); 162 | 163 | // Ensure all states are bound 164 | ensure_states(); 165 | 166 | // Spawn exchange (background thread) 167 | thread::spawn(spawn_exchange); 168 | 169 | // Spawn Web responder (foreground thread) 170 | spawn_responder(); 171 | 172 | log::error!("could not start"); 173 | } 174 | -------------------------------------------------------------------------------- /src/management/account.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use bigdecimal::BigDecimal; 8 | use chrono::offset::Utc; 9 | use diesel; 10 | use diesel::prelude::*; 11 | use validate::rules::email as validate_email; 12 | 13 | use notifier::email::EmailNotifier; 14 | use responder::auth_guard::password_generate as auth_password_generate; 15 | use storage::db::DbConn; 16 | use storage::schemas::account::dsl::{ 17 | account, address as account_address, commission as account_commission, 18 | country as account_country, created_at as account_created_at, email as account_email, 19 | full_name as account_full_name, password as account_password, updated_at as account_updated_at, 20 | }; 21 | use APP_CONF; 22 | 23 | pub enum HandleAccountError { 24 | Aborted, 25 | Duplicate, 26 | InvalidEmail, 27 | } 28 | 29 | pub fn handle_account( 30 | db: &DbConn, 31 | email: &str, 32 | full_name: &Option, 33 | address: &Option, 34 | country: &Option, 35 | ) -> Result<(), HandleAccountError> { 36 | log::debug!("account management handle: {}", email); 37 | 38 | // Validate email address against policy 39 | if email.is_empty() == false && validate_email().validate(email).is_ok() == true { 40 | // Auto-generate a strong random password 41 | let password_params = auth_password_generate(); 42 | 43 | // Insert account 44 | let now_date = Utc::now().naive_utc(); 45 | 46 | let insert_result = diesel::insert_into(account) 47 | .values(( 48 | &account_email.eq(email), 49 | &account_password.eq(&password_params.0), 50 | &account_full_name.eq(full_name), 51 | &account_address.eq(address), 52 | &account_country.eq(country), 53 | &account_commission.eq(BigDecimal::from(APP_CONF.tracker.commission_default)), 54 | &account_created_at.eq(&now_date), 55 | &account_updated_at.eq(&now_date), 56 | )) 57 | .execute(&**db); 58 | 59 | if insert_result.is_ok() == true { 60 | log::debug!( 61 | "will send created account password to email: {} with password: {}", 62 | email, 63 | password_params.1 64 | ); 65 | 66 | // Generate account password message 67 | let mut message = String::new(); 68 | 69 | message.push_str("Hi,\n\n"); 70 | 71 | message.push_str(&format!( 72 | "Your {} account with email: {} has been created.\n", 73 | &APP_CONF.branding.page_title, email 74 | )); 75 | 76 | message.push_str(&format!( 77 | "Please login with this password to access your dashboard: {}\n\n", 78 | password_params.1 79 | )); 80 | 81 | message.push_str("You may change this password once logged in."); 82 | 83 | // Send account password email 84 | if EmailNotifier::dispatch(email, "Your account password".to_string(), &message).is_ok() 85 | == true 86 | { 87 | Ok(()) 88 | } else { 89 | log::warn!("account password: {} could not be emailed", email); 90 | 91 | Err(HandleAccountError::Aborted) 92 | } 93 | } else { 94 | // Account is likely duplicate (as there was a database failure) 95 | log::warn!( 96 | "account: {} could not be created due to database rejection", 97 | email 98 | ); 99 | 100 | Err(HandleAccountError::Duplicate) 101 | } 102 | } else { 103 | log::warn!( 104 | "account: {} could not be created due to invalid email", 105 | email 106 | ); 107 | 108 | Err(HandleAccountError::InvalidEmail) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/management/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod account; 8 | -------------------------------------------------------------------------------- /src/notifier/email.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use lettre::smtp::authentication::Credentials; 8 | use lettre::smtp::client::net::ClientTlsParameters; 9 | use lettre::smtp::{ClientSecurity, ConnectionReuseParameters}; 10 | use lettre::{SmtpClient, SmtpTransport, Transport}; 11 | use lettre_email::Email; 12 | use log; 13 | use native_tls::TlsConnector; 14 | use std::time::Duration; 15 | 16 | use APP_CONF; 17 | 18 | pub struct EmailNotifier; 19 | 20 | impl EmailNotifier { 21 | pub fn dispatch(to: &str, subject: String, body: &str) -> Result<(), bool> { 22 | // Build up the message text 23 | let mut message = String::new(); 24 | 25 | message.push_str(body); 26 | message.push_str("\n\n--\n\n"); 27 | 28 | message.push_str(&format!( 29 | "You receive this email because an event occured on your {} account at: {}", 30 | APP_CONF.branding.page_title, 31 | APP_CONF.branding.page_url.as_str() 32 | )); 33 | 34 | log::debug!("will send email notification with message: {}", &message); 35 | 36 | // Build up the email 37 | let email_message = Email::builder() 38 | .to(to) 39 | .from(( 40 | APP_CONF.email.from.as_str(), 41 | APP_CONF.branding.page_title.as_str(), 42 | )) 43 | .subject(subject) 44 | .text(message) 45 | .build() 46 | .or(Err(true))?; 47 | 48 | // Deliver the message 49 | return acquire_transport( 50 | &APP_CONF.email.smtp_host, 51 | APP_CONF.email.smtp_port, 52 | APP_CONF.email.smtp_username.to_owned(), 53 | APP_CONF.email.smtp_password.to_owned(), 54 | APP_CONF.email.smtp_encrypt, 55 | ) 56 | .map(|mut transport| transport.send(email_message.into())) 57 | .and(Ok(())) 58 | .or(Err(true)); 59 | } 60 | } 61 | 62 | fn acquire_transport( 63 | smtp_host: &str, 64 | smtp_port: u16, 65 | smtp_username: Option, 66 | smtp_password: Option, 67 | smtp_encrypt: bool, 68 | ) -> Result { 69 | let mut security = ClientSecurity::None; 70 | 71 | if smtp_encrypt == true { 72 | if let Ok(connector) = TlsConnector::new() { 73 | security = ClientSecurity::Required(ClientTlsParameters { 74 | connector: connector, 75 | domain: smtp_host.to_string(), 76 | }); 77 | } 78 | 79 | // Do not deliver email if TLS context cannot be acquired (prevents unencrypted emails \ 80 | // to be sent) 81 | if let ClientSecurity::None = security { 82 | log::error!("could not build smtp encrypted connector"); 83 | 84 | return Err(()); 85 | } 86 | } 87 | 88 | match SmtpClient::new((smtp_host, smtp_port), security) { 89 | Ok(client) => { 90 | let mut client = client 91 | .timeout(Some(Duration::from_secs(5))) 92 | .connection_reuse(ConnectionReuseParameters::NoReuse); 93 | 94 | match (smtp_username, smtp_password) { 95 | (Some(smtp_username_value), Some(smtp_password_value)) => { 96 | client = client 97 | .credentials(Credentials::new(smtp_username_value, smtp_password_value)); 98 | } 99 | _ => {} 100 | } 101 | 102 | Ok(client.transport()) 103 | } 104 | Err(err) => { 105 | log::error!("could not acquire smtp transport: {}", err); 106 | 107 | Err(()) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/notifier/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod email; 8 | -------------------------------------------------------------------------------- /src/responder/asset_file.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::fs::File; 8 | use std::io; 9 | use std::ops::{Deref, DerefMut}; 10 | use std::path::{Path, PathBuf}; 11 | use time::{self, Duration}; 12 | 13 | use rocket::http::hyper::header::{CacheControl, CacheDirective, Expires, HttpDate}; 14 | use rocket::http::ContentType; 15 | use rocket::request::Request; 16 | use rocket::response::{self, Responder}; 17 | 18 | const ASSETS_EXPIRE_SECONDS: u32 = 10800; 19 | 20 | #[derive(Debug)] 21 | pub struct AssetFile(PathBuf, File); 22 | 23 | // Notice: this is a re-implementation of Rocket native NamedFile, with more response headers 24 | // See: https://api.rocket.rs/src/rocket/response/named_file.rs.html 25 | impl AssetFile { 26 | pub fn open>(path: P) -> io::Result { 27 | let file = File::open(path.as_ref())?; 28 | Ok(AssetFile(path.as_ref().to_path_buf(), file)) 29 | } 30 | 31 | #[inline(always)] 32 | pub fn file(&self) -> &File { 33 | &self.1 34 | } 35 | 36 | #[inline(always)] 37 | pub fn take_file(self) -> File { 38 | self.1 39 | } 40 | 41 | #[inline(always)] 42 | pub fn file_mut(&mut self) -> &mut File { 43 | &mut self.1 44 | } 45 | 46 | #[inline(always)] 47 | pub fn path(&self) -> &Path { 48 | self.0.as_path() 49 | } 50 | } 51 | 52 | impl<'r> Responder<'r> for AssetFile { 53 | fn respond_to(self, req: &Request) -> response::Result<'r> { 54 | let mut response = self.1.respond_to(req)?; 55 | 56 | // Set cache headers 57 | response.set_header(CacheControl(vec![ 58 | CacheDirective::Public, 59 | CacheDirective::MaxAge(ASSETS_EXPIRE_SECONDS), 60 | ])); 61 | 62 | response.set_header(Expires(HttpDate( 63 | time::now() + Duration::seconds(ASSETS_EXPIRE_SECONDS as i64), 64 | ))); 65 | 66 | // Set content type header? 67 | if let Some(ext) = self.0.extension() { 68 | if let Some(ct) = ContentType::from_extension(&ext.to_string_lossy()) { 69 | response.set_header(ct); 70 | } 71 | } 72 | 73 | Ok(response) 74 | } 75 | } 76 | 77 | impl Deref for AssetFile { 78 | type Target = File; 79 | 80 | fn deref(&self) -> &File { 81 | &self.1 82 | } 83 | } 84 | 85 | impl DerefMut for AssetFile { 86 | fn deref_mut(&mut self) -> &mut File { 87 | &mut self.1 88 | } 89 | } 90 | 91 | impl io::Read for AssetFile { 92 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 93 | self.file().read(buf) 94 | } 95 | 96 | fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 97 | self.file().read_to_end(buf) 98 | } 99 | } 100 | 101 | impl io::Write for AssetFile { 102 | fn write(&mut self, buf: &[u8]) -> io::Result { 103 | self.file().write(buf) 104 | } 105 | 106 | fn flush(&mut self) -> io::Result<()> { 107 | self.file().flush() 108 | } 109 | } 110 | 111 | impl io::Seek for AssetFile { 112 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 113 | self.file().seek(pos) 114 | } 115 | } 116 | 117 | impl<'a> io::Read for &'a AssetFile { 118 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 119 | self.file().read(buf) 120 | } 121 | 122 | fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 123 | self.file().read_to_end(buf) 124 | } 125 | } 126 | 127 | impl<'a> io::Write for &'a AssetFile { 128 | fn write(&mut self, buf: &[u8]) -> io::Result { 129 | self.file().write(buf) 130 | } 131 | 132 | fn flush(&mut self) -> io::Result<()> { 133 | self.file().flush() 134 | } 135 | } 136 | 137 | impl<'a> io::Seek for &'a AssetFile { 138 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 139 | self.file().seek(pos) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/responder/auth_guard.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use log; 8 | use rand::{self, Rng}; 9 | use rocket::http::{Cookie, Cookies, Status}; 10 | use rocket::request::{self, FromRequest, Request}; 11 | use rocket::Outcome; 12 | use sha2::{Digest, Sha256}; 13 | 14 | use APP_CONF; 15 | 16 | pub struct AuthGuard(pub i32); 17 | pub struct AuthAnonymousGuard; 18 | 19 | const PASSWORD_MINIMUM_LENGTH: usize = 4; 20 | const PASSWORD_MAXIMUM_LENGTH: usize = 200; 21 | 22 | pub static AUTH_USER_COOKIE_NAME: &'static str = "user_id"; 23 | 24 | impl<'a, 'r> FromRequest<'a, 'r> for AuthGuard { 25 | type Error = (); 26 | 27 | fn from_request(request: &'a Request<'r>) -> request::Outcome { 28 | if let Outcome::Success(cookies) = request.guard::() { 29 | if let Some(user_id_cookie) = read(cookies) { 30 | if let Ok(user_id) = user_id_cookie.value().parse::() { 31 | log::debug!("got user_id from cookies: {}", &user_id); 32 | 33 | return Outcome::Success(AuthGuard(user_id)); 34 | } 35 | } 36 | } 37 | 38 | Outcome::Failure((Status::Forbidden, ())) 39 | } 40 | } 41 | 42 | impl<'a, 'r> FromRequest<'a, 'r> for AuthAnonymousGuard { 43 | type Error = (); 44 | 45 | fn from_request(request: &'a Request<'r>) -> request::Outcome { 46 | match request.guard::() { 47 | Outcome::Success(_) => Outcome::Failure((Status::Gone, ())), 48 | _ => Outcome::Success(AuthAnonymousGuard), 49 | } 50 | } 51 | } 52 | 53 | pub fn insert(mut cookies: Cookies, user_id: String) { 54 | cookies.add_private(Cookie::new(AUTH_USER_COOKIE_NAME, user_id)); 55 | } 56 | 57 | pub fn cleanup(mut cookies: Cookies) { 58 | cookies.remove_private(Cookie::named(AUTH_USER_COOKIE_NAME)); 59 | } 60 | 61 | fn read(mut cookies: Cookies) -> Option { 62 | cookies.get_private(AUTH_USER_COOKIE_NAME) 63 | } 64 | 65 | pub fn password_encode(password: &str) -> Vec { 66 | let password_salted = [password, APP_CONF.database.password_salt.as_str()].join(""); 67 | 68 | log::debug!( 69 | "salted password: {} and got result: {}", 70 | password, 71 | &password_salted 72 | ); 73 | 74 | let mut hasher = Sha256::default(); 75 | 76 | hasher.input(&password_salted.into_bytes()); 77 | 78 | hasher.result().to_vec() 79 | } 80 | 81 | pub fn password_verify(reference: &[u8], password: &str) -> bool { 82 | let password_encoded = password_encode(password); 83 | 84 | password_encoded == reference 85 | } 86 | 87 | pub fn password_generate() -> (Vec, String) { 88 | let password = rand::thread_rng() 89 | .gen_ascii_chars() 90 | .take(60) 91 | .collect::(); 92 | 93 | (password_encode(&password), password) 94 | } 95 | 96 | pub fn recovery_generate() -> (Vec, String) { 97 | let recovery_password = rand::thread_rng() 98 | .gen_ascii_chars() 99 | .take(40) 100 | .collect::(); 101 | 102 | (password_encode(&recovery_password), recovery_password) 103 | } 104 | 105 | pub fn password_policy_check(password: &str) -> bool { 106 | let size = password.len(); 107 | 108 | size >= PASSWORD_MINIMUM_LENGTH && size <= PASSWORD_MAXIMUM_LENGTH 109 | } 110 | -------------------------------------------------------------------------------- /src/responder/catchers.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use rocket::response::Redirect; 8 | 9 | #[catch(403)] 10 | pub fn forbidden() -> Redirect { 11 | Redirect::to("/initiate/") 12 | } 13 | 14 | #[catch(410)] 15 | pub fn gone() -> Redirect { 16 | Redirect::to("/dashboard/") 17 | } 18 | -------------------------------------------------------------------------------- /src/responder/context.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use separator::FixedPlaceSeparatable; 8 | use url_serde::SerdeUrl; 9 | 10 | use config::config::ConfigTrackerBanner; 11 | use APP_CONF; 12 | 13 | const LOGO_EXTENSION_SPLIT_SPAN: usize = 4; 14 | 15 | lazy_static! { 16 | pub static ref CONFIG_CONTEXT: ConfigContext = ConfigContext { 17 | runtime_version: env!("CARGO_PKG_VERSION").to_string(), 18 | page_title: APP_CONF.branding.page_title.to_owned(), 19 | help_url: APP_CONF.branding.help_url.to_owned(), 20 | support_url: APP_CONF.branding.support_url.to_owned(), 21 | icon_color: APP_CONF.branding.icon_color.to_owned(), 22 | icon_url: APP_CONF.branding.icon_url.to_owned(), 23 | icon_mime: ImageMime::guess_from(APP_CONF.branding.icon_url.as_str()), 24 | logo_white_url: APP_CONF.branding.logo_white_url.to_owned(), 25 | logo_dark_url: APP_CONF.branding.logo_dark_url.to_owned(), 26 | custom_html: APP_CONF.branding.custom_html.to_owned(), 27 | account_create_allow: APP_CONF.database.account_create_allow, 28 | payout_currency: APP_CONF.payout.currency.to_owned(), 29 | payout_amount_minimum: APP_CONF 30 | .payout 31 | .amount_minimum 32 | .separated_string_with_fixed_place(2), 33 | track_url: APP_CONF.tracker.track_url.to_owned(), 34 | track_parameter: APP_CONF.tracker.track_parameter.to_owned(), 35 | banners: ConfigContext::map_banners(&APP_CONF.tracker.banner) 36 | }; 37 | } 38 | 39 | #[derive(Serialize)] 40 | pub enum ImageMime { 41 | #[serde(rename = "image/png")] 42 | ImagePNG, 43 | 44 | #[serde(rename = "image/jpeg")] 45 | ImageJPEG, 46 | 47 | #[serde(rename = "image/gif")] 48 | ImageGIF, 49 | 50 | #[serde(rename = "image/svg")] 51 | ImageSVG, 52 | } 53 | 54 | impl ImageMime { 55 | fn guess_from(logo_url: &str) -> ImageMime { 56 | if logo_url.len() > LOGO_EXTENSION_SPLIT_SPAN { 57 | let (_, logo_url_extension) = 58 | logo_url.split_at(logo_url.len() - LOGO_EXTENSION_SPLIT_SPAN); 59 | 60 | match logo_url_extension { 61 | ".svg" => ImageMime::ImageSVG, 62 | ".jpg" => ImageMime::ImageJPEG, 63 | ".gif" => ImageMime::ImageGIF, 64 | _ => ImageMime::ImagePNG, 65 | } 66 | } else { 67 | ImageMime::ImagePNG 68 | } 69 | } 70 | } 71 | 72 | #[derive(Serialize)] 73 | pub struct ConfigContext { 74 | pub runtime_version: String, 75 | pub page_title: String, 76 | pub help_url: SerdeUrl, 77 | pub support_url: SerdeUrl, 78 | pub icon_color: String, 79 | pub icon_url: SerdeUrl, 80 | pub icon_mime: ImageMime, 81 | pub logo_white_url: SerdeUrl, 82 | pub logo_dark_url: SerdeUrl, 83 | pub custom_html: Option, 84 | pub account_create_allow: bool, 85 | pub payout_currency: String, 86 | pub payout_amount_minimum: String, 87 | pub track_url: String, 88 | pub track_parameter: String, 89 | pub banners: Vec<(SerdeUrl, u16, u16)>, 90 | } 91 | 92 | impl ConfigContext { 93 | fn map_banners(banners: &Vec) -> Vec<(SerdeUrl, u16, u16)> { 94 | banners 95 | .into_iter() 96 | .map(|banner| { 97 | ( 98 | banner.banner_url.to_owned(), 99 | banner.size_width, 100 | banner.size_height, 101 | ) 102 | }) 103 | .collect::>() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/responder/macros.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #[macro_export] 8 | macro_rules! gen_auth_guard { 9 | ($name:ident, $token:expr) => { 10 | pub struct $name; 11 | 12 | pub struct Authorization { 13 | pub username: String, 14 | pub password: String, 15 | } 16 | 17 | impl Authorization { 18 | pub fn parse_from(scheme: &str, key: &str) -> Result { 19 | let start_from = scheme.len() + 1; 20 | 21 | if key.starts_with(scheme) && key.len() > start_from { 22 | base64::decode(&key[start_from..]) 23 | .or(Err(())) 24 | .and_then(|decoded| String::from_utf8(decoded).or(Err(()))) 25 | .and_then(|text| { 26 | let parts: Vec<&str> = text.split(":").collect(); 27 | 28 | if parts.len() == 2 { 29 | Ok(Authorization { 30 | username: parts[0].to_owned(), 31 | password: parts[1].to_owned(), 32 | }) 33 | } else { 34 | Err(()) 35 | } 36 | }) 37 | } else { 38 | Err(()) 39 | } 40 | } 41 | } 42 | 43 | impl<'a, 'r> FromRequest<'a, 'r> for $name { 44 | type Error = (); 45 | 46 | fn from_request(request: &'a Request<'r>) -> request::Outcome<$name, ()> { 47 | if let Some(authorization_value) = request.headers().get_one("authorization") { 48 | match Authorization::parse_from("Basic", authorization_value) { 49 | Ok(authorization) => { 50 | if authorization.password == $token { 51 | Outcome::Success($name) 52 | } else { 53 | Outcome::Failure((Status::Unauthorized, ())) 54 | } 55 | } 56 | Err(_) => Outcome::Failure((Status::BadRequest, ())), 57 | } 58 | } else { 59 | Outcome::Failure((Status::Unauthorized, ())) 60 | } 61 | } 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/responder/management_guard.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2021, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use base64; 8 | use rocket::http::Status; 9 | use rocket::request::{self, FromRequest, Request}; 10 | use rocket::Outcome; 11 | use APP_CONF; 12 | 13 | gen_auth_guard!(ManagementGuard, APP_CONF.server.management_token); 14 | -------------------------------------------------------------------------------- /src/responder/manager.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use std::collections::HashMap; 8 | 9 | use rocket; 10 | use rocket::config::{Config, Environment}; 11 | use rocket_contrib::templates::Template; 12 | 13 | use super::{catchers, routes}; 14 | use storage::db; 15 | 16 | use APP_CONF; 17 | 18 | pub fn run() { 19 | // Build Rocket configuration 20 | let mut config = Config::build(Environment::Production) 21 | .address(APP_CONF.server.inet.ip().to_string()) 22 | .port(APP_CONF.server.inet.port()) 23 | .workers(APP_CONF.server.workers) 24 | .secret_key(APP_CONF.server.secret_key.as_str()) 25 | .finalize() 26 | .unwrap(); 27 | 28 | // Append extra options 29 | let mut extras = HashMap::new(); 30 | 31 | extras.insert( 32 | "template_dir".to_string(), 33 | APP_CONF 34 | .assets 35 | .path 36 | .join("./templates") 37 | .to_str() 38 | .unwrap() 39 | .into(), 40 | ); 41 | 42 | config.set_extras(extras); 43 | 44 | // Build and run Rocket instance 45 | rocket::custom(config) 46 | .mount( 47 | "/", 48 | routes![ 49 | routes::get_index, 50 | routes::get_robots, 51 | routes::get_initiate_base, 52 | routes::get_initiate_login, 53 | routes::get_initiate_signup, 54 | routes::get_initiate_recover, 55 | routes::get_initiate_logout, 56 | routes::get_dashboard_base, 57 | routes::get_dashboard_welcome, 58 | routes::get_dashboard_trackers, 59 | routes::get_dashboard_payouts, 60 | routes::get_dashboard_payouts_partial_payouts, 61 | routes::get_dashboard_account, 62 | routes::get_assets_fonts, 63 | routes::get_assets_images, 64 | routes::get_assets_stylesheets, 65 | routes::get_assets_javascripts, 66 | routes::post_initiate_login_form_login, 67 | routes::post_initiate_signup_form_signup, 68 | routes::post_initiate_recover_form_recover, 69 | routes::post_dashboard_trackers_form_create, 70 | routes::post_dashboard_trackers_form_remove, 71 | routes::post_dashboard_payouts_form_request, 72 | routes::post_dashboard_account_form_account, 73 | routes::post_dashboard_account_form_payout, 74 | routes::post_track_payment, 75 | routes::post_track_signup, 76 | routes::post_management_account, 77 | ], 78 | ) 79 | .register(catchers![catchers::forbidden, catchers::gone,]) 80 | .attach(Template::fairing()) 81 | .manage(db::pool()) 82 | .launch(); 83 | } 84 | -------------------------------------------------------------------------------- /src/responder/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | #[macro_use] 8 | mod macros; 9 | 10 | mod asset_file; 11 | mod catchers; 12 | mod context; 13 | mod management_guard; 14 | mod routes; 15 | mod track_guard; 16 | mod utilities; 17 | 18 | pub mod auth_guard; 19 | pub mod manager; 20 | -------------------------------------------------------------------------------- /src/responder/track_guard.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use base64; 8 | use rocket::http::Status; 9 | use rocket::request::{self, FromRequest, Request}; 10 | use rocket::Outcome; 11 | use APP_CONF; 12 | 13 | gen_auth_guard!(TrackGuard, APP_CONF.server.track_token); 14 | -------------------------------------------------------------------------------- /src/responder/utilities.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use bigdecimal::BigDecimal; 8 | use diesel::dsl::sum; 9 | use diesel::prelude::*; 10 | use log; 11 | use num_traits::cast::ToPrimitive; 12 | use separator::FixedPlaceSeparatable; 13 | use std::cmp; 14 | 15 | use super::routes::DashboardPayoutsContextPayout; 16 | use notifier::email::EmailNotifier; 17 | use storage::db::DbConn; 18 | use storage::models::Payout; 19 | use storage::schemas::balance::dsl::{ 20 | account_id as balance_account_id, amount as balance_amount, balance, 21 | released as balance_released, 22 | }; 23 | use storage::schemas::payout::dsl::{ 24 | account_id as payout_account_id, created_at as payout_created_at, payout, 25 | }; 26 | use APP_CONF; 27 | 28 | const PAYOUTS_LIMIT_PER_PAGE: i64 = 20; 29 | 30 | pub fn get_balance(db: &DbConn, user_id: i32, released: Option) -> f32 { 31 | let balance_result = if let Some(released_inner) = released { 32 | balance 33 | .filter(balance_account_id.eq(user_id)) 34 | .filter(balance_released.eq(released_inner)) 35 | .select(sum(balance_amount)) 36 | .first(&**db) 37 | } else { 38 | balance 39 | .filter(balance_account_id.eq(user_id)) 40 | .select(sum(balance_amount)) 41 | .first(&**db) 42 | }; 43 | 44 | let balance_count: Option = balance_result.ok().and_then(|value: Option| { 45 | if let Some(value_inner) = value { 46 | value_inner.to_f32() 47 | } else { 48 | None 49 | } 50 | }); 51 | 52 | balance_count.unwrap_or(0.0) 53 | } 54 | 55 | pub fn get_balance_string(db: &DbConn, user_id: i32, released: Option) -> String { 56 | get_balance(db, user_id, released).separated_string_with_fixed_place(2) 57 | } 58 | 59 | pub fn check_argument_value(argument: &Option, against: &str) -> bool { 60 | if let &Some(ref value) = argument { 61 | value == against 62 | } else { 63 | false 64 | } 65 | } 66 | 67 | pub fn list_payouts( 68 | db: &DbConn, 69 | user_id: i32, 70 | page_number: u16, 71 | ) -> (Vec, bool) { 72 | let mut payouts = Vec::new(); 73 | let mut has_more = false; 74 | 75 | payout 76 | .filter(payout_account_id.eq(user_id)) 77 | .order(payout_created_at.desc()) 78 | .limit(PAYOUTS_LIMIT_PER_PAGE + 1) 79 | .offset(paging_to_offset(page_number, PAYOUTS_LIMIT_PER_PAGE)) 80 | .load::(&**db) 81 | .map(|results| { 82 | for (index, result) in results.into_iter().enumerate() { 83 | if (index as i64) < PAYOUTS_LIMIT_PER_PAGE { 84 | log::debug!("got payout #{}: {:?}", index, result); 85 | 86 | let amount_value = result 87 | .amount 88 | .to_f32() 89 | .unwrap_or(0.0) 90 | .separated_string_with_fixed_place(2); 91 | 92 | payouts.push(DashboardPayoutsContextPayout { 93 | number: result.number, 94 | status: result.status, 95 | amount: amount_value, 96 | currency: result.currency, 97 | account: result.account.unwrap_or("".to_string()), 98 | invoice_url: result.invoice_url.unwrap_or("".to_string()), 99 | date: result.created_at.date().format("%d/%m/%Y").to_string(), 100 | }); 101 | } else { 102 | has_more = true; 103 | } 104 | } 105 | }) 106 | .ok(); 107 | 108 | (payouts, has_more) 109 | } 110 | 111 | pub fn send_payout_emails(user_id: i32, user_email: &str, balance_due: f32, currency: &str) { 112 | // Send request email to administrator 113 | { 114 | // Generate message 115 | let mut message = String::new(); 116 | 117 | message.push_str(&format!( 118 | "A payout of {} {} has been requested by user #{} with email: {}\n\n", 119 | currency, 120 | balance_due.separated_string_with_fixed_place(2), 121 | user_id, 122 | user_email 123 | )); 124 | 125 | message.push_str("Here are the steps to take:\n\n"); 126 | message 127 | .push_str(" 1. Review the pending payout in the database and accept or refuse it.\n"); 128 | message.push_str(" 2. Generate an invoice and update the database accordingly.\n"); 129 | message.push_str(" 3. Send the money using user payout details.\n"); 130 | message.push_str(" 4. Notify the user by email that the payout has been processed.\n"); 131 | message.push_str(" 5. Mark the payout as processed in the database."); 132 | 133 | // Send email 134 | if EmailNotifier::dispatch( 135 | &APP_CONF.payout.administrator_email, 136 | "Pending payout request".to_string(), 137 | &message, 138 | ) 139 | .is_ok() 140 | == true 141 | { 142 | log::debug!( 143 | "sent payout request email to administrator on: {}", 144 | &APP_CONF.payout.administrator_email 145 | ); 146 | } else { 147 | log::error!( 148 | "could not send payout request email to administrator on: {}", 149 | &APP_CONF.payout.administrator_email 150 | ); 151 | } 152 | } 153 | 154 | // Send confirmation email to user 155 | { 156 | // Generate message 157 | let mut message = String::new(); 158 | 159 | message.push_str("Hi,\n\n"); 160 | 161 | message.push_str(&format!( 162 | "Your payout request of {} {} has been submitted for processing.\n\n", 163 | currency, 164 | balance_due.separated_string_with_fixed_place(2) 165 | )); 166 | 167 | message.push_str("Our team has been notified and will process it as soon as possible. "); 168 | message.push_str("The money will then be sent to your registered payout method."); 169 | 170 | // Send email 171 | if EmailNotifier::dispatch(user_email, "Payout request submitted".to_string(), &message) 172 | .is_ok() 173 | == true 174 | { 175 | log::debug!("sent payout confirmation email to user on: {}", user_email); 176 | } else { 177 | log::error!( 178 | "could not send payout confirmation email to user on: {}", 179 | user_email 180 | ); 181 | } 182 | } 183 | } 184 | 185 | fn paging_to_offset(page_number: u16, limit_per_page: i64) -> i64 { 186 | ((cmp::max(page_number, 1) - 1) as i64) * limit_per_page 187 | } 188 | -------------------------------------------------------------------------------- /src/storage/choices.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub const ACCOUNT_PAYOUT_METHODS: &'static [(&'static str, &'static str)] = &[ 8 | ("bank", "Bank wire"), 9 | ("paypal", "PayPal"), 10 | ("bitcoin", "Bitcoin"), 11 | ("other", "Other (your instructions)"), 12 | ]; 13 | -------------------------------------------------------------------------------- /src/storage/db.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use diesel::mysql::MysqlConnection; 8 | use log; 9 | use r2d2; 10 | use r2d2_diesel::ConnectionManager; 11 | use rocket::http::Status; 12 | use rocket::request::{self, FromRequest}; 13 | use rocket::{Outcome, Request, State}; 14 | use std::ops::Deref; 15 | use std::time::Duration; 16 | 17 | use APP_CONF; 18 | 19 | type Pool = r2d2::Pool>; 20 | 21 | pub struct DbConn(pub r2d2::PooledConnection>); 22 | 23 | impl Deref for DbConn { 24 | type Target = MysqlConnection; 25 | 26 | #[inline(always)] 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | impl<'a, 'r> FromRequest<'a, 'r> for DbConn { 33 | type Error = (); 34 | 35 | fn from_request(request: &'a Request<'r>) -> request::Outcome { 36 | let pool = request.guard::>()?; 37 | match pool.get() { 38 | Ok(conn) => Outcome::Success(DbConn(conn)), 39 | Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), 40 | } 41 | } 42 | } 43 | 44 | pub fn pool() -> Pool { 45 | log::debug!("setting up db pool..."); 46 | 47 | let manager = ConnectionManager::::new(APP_CONF.database.url.as_str()); 48 | 49 | let pool = r2d2::Pool::builder() 50 | .max_size(APP_CONF.database.pool_size) 51 | .idle_timeout(Some(Duration::from_secs(APP_CONF.database.idle_timeout))) 52 | .connection_timeout(Duration::from_secs(APP_CONF.database.connection_timeout)) 53 | .build(manager) 54 | .expect("db pool"); 55 | 56 | log::debug!("db pool configured"); 57 | 58 | pool 59 | } 60 | -------------------------------------------------------------------------------- /src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod choices; 8 | pub mod db; 9 | pub mod models; 10 | pub mod schemas; 11 | -------------------------------------------------------------------------------- /src/storage/models.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use bigdecimal::BigDecimal; 8 | use chrono::naive::NaiveDateTime; 9 | 10 | use super::schemas::{account, balance, payout, tracker}; 11 | 12 | #[derive(Identifiable, Queryable, Associations, Debug)] 13 | #[table_name = "account"] 14 | pub struct Account { 15 | pub id: i32, 16 | pub email: String, 17 | pub password: Vec, 18 | pub recovery: Option>, 19 | pub commission: BigDecimal, 20 | pub full_name: Option, 21 | pub address: Option, 22 | pub country: Option, 23 | pub payout_method: Option, 24 | pub payout_instructions: Option, 25 | pub notify_balance: bool, 26 | pub created_at: NaiveDateTime, 27 | pub updated_at: NaiveDateTime, 28 | } 29 | 30 | #[derive(Identifiable, Queryable, Associations, Debug)] 31 | #[table_name = "balance"] 32 | pub struct Balance { 33 | pub id: i32, 34 | pub amount: BigDecimal, 35 | pub currency: String, 36 | pub released: bool, 37 | pub trace: Option, 38 | pub account_id: i32, 39 | pub tracker_id: Option, 40 | pub created_at: NaiveDateTime, 41 | pub updated_at: NaiveDateTime, 42 | } 43 | 44 | #[derive(Identifiable, Queryable, Associations, Debug)] 45 | #[table_name = "payout"] 46 | pub struct Payout { 47 | pub id: i32, 48 | pub number: i32, 49 | pub amount: BigDecimal, 50 | pub currency: String, 51 | pub status: String, 52 | pub account: Option, 53 | pub invoice_url: Option, 54 | pub account_id: i32, 55 | pub created_at: NaiveDateTime, 56 | pub updated_at: NaiveDateTime, 57 | } 58 | 59 | #[derive(Identifiable, Queryable, Associations, Debug)] 60 | #[table_name = "tracker"] 61 | pub struct Tracker { 62 | pub id: String, 63 | pub label: String, 64 | pub statistics_signups: i32, 65 | pub account_id: i32, 66 | pub created_at: NaiveDateTime, 67 | pub updated_at: NaiveDateTime, 68 | } 69 | 70 | #[derive(AsChangeset)] 71 | #[table_name = "account"] 72 | pub struct AccountRecoveryUpdate { 73 | pub recovery: Vec, 74 | } 75 | -------------------------------------------------------------------------------- /src/storage/schemas.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | table! { 8 | account (id) { 9 | id -> Integer, 10 | email -> Varchar, 11 | password -> Binary, 12 | recovery -> Nullable, 13 | commission -> Numeric, 14 | full_name -> Nullable, 15 | address -> Nullable, 16 | country -> Nullable, 17 | payout_method -> Nullable, 18 | payout_instructions -> Nullable, 19 | notify_balance -> Bool, 20 | created_at -> Timestamp, 21 | updated_at -> Timestamp, 22 | } 23 | } 24 | 25 | table! { 26 | balance (id) { 27 | id -> Integer, 28 | amount -> Numeric, 29 | currency -> VarChar, 30 | released -> Bool, 31 | trace -> Nullable, 32 | account_id -> Integer, 33 | tracker_id -> Nullable, 34 | created_at -> Timestamp, 35 | updated_at -> Timestamp, 36 | } 37 | } 38 | 39 | table! { 40 | payout (id) { 41 | id -> Integer, 42 | number -> Integer, 43 | amount -> Numeric, 44 | currency -> VarChar, 45 | status -> VarChar, 46 | account -> Nullable, 47 | invoice_url -> Nullable, 48 | account_id -> Integer, 49 | created_at -> Timestamp, 50 | updated_at -> Timestamp, 51 | } 52 | } 53 | 54 | table! { 55 | tracker (id) { 56 | id -> VarChar, 57 | label -> VarChar, 58 | statistics_signups -> Integer, 59 | account_id -> Integer, 60 | created_at -> Timestamp, 61 | updated_at -> Timestamp, 62 | } 63 | } 64 | 65 | joinable!(balance -> account(account_id)); 66 | joinable!(balance -> tracker(tracker_id)); 67 | joinable!(payout -> account(account_id)); 68 | joinable!(tracker -> account(account_id)); 69 | 70 | allow_tables_to_appear_in_same_query!(account, tracker); 71 | -------------------------------------------------------------------------------- /src/track/mod.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | pub mod payment; 8 | -------------------------------------------------------------------------------- /src/track/payment.rs: -------------------------------------------------------------------------------- 1 | // Raider 2 | // 3 | // Affiliates dashboard 4 | // Copyright: 2018, Valerian Saliou 5 | // License: Mozilla Public License v2.0 (MPL v2.0) 6 | 7 | use bigdecimal::BigDecimal; 8 | use chrono::offset::Utc; 9 | use diesel; 10 | use diesel::prelude::*; 11 | use log; 12 | use num_traits::ToPrimitive; 13 | use separator::FixedPlaceSeparatable; 14 | use std::thread; 15 | 16 | use exchange::manager::normalize as exchange_normalize; 17 | use notifier::email::EmailNotifier; 18 | use storage::db::DbConn; 19 | use storage::models::{Account, Tracker}; 20 | use storage::schemas::account::dsl::account; 21 | use storage::schemas::balance::dsl::{ 22 | account_id as balance_account_id, amount as balance_amount, balance, 23 | created_at as balance_created_at, currency as balance_currency, trace as balance_trace, 24 | tracker_id as balance_tracker_id, updated_at as balance_updated_at, 25 | }; 26 | use storage::schemas::tracker::dsl::{ 27 | id as tracker_id, statistics_signups as tracker_statistics_signups, tracker, 28 | updated_at as tracker_updated_at, 29 | }; 30 | use APP_CONF; 31 | 32 | pub enum HandlePaymentError { 33 | InvalidAmount, 34 | BadCurrency, 35 | NotFound, 36 | } 37 | 38 | pub enum HandleSignupError { 39 | NotFound, 40 | } 41 | 42 | pub fn handle_payment( 43 | db: &DbConn, 44 | tracking_id: &str, 45 | amount_real: f32, 46 | currency: &str, 47 | trace: &Option, 48 | ) -> Result, HandlePaymentError> { 49 | log::debug!( 50 | "payment track handle: {} of real amount: {} {}", 51 | tracking_id, 52 | currency, 53 | amount_real 54 | ); 55 | 56 | // Normalize amount 57 | if let Ok(amount) = exchange_normalize(amount_real, currency) { 58 | log::debug!( 59 | "normalized real amount: {} {} to: {} {}", 60 | currency, 61 | amount_real, 62 | &APP_CONF.payout.currency, 63 | amount 64 | ); 65 | 66 | // Validate amount 67 | if amount < 0.00 { 68 | return Err(HandlePaymentError::InvalidAmount); 69 | } 70 | 71 | // Ignore zero amount 72 | if amount == 0.00 { 73 | return Ok(None); 74 | } 75 | 76 | // Resolve user for tracking code 77 | let track_result = tracker 78 | .filter(tracker_id.eq(tracking_id)) 79 | .inner_join(account) 80 | .first::<(Tracker, Account)>(&**db); 81 | 82 | if let Ok(track_inner) = track_result { 83 | // Apply user commission percentage to amount 84 | let commission_amount = amount * track_inner.1.commission.to_f32().unwrap_or(0.0); 85 | 86 | if commission_amount > 0.0 { 87 | let now_date = Utc::now().naive_utc(); 88 | 89 | let insert_result = diesel::insert_into(balance) 90 | .values(( 91 | &balance_amount.eq(BigDecimal::from(commission_amount)), 92 | &balance_currency.eq(&APP_CONF.payout.currency), 93 | &balance_trace.eq(trace), 94 | &balance_account_id.eq(&track_inner.1.id), 95 | &balance_tracker_id.eq(&track_inner.0.id), 96 | &balance_created_at.eq(&now_date), 97 | &balance_updated_at.eq(&now_date), 98 | )) 99 | .execute(&**db); 100 | 101 | if insert_result.is_ok() == true { 102 | return Ok(Some(( 103 | track_inner.1.notify_balance, 104 | track_inner.1.email.to_owned(), 105 | track_inner.0.id.to_owned(), 106 | commission_amount, 107 | APP_CONF.payout.currency.to_owned(), 108 | ))); 109 | } 110 | } 111 | } 112 | 113 | log::warn!( 114 | "payment track: {} could not be stored to balance for amount: {} {}", 115 | tracking_id, 116 | currency, 117 | amount 118 | ); 119 | 120 | Err(HandlePaymentError::NotFound) 121 | } else { 122 | Err(HandlePaymentError::BadCurrency) 123 | } 124 | } 125 | 126 | pub fn handle_signup(db: &DbConn, tracking_id: &str) -> Result<(), HandleSignupError> { 127 | log::debug!("signup track handle: {}", tracking_id); 128 | 129 | // Resolve tracking code 130 | let tracker_result = tracker 131 | .filter(tracker_id.eq(tracking_id)) 132 | .first::(&**db); 133 | 134 | if let Ok(tracker_inner) = tracker_result { 135 | // Notice: this increment is not atomic; thus it is not 100% safe. We do this for \ 136 | // simplicity as Diesel doesnt seem to provide a way to do an increment in the query. 137 | let update_result = diesel::update(tracker.filter(tracker_id.eq(tracking_id))) 138 | .set(( 139 | tracker_statistics_signups.eq(tracker_inner.statistics_signups + 1), 140 | tracker_updated_at.eq(Utc::now().naive_utc()), 141 | )) 142 | .execute(&**db); 143 | 144 | if update_result.is_ok() == true { 145 | return Ok(()); 146 | } 147 | } 148 | 149 | log::warn!("signup track: {} could not be stored", tracking_id); 150 | 151 | Err(HandleSignupError::NotFound) 152 | } 153 | 154 | pub fn run_notify_payment( 155 | user_email: String, 156 | source_tracker_id: String, 157 | commission_amount: f32, 158 | commission_currency: String, 159 | ) { 160 | thread::spawn(move || { 161 | dispatch_notify_payment( 162 | user_email, 163 | source_tracker_id, 164 | commission_amount, 165 | commission_currency, 166 | ); 167 | }); 168 | } 169 | 170 | fn dispatch_notify_payment( 171 | user_email: String, 172 | source_tracker_id: String, 173 | commission_amount: f32, 174 | commission_currency: String, 175 | ) { 176 | // Generate message 177 | let mut message = String::new(); 178 | 179 | message.push_str("Hi,\n\n"); 180 | 181 | message.push_str(&format!( 182 | "You just received commission money of {} {} on your affiliates account balance.\n", 183 | &commission_currency, 184 | &commission_amount.separated_string_with_fixed_place(2) 185 | )); 186 | 187 | message.push_str(&format!( 188 | "This commission was generated by your tracker with ID: {}\n\n", 189 | &source_tracker_id 190 | )); 191 | 192 | message.push_str( 193 | "You can request for a payout anytime on your dashboard (payouts are not automatic).", 194 | ); 195 | 196 | // Send email 197 | if EmailNotifier::dispatch( 198 | &user_email, 199 | "You received commission money".to_string(), 200 | &message, 201 | ) 202 | .is_ok() 203 | == true 204 | { 205 | log::debug!( 206 | "sent balance commission notification email to user on: {}", 207 | user_email 208 | ); 209 | } else { 210 | log::error!( 211 | "could not send balance commission notification email to user on: {}", 212 | user_email 213 | ); 214 | } 215 | } 216 | --------------------------------------------------------------------------------