├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .versionrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── api ├── parser.ts ├── providers.ts ├── routes-api.ts ├── rss.ts ├── styles.ts └── users.ts ├── client ├── .browserslistrc ├── .env.template ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── 192.png │ ├── 512.png │ ├── apple.png │ ├── favicon.ico │ ├── icon.svg │ ├── index.html │ ├── opensearchdescription.xml │ ├── robots.txt │ ├── site.webmanifest │ └── sitemap.xml ├── src │ ├── App.vue │ ├── components │ │ ├── Alert.vue │ │ ├── AppFooter.vue │ │ ├── AppHeader.vue │ │ ├── CloseButton.vue │ │ ├── StyleEditor.vue │ │ ├── TopicCloud.vue │ │ ├── cards │ │ │ ├── BaseCard.vue │ │ │ ├── StyleCard.vue │ │ │ └── StyleCardSkeleton.vue │ │ └── dialogs │ │ │ ├── AddStyleDialog.vue │ │ │ ├── BaseDialog.vue │ │ │ ├── ConfirmationDialog.vue │ │ │ ├── HowToUseDialog.vue │ │ │ ├── LoginDialog.vue │ │ │ ├── PrivacyPolicyDialog.vue │ │ │ └── StyleInfoDialog.vue │ ├── githubEmoji.json │ ├── images │ │ ├── codeberg-logo.png │ │ ├── github-logo.png │ │ └── no-image.png │ ├── main.js │ ├── mixins.js │ ├── router.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── alert │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ │ ├── styleGrid │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ │ └── user │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutations.js │ │ │ └── state.js │ ├── styles │ │ ├── buttons.scss │ │ ├── fonts │ │ │ ├── Gilroy-Bold.woff │ │ │ ├── Gilroy-Bold.woff2 │ │ │ ├── Gilroy-Regular.woff │ │ │ └── Gilroy-Regular.woff2 │ │ ├── main.scss │ │ ├── mixins │ │ │ └── media.scss │ │ └── transitions.scss │ └── views │ │ ├── Home.vue │ │ └── Profile.vue └── vue.config.js ├── config.template.ts ├── maintenance.html ├── meta └── preview.png ├── middleware.ts ├── models ├── Style.ts ├── User.ts └── init.ts ├── package.json ├── routes.ts ├── server.ts ├── tsconfig.json └── types ├── api └── index.d.ts ├── index.d.ts └── server └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | end_of_line = crlf 9 | charset = utf-8 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, 6 | "project": [ 7 | "./tsconfig.json" 8 | ] 9 | }, 10 | "extends": [ 11 | "airbnb-base", 12 | "airbnb-typescript/base", 13 | "plugin:sonarjs/recommended", 14 | "plugin:import/errors", 15 | "plugin:import/warnings", 16 | "plugin:import/typescript" 17 | ], 18 | "env": { 19 | "node": true, 20 | "es6": true 21 | }, 22 | "plugins": ["import", "sonarjs"], 23 | "rules": { 24 | "func-names": "off", 25 | "object-curly-newline": "off", 26 | "consistent-return": "off", 27 | "linebreak-style": "off", 28 | "no-console": "off", 29 | "import/no-cycle": "error", 30 | "max-len": ["error", {"code": 120 }], 31 | "no-underscore-dangle": ["error", {"allow": ["_id"]}], 32 | "@typescript-eslint/naming-convention": "off", 33 | "@typescript-eslint/ban-ts-comment": "warn", 34 | "@typescript-eslint/no-inferrable-types": "error", 35 | "@typescript-eslint/comma-dangle": ["error", "never"], 36 | "@typescript-eslint/quotes": ["error", "double", {"avoidEscape": true, "allowTemplateLiterals": true}] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: vchet 2 | liberapay: vchet 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | timezone: Asia/Yekaterinburg 8 | open-pull-requests-limit: 10 9 | target-branch: develop 10 | - package-ecosystem: npm 11 | directory: "/client" 12 | schedule: 13 | interval: monthly 14 | timezone: Asia/Yekaterinburg 15 | open-pull-requests-limit: 10 16 | target-branch: develop 17 | ignore: 18 | - dependency-name: sass-loader 19 | versions: 20 | - ">= 11.a" 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Get Release Notes 13 | uses: yashanand1910/standard-release-notes@v1.2.1 14 | id: get_release_notes 15 | with: 16 | version: ${{ github.ref }} 17 | - name: Create Release 18 | uses: actions/create-release@v1.1.4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: ${{ github.ref }} 24 | body: ${{ steps.get_release_notes.outputs.release_notes }} 25 | draft: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Libs 2 | node_modules/ 3 | 4 | # Build files 5 | /public/ 6 | 7 | # Configuration 8 | config.js 9 | config.ts 10 | .env 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "bumpFiles": [ 3 | { 4 | "filename": "./package.json", 5 | "type": "json" 6 | }, 7 | { 8 | "filename": "./client/package.json", 9 | "type": "json" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [4.6.2](https://github.com/VChet/StyleBase/compare/v4.6.1...v4.6.2) (2021-08-24) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **style info:** fix empty custom fields ([43596fd](https://github.com/VChet/StyleBase/commit/43596fddfb9a2aede9dd8b05549b38b37173f707)) 11 | * **topics:** update filter ([4c17d20](https://github.com/VChet/StyleBase/commit/4c17d209c3711af689abae5225aa71965afed360)) 12 | * update placeholder text color ([4d3e0c9](https://github.com/VChet/StyleBase/commit/4d3e0c9bca5bceca9f2d33e648930dd8881a21d7)) 13 | 14 | ### [4.6.1](https://github.com/VChet/StyleBase/compare/v4.6.0...v4.6.1) (2021-07-10) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **topics:** hide topics when filters are active ([c358e29](https://github.com/VChet/StyleBase/commit/c358e29cd977d476a58cc005f8d10bf270e7525d)) 20 | 21 | ## [4.6.0](https://github.com/VChet/StyleBase/compare/v4.5.2...v4.6.0) (2021-05-29) 22 | 23 | 24 | ### Features 25 | 26 | * **style:** parse and store parent for forked repositories ([7c7c4f3](https://github.com/VChet/StyleBase/commit/7c7c4f3a37d662b291b2584a9e57be71f2bedabc)) 27 | * **style info:** show parent repo for forked styles ([870c845](https://github.com/VChet/StyleBase/commit/870c8456847be7601342b5a8f9d5acbfec5cd5cb)) 28 | * **topics:** add topics aggregation request ([f97dab8](https://github.com/VChet/StyleBase/commit/f97dab8be4f9b898fe68e5a9cdeb1bf388506951)) 29 | * **topics:** replace features block with topics list ([2fe9169](https://github.com/VChet/StyleBase/commit/2fe9169a683b790b554bf38f7202843cbc2e08ee)) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **alert:** increase timeout before disappearing ([fdf5fa6](https://github.com/VChet/StyleBase/commit/fdf5fa6b70edcc54f99f20f33634489608d2c16b)) 35 | * **router:** fix redirect on non-existing pages ([6af2029](https://github.com/VChet/StyleBase/commit/6af2029564f45fa44bf676f3367b40508f3d0137)) 36 | 37 | ### [4.5.2](https://github.com/VChet/StyleBase/compare/v4.5.1...v4.5.2) (2021-05-27) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **parser:** allow forked repos ([70cf27f](https://github.com/VChet/StyleBase/commit/70cf27f32e899890fc39c3295c90086966c43be6)) 43 | * **style card:** stop propagation on install button click ([c613096](https://github.com/VChet/StyleBase/commit/c613096ac5f859611df8c49505c061cea831009e)) 44 | 45 | ### [4.5.1](https://github.com/VChet/StyleBase/compare/v4.5.0...v4.5.1) (2021-05-04) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * **maintenance:** remove status page link ([7b2ec56](https://github.com/VChet/StyleBase/commit/7b2ec5642b06de5d341ef8bd4bc3b769f5014fc2)) 51 | * **scroll:** decrease throttle limit ([3402a4b](https://github.com/VChet/StyleBase/commit/3402a4bb3041e1372101fdcdddb065cf7e360eee)) 52 | 53 | ## [4.5.0](https://github.com/VChet/StyleBase/compare/v4.4.3...v4.5.0) (2021-04-24) 54 | 55 | 56 | ### Features 57 | 58 | * **confirmation:** add loading state ([3fb01ba](https://github.com/VChet/StyleBase/commit/3fb01bae3f2b6051116d6546aee43132be94afaf)) 59 | * **profile:** add profile page ([bef9568](https://github.com/VChet/StyleBase/commit/bef95681d847f90141b16af952bfea62940aff7d)) 60 | * **profile:** add style info editor ([2d82d33](https://github.com/VChet/StyleBase/commit/2d82d3344100dcb7d0da3561e8a4935fbc35ce53)) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * **add style:** update user styles on creation ([7aa833e](https://github.com/VChet/StyleBase/commit/7aa833ea4398b9574cf2aabcf92359e8d6fab00a)) 66 | * **style editor:** update styles ([00d2b56](https://github.com/VChet/StyleBase/commit/00d2b5624cde14baa16fa6bb6c01076dd3b4d745)) 67 | 68 | ### [4.4.3](https://github.com/VChet/StyleBase/compare/v4.4.2...v4.4.3) (2021-04-04) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **preview:** render all gif frames instead of only the first one ([133c7ce](https://github.com/VChet/StyleBase/commit/133c7ce86ae99ca58567e6f6bee7ceb688cc5e6d)) 74 | 75 | ### [4.4.2](https://github.com/VChet/StyleBase/compare/v4.4.1...v4.4.2) (2021-03-23) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * **preview:** fallback to original url if resize service returned error ([d4f64bd](https://github.com/VChet/StyleBase/commit/d4f64bdc7b01f65e07b2996f790033695a5d1826)) 81 | 82 | ### [4.4.1](https://github.com/VChet/StyleBase/compare/v4.4.0...v4.4.1) (2021-03-21) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * **maintenance:** add styles for dark color-scheme, add status page ([6c45282](https://github.com/VChet/StyleBase/commit/6c452827243a862ce17768d40244ff72a58060e5)) 88 | * **preview:** keep protocol in 'images.weserv.nl' request ([452b229](https://github.com/VChet/StyleBase/commit/452b2295426c90962cc81a161a0de373dfbcce7d)) 89 | 90 | ## [4.4.0](https://github.com/VChet/StyleBase/compare/v4.3.0...v4.4.0) (2021-03-21) 91 | 92 | 93 | ### Features 94 | 95 | * **api:** add limit parameter to /styles request ([b23893e](https://github.com/VChet/StyleBase/commit/b23893e2ebd8ab1f4d2c6721787f1eda7e8789c4)) 96 | * **preview:** compress preview images via images.weserv.nl ([aa042d0](https://github.com/VChet/StyleBase/commit/aa042d02ac78639bdf69145b65dedc464d6c0673)) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * **emoji:** remove extra file extension ([4b6a60f](https://github.com/VChet/StyleBase/commit/4b6a60f1e02955ead3ec698cf859ca19751c7745)) 102 | * **images:** invert dark images in dark-theme ([5c68f79](https://github.com/VChet/StyleBase/commit/5c68f79045223f79cae160173fed93708dd2682f)) 103 | * **style update:** output update result as string ([8f1877e](https://github.com/VChet/StyleBase/commit/8f1877eab827406aef47c175ff96861a2085bcb6)) 104 | 105 | ## [4.3.0](https://github.com/VChet/StyleBase/compare/v4.2.2...v4.3.0) (2021-02-19) 106 | 107 | 108 | ### Features 109 | 110 | * **style search:** add search by substring ([8b55132](https://github.com/VChet/StyleBase/commit/8b551325f567bde21fbecf26efab833bbef5bd42)) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * **sort options:** keep options while loading, decrease opacity ([33c5587](https://github.com/VChet/StyleBase/commit/33c5587a54dbfe9ce1814e5c7f0af885b7ac8411)) 116 | * **style search:** allow search only with query longer than 2 characters ([dd6a3d8](https://github.com/VChet/StyleBase/commit/dd6a3d82f8f16ddf99b116807568f61e4e995632)) 117 | * **styles:** fix style duplication ([7b200db](https://github.com/VChet/StyleBase/commit/7b200db80c9855da60e4d272134c74ab6e75cee3)) 118 | 119 | ### [4.2.2](https://github.com/VChet/StyleBase/compare/v4.2.1...v4.2.2) (2021-02-14) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **User model:** add sparse parameter to ids ([5b90c6f](https://github.com/VChet/StyleBase/commit/5b90c6fa2ea9b17497bb12ba22f89dcdcdab92a5)) 125 | 126 | ### [4.2.1](https://github.com/VChet/StyleBase/compare/v4.2.0...v4.2.1) (2021-02-11) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * **add style:** add custom fields description ([dd6992a](https://github.com/VChet/StyleBase/commit/dd6992a8dfe011bbc7201be5be39293e29a961e0)) 132 | * **style grid:** throttle scroll, do not show skeleton on loaded cards ([2c08316](https://github.com/VChet/StyleBase/commit/2c08316eb5ec079f5edd5d47f57d9c40e47dbe82)) 133 | 134 | ## [4.2.0](https://github.com/VChet/StyleBase/compare/v4.1.1...v4.2.0) (2021-02-01) 135 | 136 | 137 | ### Features 138 | 139 | * **parser:** search usercss files in folders ([3b42951](https://github.com/VChet/StyleBase/commit/3b42951618df4cacd9926e87e503ee80c94e84ab)) 140 | * **parser:** search usercss files in subfolders ([d062a2b](https://github.com/VChet/StyleBase/commit/d062a2b410acc1f1f0f6d053fcbf3ac365ee72f8)) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * **User:** fix Codeberg organization name parsing ([8cdaa7c](https://github.com/VChet/StyleBase/commit/8cdaa7c7ab1428069f982e2e868047d80e25344e)) 146 | 147 | ### [4.1.1](https://github.com/VChet/StyleBase/compare/v4.1.0...v4.1.1) (2021-01-29) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * **auth:** fix codeberg login ([ad265fe](https://github.com/VChet/StyleBase/commit/ad265feee88df1a05e3c841b8728eee85b2ff062)) 153 | * **style card:** open style dialog by clicking anywhere on the card ([54c7077](https://github.com/VChet/StyleBase/commit/54c7077c91abcbdc691868ff4827e6ce8c0e3766)) 154 | 155 | ## [4.1.0](https://github.com/VChet/StyleBase/compare/v4.0.1...v4.1.0) (2021-01-15) 156 | 157 | 158 | ### Features 159 | 160 | * **meta:** change title and description in different states ([85893bc](https://github.com/VChet/StyleBase/commit/85893bcce37cd2698df79c1b63e98b5dffb50619)) 161 | * **search:** add search by query with active owner filter ([45e0ddd](https://github.com/VChet/StyleBase/commit/45e0ddd592565aa9357f3a7c19cda20a250441f3)) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * **seo:** disallow "/search/" in robots.txt ([c93ee5d](https://github.com/VChet/StyleBase/commit/c93ee5d09f2c5ae4ddcffda1614e63b0e6ed6006)) 167 | * **url:** correctly set URL when closing a modal with active filters ([a459df5](https://github.com/VChet/StyleBase/commit/a459df50cc8c902cedafb979950396530018fcdb)) 168 | 169 | ### [4.0.1](https://github.com/VChet/StyleBase/compare/v4.0.0...v4.0.1) (2021-01-11) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * **auth:** store username instead of displayName ([f3c73a5](https://github.com/VChet/StyleBase/commit/f3c73a5808e9e5a5f227aa8d9239aa40177eb56c)) 175 | * **session:** keep user logged in ([d3cbd63](https://github.com/VChet/StyleBase/commit/d3cbd63892dc98578435e1e9953e76bb97467e05)) 176 | * **style card:** add gap between "stars" and "last update" blocks ([ce45e87](https://github.com/VChet/StyleBase/commit/ce45e8720cb582a34789efd2876a6e8064ca6f3d)) 177 | * **style card:** replace text with emojis ([9e4cf64](https://github.com/VChet/StyleBase/commit/9e4cf649cbcb7189ae533bc5d0e6689e658fa642)) 178 | 179 | ## [4.0.0](https://github.com/VChet/StyleBase/compare/v3.5.0...v4.0.0) (2021-01-07) 180 | 181 | 182 | ### ⚠ BREAKING CHANGES 183 | 184 | * **styles:** URL's for styles and users have changed. 185 | New style URL: stylebase.cc/style/%url% 186 | New user URL: stylebase.cc/user/%username% 187 | 188 | ### Features 189 | 190 | * **styles:** add styleId field to be used in style url ([9ebb722](https://github.com/VChet/StyleBase/commit/9ebb722cdf3be6d45e1aab1312492bdfffcec030)) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * **favicon:** optimize svg ([9523220](https://github.com/VChet/StyleBase/commit/952322076caf69c1a82d89b4732d4c6f66a38e76)) 196 | * **parser:** store original style name ([e1d8ecc](https://github.com/VChet/StyleBase/commit/e1d8ecc21d66f69a74171974f97647f18fb6da8d)) 197 | * **style card:** show original style name ([a8f9e06](https://github.com/VChet/StyleBase/commit/a8f9e06d3e4cac9957bad66f76227f66eca21589)) 198 | 199 | ## [3.5.0](https://github.com/VChet/StyleBase/compare/v3.4.1...v3.5.0) (2021-01-03) 200 | 201 | ### Features 202 | 203 | * TypeScript support for server 204 | 205 | ### Bug Fixes 206 | 207 | * **dialogs:** update titles and paddings ([377fc6e](https://github.com/VChet/StyleBase/commit/377fc6e61eccb946c3172f4499f31df9a84d9705)) 208 | * **favicon:** optimize svg ([145fdda](https://github.com/VChet/StyleBase/commit/145fddaac22a56aac36c942ea6846ab0fa299de0)) 209 | * **favicon:** remove legacy icons ([38d9a61](https://github.com/VChet/StyleBase/commit/38d9a61452e478038c324f5a12e1964aa2c99550)) 210 | * **filter:** do not request all styles when the owner filter is active ([e118225](https://github.com/VChet/StyleBase/commit/e11822561be97dff1a7648fd05baeb3b97921563)) 211 | * **HowToUse:** update texts ([f742e79](https://github.com/VChet/StyleBase/commit/f742e79ab4a5c051298ab49d75cb6cdb13c5f5e9)) 212 | * **parser:** throw and catch metadata parser errors ([6f44ab2](https://github.com/VChet/StyleBase/commit/6f44ab2bd1ead4926d861d273d5d40420cb187df)) 213 | * **style card:** truncate name that's longer than 2 lines ([5845bec](https://github.com/VChet/StyleBase/commit/5845bec6ea337e230b0889cc1705dbc690d20114)) 214 | 215 | ### [3.4.1](https://github.com/VChet/StyleBase/compare/v3.4.0...v3.4.1) (2020-12-27) 216 | 217 | 218 | ### Bug Fixes 219 | 220 | * **alert:** support multiple alerts ([7e8f6f7](https://github.com/VChet/StyleBase/commit/7e8f6f718e4adc37c8e85d2287d5abb26455719f)) 221 | * **helmet:** add data URL to script-src directive ([37ceb3f](https://github.com/VChet/StyleBase/commit/37ceb3fe33b57897c013ecd50461b61c08f528ed)) 222 | * **parser:** do not replace parentheses in style name ([842b64b](https://github.com/VChet/StyleBase/commit/842b64bb21e3b2c720a1a66c3f49fa000ddde0a1)) 223 | 224 | ## [3.4.0](https://github.com/VChet/StyleBase/compare/v3.3.0...v3.4.0) (2020-12-27) 225 | 226 | 227 | ### Features 228 | 229 | * **parser:** fill style name, description, license from metadata ([efc6e07](https://github.com/VChet/StyleBase/commit/efc6e07b6b43c8e7025aa55cdbeaa89e389bf162)) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * **database:** fix owner id being stored as string instead of number ([c060cf0](https://github.com/VChet/StyleBase/commit/c060cf065a27f20ad7b7313e15e52623a0cd9df2)) 235 | * **features:** update texts ([f302361](https://github.com/VChet/StyleBase/commit/f302361d4a8702d19e3b3778c56d6fea26626a93)) 236 | * **login:** add authorization information ([e5fe070](https://github.com/VChet/StyleBase/commit/e5fe070cf0d94ed293d9324c36ad3acffb326133)) 237 | * **rss:** add RSS feed link in the head tag ([3536e92](https://github.com/VChet/StyleBase/commit/3536e92f1ac632d85b0c71dcc48a563a4f50f78d)) 238 | * **server:** remove Access-Control-Allow-Origin header ([b05eca6](https://github.com/VChet/StyleBase/commit/b05eca6792422e9065e60f630c5d94fdc1334c2c)) 239 | * **style edit:** fix user organizations check ([e1cd00c](https://github.com/VChet/StyleBase/commit/e1cd00cae733f0f0a3ffd6544453522938f0a6ab)) 240 | * **style update:** update style with metadata in scheduled update ([0c522e7](https://github.com/VChet/StyleBase/commit/0c522e75bb605e4c5df2911d602714346a81f052)) 241 | * **styles:** rearrange sort options, set 'most liked' as default ([bd553fc](https://github.com/VChet/StyleBase/commit/bd553fc569c63d5b389954e2d948bcf93abc5e09)) 242 | * **styles:** set initial sort order after filter reset ([4372346](https://github.com/VChet/StyleBase/commit/437234666c623fed18d761f48e4a8f737bd86724)) 243 | 244 | ## [3.3.0](https://github.com/VChet/StyleBase/compare/v3.2.1...v3.3.0) (2020-12-17) 245 | 246 | 247 | ### Features 248 | 249 | * **dark mode:** add dark theme and system theme detection ([e103613](https://github.com/VChet/StyleBase/commit/e103613243443624013181925c8376a5bb2acdf9)) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * **dark mode:** add ARIA attributes to switcher ([23bf2fa](https://github.com/VChet/StyleBase/commit/23bf2fa105b224e68f262bd47c040c652361b5f4)) 255 | * **styles:** show fetch errors in snackbar ([001c664](https://github.com/VChet/StyleBase/commit/001c66413f2963dc01f0b864e8d91ae069d6d04e)) 256 | * update color scheme and 'no-preview' image ([5233b77](https://github.com/VChet/StyleBase/commit/5233b77164df697d95c9c9a56a70dd60fd22d4d1)) 257 | 258 | ### [3.2.1](https://github.com/VChet/StyleBase/compare/v3.2.0...v3.2.1) (2020-12-16) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * **close button:** increase lines width ([376b335](https://github.com/VChet/StyleBase/commit/376b335db7afba23500ce198009555f42da189d2)) 264 | * **feature block:** fix border-width ([b8c1fd0](https://github.com/VChet/StyleBase/commit/b8c1fd03ca77f02c7664014c385122b1d3965479)) 265 | * **header:** clear filters on logo click ([e15825e](https://github.com/VChet/StyleBase/commit/e15825ef35843a51c8a970892b6ff7344e50d0c4)) 266 | 267 | ## [3.2.0](https://github.com/VChet/StyleBase/compare/v3.1.1...v3.2.0) (2020-12-15) 268 | 269 | 270 | ### Features 271 | 272 | * **add style:** set custom fields before submitting a style ([b408ab5](https://github.com/VChet/StyleBase/commit/b408ab5916ee7e3e8b65ba02a3605f08fe129156)) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * **custom fields:** fix old values rewriting ([06b4175](https://github.com/VChet/StyleBase/commit/06b4175532810b6b8e3b7c7b878c3d25f65cd1a5)) 278 | * **custom preview:** disallow icns, ico, sketch extensions ([a54719f](https://github.com/VChet/StyleBase/commit/a54719f5822193255c7f47814859f8e7079898a7)) 279 | * **style grid:** fetch style list on a style update ([cf0b3a4](https://github.com/VChet/StyleBase/commit/cf0b3a4f856c2733bdf1a473bf60a83962e639b5)) 280 | * **style update:** skip failed requests and update other styles ([08b3db8](https://github.com/VChet/StyleBase/commit/08b3db89300b424316ee3c16059a64090d4b3716)) 281 | * **styles api:** change 'edit style' request method to patch ([698b239](https://github.com/VChet/StyleBase/commit/698b2396dcfb28e7746faca81d49f01d53ee38ff)) 282 | 283 | ### [3.1.1](https://github.com/VChet/StyleBase/compare/v3.1.0...v3.1.1) (2020-12-13) 284 | 285 | 286 | ### Bug Fixes 287 | 288 | * **parser:** add repository name to parser errors ([e4c36ba](https://github.com/VChet/StyleBase/commit/e4c36baad6ff01cc6b58f0d0fd5fee97846abbdd)) 289 | * **style update:** convert style owner id value to string ([9b1c181](https://github.com/VChet/StyleBase/commit/9b1c1816e7d26d113255b299ebeecc68f1ed69c5)) 290 | * **user:** request organizations depending on a provider ([7a19bc4](https://github.com/VChet/StyleBase/commit/7a19bc4bc7afd47d2902e529d0f62f651f578735)) 291 | 292 | ## [3.1.0](https://github.com/VChet/StyleBase/compare/v3.0.3...v3.1.0) (2020-12-13) 293 | 294 | 295 | ### Features 296 | 297 | * **add style:** support codeberg.org repos ([730a5aa](https://github.com/VChet/StyleBase/commit/730a5aaa68189ba0c23d1b685b33e21588756504)) 298 | * **authentication:** add gitea login strategy ([885ddcd](https://github.com/VChet/StyleBase/commit/885ddcd69033adb275b6a0f0f981d3c0d0d5b301)) 299 | * **login:** add dialog with login options ([47ffca7](https://github.com/VChet/StyleBase/commit/47ffca731ee9fdd9ea195e5d070ce167bc478a42)) 300 | * **parser:** support codeberg.org repos ([97fdee9](https://github.com/VChet/StyleBase/commit/97fdee91f8245efbe7438dac06cc607f3418c453)) 301 | 302 | 303 | ### Bug Fixes 304 | 305 | * **add style:** fix wrong parameters when looking for duplicates ([27675f0](https://github.com/VChet/StyleBase/commit/27675f0465f02dfcfbb3f28ee93ba17a09da2441)) 306 | * **authorization:** check owner by id instead of username ([5b921ec](https://github.com/VChet/StyleBase/commit/5b921ec8bb1b4c93d98dabf0d28ec47b0bacac95)) 307 | * **parser:** handle empty url, handle unsupported hosting providers ([caddb9a](https://github.com/VChet/StyleBase/commit/caddb9a143a53ea1673d77cff244875f873b03ae)) 308 | * **style card:** capitalize first letters of a style name ([4dfbc40](https://github.com/VChet/StyleBase/commit/4dfbc408ed5fbefd24626958adc8cfc1cafff283)) 309 | * **user model:** fix creating new user with codeberg id ([ad7f2e3](https://github.com/VChet/StyleBase/commit/ad7f2e39f35ae653e947abc4bd1c454701d75f81)) 310 | 311 | ### [3.0.3](https://github.com/VChet/StyleBase/compare/v3.0.2...v3.0.3) (2020-12-09) 312 | 313 | 314 | ### Bug Fixes 315 | 316 | * **style update:** fix passing wrong argument to the parser ([a1a823b](https://github.com/VChet/StyleBase/commit/a1a823b14faabbc270b24b5ebcfa2acd67ef5d9b)) 317 | 318 | ### [3.0.2](https://github.com/VChet/StyleBase/compare/v3.0.1...v3.0.2) (2020-12-09) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * **robots.txt:** disallow /login and /logout urls ([e0a4496](https://github.com/VChet/StyleBase/commit/e0a4496e7c936f5e298f6aa470a77f03adcc2baf)) 324 | * **style model:** convert style name to lowercase ([4ef38ff](https://github.com/VChet/StyleBase/commit/4ef38ff6bfd2c94e212b25a06810071fc7a86bc3)) 325 | 326 | ### [3.0.1](https://github.com/VChet/StyleBase/compare/v3.0.0...v3.0.1) (2020-12-06) 327 | 328 | 329 | ### Bug Fixes 330 | 331 | * **alerts:** show custom style name in add/update/delete notifications ([adcbcbd](https://github.com/VChet/StyleBase/commit/adcbcbd36597dc04a2ef099abb25d905183cd27c)) 332 | 333 | ## [3.0.0](https://github.com/VChet/StyleBase/compare/v2.2.1...v3.0.0) (2020-12-04) 334 | 335 | 336 | ### ⚠ BREAKING CHANGES 337 | 338 | * **styles api:** to select style which you want to edit/delete/update pass it's _id (was url) 339 | 340 | ### Features 341 | 342 | * **style model:** remove unique flag from repository, add flag to style ([b272b1d](https://github.com/VChet/StyleBase/commit/b272b1d5f55d6bc02299a4fc1fd2a76ad80d08ca)) 343 | * **styles:** add style file selection ([eae1d73](https://github.com/VChet/StyleBase/commit/eae1d739d2dbdda878198b205e3e8ce5f5fd3169)) 344 | * **styles api:** add 'custom description' field ([e42bd82](https://github.com/VChet/StyleBase/commit/e42bd823afa2fd29d01145ac38302f8b9fef6a16)) 345 | 346 | 347 | ### Bug Fixes 348 | 349 | * **parser:** save style name without file extension ([1ab4111](https://github.com/VChet/StyleBase/commit/1ab4111ad47b06ccae7d16f826bf3bc563871821)) 350 | * **styles api:** edit/delete/update styles using _id field ([f55fbb5](https://github.com/VChet/StyleBase/commit/f55fbb53d62749d23dbb872e4749d940007eee6e)) 351 | 352 | ### [2.2.1](https://github.com/VChet/StyleBase/compare/v2.2.0...v2.2.1) (2020-11-30) 353 | 354 | 355 | ### Bug Fixes 356 | 357 | * **add style:** trim trailing slash from url ([74adde3](https://github.com/VChet/StyleBase/commit/74adde38afdad0bdb30a5a9aadeb4ed026dfb79c)) 358 | * **footer:** fix footer to the bottom of page on desktop and tablets ([cc81131](https://github.com/VChet/StyleBase/commit/cc81131d38a9e98e282b167235dd990150ac6022)) 359 | 360 | ## [2.2.0](https://github.com/VChet/StyleBase/compare/v2.1.0...v2.2.0) (2020-11-27) 361 | 362 | 363 | ### Features 364 | 365 | * **rss:** add rss generation ([0438e9c](https://github.com/VChet/StyleBase/commit/0438e9c3d76b1b5bc90dfd6c745a5484f034a3c4)) 366 | * **rss:** add rss link to footer ([4b1da26](https://github.com/VChet/StyleBase/commit/4b1da26c6cd51dcd8f65dc0f647aa5418dc5ec87)) 367 | 368 | 369 | ### Bug Fixes 370 | 371 | * **footer:** increase links font size and paddings in footer on mobile ([2b7302c](https://github.com/VChet/StyleBase/commit/2b7302c238b994db0afb7f43ee98d1ecc8b345d6)) 372 | 373 | ## [2.1.0](https://github.com/VChet/StyleBase/compare/v2.0.1...v2.1.0) (2020-11-22) 374 | 375 | 376 | ### Features 377 | 378 | * **alert:** add alert snackbar ([6dfb1ce](https://github.com/VChet/StyleBase/commit/6dfb1ce62fe23f549adbe41987f7f8ae7017930c)) 379 | * **authorization:** allow organization members to edit repositories ([2e8afb0](https://github.com/VChet/StyleBase/commit/2e8afb054167ddac2b74b95712e3b1fd4df7e190)) 380 | * **confirmation:** add confirmation dialog component ([ae03ddc](https://github.com/VChet/StyleBase/commit/ae03ddc0946d3235c732babc6d8ff633dc3df502)) 381 | * **style info:** add Twitter share button ([5640879](https://github.com/VChet/StyleBase/commit/564087947ca6bd2537a669f2293dfb51067ecd1a)) 382 | * **style info:** ask for confirmation before deleting a style ([762e4ff](https://github.com/VChet/StyleBase/commit/762e4ffe9845bd6e6c1c8a6ece7dd661f4454473)) 383 | * **user:** store user organizations ([b96a624](https://github.com/VChet/StyleBase/commit/b96a624c9741739b99c1f3387281f0aeae7cfa30)) 384 | 385 | 386 | ### Bug Fixes 387 | 388 | * **authentication:** remove scope to access only public data ([14e60da](https://github.com/VChet/StyleBase/commit/14e60da6e00ea70d520735ccb5f761d8ae6dbe92)) 389 | * **authorization:** handle empty user ([8fffddf](https://github.com/VChet/StyleBase/commit/8fffddff536abdf0f4a4d2aabe174041e96c9d7a)) 390 | * **user model:** set empty orgs array by default ([28a1032](https://github.com/VChet/StyleBase/commit/28a10325a353a35ffea213abacc1ad5af0cfb772)) 391 | 392 | ### [2.0.1](https://github.com/VChet/StyleBase/compare/v2.0.0...v2.0.1) (2020-11-17) 393 | 394 | 395 | ### Bug Fixes 396 | 397 | * **db:** send code and name on save error ([25cf268](https://github.com/VChet/StyleBase/commit/25cf26852e2683610de33505f284de803aa2fbce)) 398 | * **style info:** fix actions on style removal ([2b909c1](https://github.com/VChet/StyleBase/commit/2b909c14b3ded73ef8ffc7ca67c7f1d49e90cc17)) 399 | * fix dialog component names ([e04f767](https://github.com/VChet/StyleBase/commit/e04f76724e743fb514e9e6455ff85d75101c73da)) 400 | 401 | ## [2.0.0](https://github.com/VChet/StyleBase/compare/v1.8.0...v2.0.0) (2020-11-14) 402 | 403 | 404 | ### ⚠ BREAKING CHANGES 405 | 406 | * endpoint for styles by specific owner is changed from 'api/owner/:owner' to 'api/styles/:owner' 407 | * endpoint for styles filtered by query is removed ('api/search/'). Now use 'api/styles/' with the 'query' GET parameter 408 | 409 | ### Features 410 | 411 | * **parser:** support '.user.styl' files ([f7dd9aa](https://github.com/VChet/StyleBase/commit/f7dd9aa5043b0baedb3074d70dccf599ad8f44f6)) 412 | * **styles:** add sorting for styles filtered by query and by owner ([df46ecc](https://github.com/VChet/StyleBase/commit/df46ecc15459557b427e0ee99d2d7c98e9e86df6)) 413 | * **vuex:** add vuex, styles and user states, mutations, actions ([a036be5](https://github.com/VChet/StyleBase/commit/a036be5f43d43e99f7919f248cd58e1218c7f759)) 414 | 415 | 416 | ### Bug Fixes 417 | 418 | * **no script:** add styles for no-script block ([9b05746](https://github.com/VChet/StyleBase/commit/9b05746239811bd0da25c5d66e251afdf635fce8)) 419 | * **search:** change debounce behavior ([a7c1fc0](https://github.com/VChet/StyleBase/commit/a7c1fc0a31975315f045e00a514ffd758ef319d6)) 420 | * **search:** clear timeout on reset ([fd408c3](https://github.com/VChet/StyleBase/commit/fd408c3af3a1f3c302d31cb5454af80a3dfc306a)) 421 | * **sort order:** disable sort buttons on loading ([5651789](https://github.com/VChet/StyleBase/commit/565178905ac20551c9936d6e1a6621443b2757d2)) 422 | * **sort order:** hide sort order if only one style found ([4733f24](https://github.com/VChet/StyleBase/commit/4733f24a4e92f1467cca560ec2abf3d87648eb05)) 423 | * **styles api:** add lean parameter ([6713392](https://github.com/VChet/StyleBase/commit/6713392c5a387953ad007c8b10e55fc344600038)) 424 | * **styles api:** allow empty custom name and custom preview fields ([e154638](https://github.com/VChet/StyleBase/commit/e154638f91b66598916242a13ef009b313a3c7d6)) 425 | * **styles api:** check custom preview file extension ([a197043](https://github.com/VChet/StyleBase/commit/a19704335203e168d85a549aa38e1b9d5281815b)) 426 | * **vuex:** close modal window only if opened ([0033769](https://github.com/VChet/StyleBase/commit/0033769843ba44e26e879e1a60020d0ef0a28135)) 427 | * **vuex:** set search query even if it's empty ([8b32947](https://github.com/VChet/StyleBase/commit/8b32947df35b0fe5f8cc28df9e4ef86eea76ec0d)) 428 | 429 | 430 | * refactor!(styles api): use same function for query and owner search ([b39ffc5](https://github.com/VChet/StyleBase/commit/b39ffc5ca52436997624d5a1c596d5fa0b401631)) 431 | 432 | ## [1.8.0](https://github.com/VChet/StyleBase/compare/v1.7.0...v1.8.0) (2020-10-31) 433 | 434 | 435 | ### Features 436 | 437 | * **components:** add BaseCard component ([bbc8807](https://github.com/VChet/StyleBase/commit/bbc880768c9e654b451a7572f1358a1e74805875)) 438 | * **components:** update StyleCard and StyleCardSkeleton ([2eb0ed0](https://github.com/VChet/StyleBase/commit/2eb0ed03d90bedc58bb6eeda6af7be75bf35f2a5)) 439 | * **Components:** add skeleton loader component for StyleCard ([10f4321](https://github.com/VChet/StyleBase/commit/10f43216f71494c8ff96de6fdd2572bf1398a95e)) 440 | * **recaptcha:** use rate-limiter instead of recaptcha ([075ddbe](https://github.com/VChet/StyleBase/commit/075ddbe14d23d2467ee2ac05b15b08ef55f0a4bf)) 441 | * **style card:** change skeleton animation ([1270691](https://github.com/VChet/StyleBase/commit/1270691e3369fefd95ac60f2029961f5f3833d52)) 442 | * **style info:** add button to delete the style ([11e1bb6](https://github.com/VChet/StyleBase/commit/11e1bb64d6fd7ffd36093a9c2512e305de6251c4)) 443 | 444 | 445 | ### Bug Fixes 446 | 447 | * **helmet:** remove google recaptcha from csp ([6d2e37d](https://github.com/VChet/StyleBase/commit/6d2e37d8fd7e2ffe5a0e3b1e48bb99a5a705b77c)) 448 | * **style grid:** set skeleton cards amount depending on the current page ([27f33d8](https://github.com/VChet/StyleBase/commit/27f33d8e99c313f96c0b1e9e17396273835b2711)) 449 | * **style info:** handle submit via form ([8b6780a](https://github.com/VChet/StyleBase/commit/8b6780a9c2cb5a037dc40e87877471a973428a54)) 450 | * **styles api:** fix url parameters in update and delete requests ([959570c](https://github.com/VChet/StyleBase/commit/959570c114ea431c3ba74c9d2e8ea3dddcf754b1)) 451 | * **vue config:** add login and logout urls to devServer proxy ([d9e7a16](https://github.com/VChet/StyleBase/commit/d9e7a16e8a129f9bc6a993b94993ddee3ab0cfa4)) 452 | * **vue-loader:** preserve whitespace ([61566f1](https://github.com/VChet/StyleBase/commit/61566f1cbbcc3e891058ca3dac2bf296910caa1d)) 453 | 454 | ## [1.7.0](https://github.com/VChet/StyleBase/compare/v1.6.0...v1.7.0) (2020-10-17) 455 | 456 | 457 | ### Features 458 | 459 | * add logout button ([be178f4](https://github.com/VChet/StyleBase/commit/be178f492cc7b967a44e7d8314b9c7e4d839181f)) 460 | 461 | 462 | ### Bug Fixes 463 | 464 | * **buttons:** add types and aria-labels ([c83fea0](https://github.com/VChet/StyleBase/commit/c83fea032a90ad3b704211f8ca8383a8204eca7a)) 465 | * **style info:** add alternative text to image ([401fb94](https://github.com/VChet/StyleBase/commit/401fb9454fa8d54520fad759d5edd3dd05909da3)) 466 | * **style info:** fix mobile styles ([2380dd5](https://github.com/VChet/StyleBase/commit/2380dd529b130334efb8d4b4017c45f360c864b8)) 467 | 468 | ## [1.6.0](https://github.com/VChet/StyleBase/compare/v1.5.0...v1.6.0) (2020-10-15) 469 | 470 | 471 | ### Features 472 | 473 | * **parser:** add mercy-preview header ([a121495](https://github.com/VChet/StyleBase/commit/a12149562de37a19f534b8c0456c3f5f304bef9c)) 474 | * **style info:** add search by topic ([231f750](https://github.com/VChet/StyleBase/commit/231f750fd6b9643888f8ce774bab6eeb4220f098)) 475 | * **style info:** add topics block ([162d927](https://github.com/VChet/StyleBase/commit/162d9278d38006882166262bffd77d82cc180148)) 476 | * **styles:** store topics in DB ([f46a996](https://github.com/VChet/StyleBase/commit/f46a99621bb5ba40da22ecbf4fa3e10deaa6f8e4)) 477 | 478 | ## [1.5.0](https://github.com/VChet/StyleBase/compare/v1.4.0...v1.5.0) (2020-10-13) 479 | 480 | 481 | ### Features 482 | 483 | * **login:** add login button and user session check ([cf17bc6](https://github.com/VChet/StyleBase/commit/cf17bc61c5d266a93c7cd0a890d8618a38675b41)) 484 | * **Style:** add 'edit style' request and custom fields ([fe3b347](https://github.com/VChet/StyleBase/commit/fe3b3470539b4b6d2126f469635fdf825867950b)) 485 | * **style card:** use manually added title and preview ([b49ad47](https://github.com/VChet/StyleBase/commit/b49ad47cf51ca07410de34a6d6f7dc6ec0aa6ae4)) 486 | * **style info:** add "edit style" block ([a95cc8d](https://github.com/VChet/StyleBase/commit/a95cc8d7ef00fd1bc09b592b614092140add0298)) 487 | 488 | 489 | ### Bug Fixes 490 | 491 | * **add style:** move disabled state from exit button to submit button ([9af0285](https://github.com/VChet/StyleBase/commit/9af0285c6fd5865a342c2b21e2f62fd6514a3451)) 492 | * **analytics:** set correct page path ([a075166](https://github.com/VChet/StyleBase/commit/a075166b8cb6a0aad0ae0ddae55e41da547c9620)) 493 | * **authentication:** change scope from email to profile data ([63b170d](https://github.com/VChet/StyleBase/commit/63b170dde08d5b65d26a1351c18e15933c35ce42)) 494 | * **buttons:** set width and height globally, add mobile styles ([4b31241](https://github.com/VChet/StyleBase/commit/4b31241435c9155f7a54bcd0d9f807f4980402fd)) 495 | * **edit style:** add custom preview protocol check ([9b9d46f](https://github.com/VChet/StyleBase/commit/9b9d46f71b19524840cd7ce420d7e19409eda9cb)) 496 | * **edit style:** allow owner to edit the style ([2bd833e](https://github.com/VChet/StyleBase/commit/2bd833e3837c0308872c36b14e0a24302b0669e7)) 497 | * **edit style:** clear cache on style edit and delete ([599891e](https://github.com/VChet/StyleBase/commit/599891ede90003fa277b636e491b813650878fcf)) 498 | * **edit style:** update only customName and customPreview fields ([944c6e9](https://github.com/VChet/StyleBase/commit/944c6e90fccc36fc0e69fe8ee17a0259e81150d4)) 499 | * **helmet:** allow images from any secure source ([1fa889b](https://github.com/VChet/StyleBase/commit/1fa889bafaf955b4c3bdd49ddcc4c57e44f85675)) 500 | * **update style:** use url instead of _id to update style ([be1cc0f](https://github.com/VChet/StyleBase/commit/be1cc0fb7d155863a330a41637e778a47dace25e)) 501 | 502 | ## [1.4.0](https://github.com/VChet/StyleBase/compare/v1.3.1...v1.4.0) (2020-10-10) 503 | 504 | 505 | ### Features 506 | 507 | * **header:** add burger menu ([a8dc3a6](https://github.com/VChet/StyleBase/commit/a8dc3a693d21b61ebf9fb770beb6057fb9416d1f)) 508 | 509 | 510 | ### Bug Fixes 511 | 512 | * **helmet:** add githubassets.com to img-src CSP ([fde5008](https://github.com/VChet/StyleBase/commit/fde5008ebe3eea2771d00b28c20eccfa56bf1cbe)) 513 | * make "b" uppercase in the site name ([a5eee88](https://github.com/VChet/StyleBase/commit/a5eee886b715669a73403d52045811ff522aa78c)) 514 | * **helmet:** add data scheme to img-src CSP ([c3485dc](https://github.com/VChet/StyleBase/commit/c3485dc25931da392264622dbd93e76a395ca36a)) 515 | * **styles:** add margin to title on mobile ([b9da67c](https://github.com/VChet/StyleBase/commit/b9da67ccdc0204c014e5420b37c16bfcee51d0f6)) 516 | 517 | ### [1.3.1](https://github.com/VChet/StyleBase/compare/v1.3.0...v1.3.1) (2020-10-08) 518 | 519 | 520 | ### Bug Fixes 521 | 522 | * **parser:** add error status and text fields ([6349c5a](https://github.com/VChet/StyleBase/commit/6349c5a84ed56eaf983de1b36802ba16747e567a)) 523 | * **parser:** set default branch ([6d41942](https://github.com/VChet/StyleBase/commit/6d4194290ddc6265070f75975134ea5035449ade)) 524 | * **rate limiter:** send error message as object ([be6f2fc](https://github.com/VChet/StyleBase/commit/be6f2fcb7bc83485b9df45511faeca795ae5bb61)) 525 | * **routes:** disable middleware checks on non production environment ([3f65f15](https://github.com/VChet/StyleBase/commit/3f65f155f8fafa709ae0046da2fd6ce1954ad2c4)) 526 | * **style info:** restore no-scroll class in body when modal is active ([62863f7](https://github.com/VChet/StyleBase/commit/62863f74122832004e04ea6591104070a57f6a2c)) 527 | 528 | ## [1.3.0](https://github.com/VChet/StyleBase/compare/v1.2.0...v1.3.0) (2020-10-07) 529 | 530 | 531 | ### Features 532 | 533 | * **footer:** add contact email ([0277f51](https://github.com/VChet/StyleBase/commit/0277f515de349f93b2f2104497dd38f754ae3f37)) 534 | * **privacy policy:** add privacy policy popup ([480b0dc](https://github.com/VChet/StyleBase/commit/480b0dc6eb51a0c77572814059589eea7b197073)) 535 | 536 | 537 | ### Bug Fixes 538 | 539 | * **header:** scroll to top on logo click ([b001569](https://github.com/VChet/StyleBase/commit/b0015691a34cb5a372fc72d5b8076e61e1be2995)) 540 | * **style card:** add image for empty preview ([f6775b3](https://github.com/VChet/StyleBase/commit/f6775b304648c7f49e9933a6091a2d4cbc2121b0)) 541 | * set document title when changing history state ([c6c608a](https://github.com/VChet/StyleBase/commit/c6c608a1acfc01b7faa6bd235418d300ade841a8)) 542 | 543 | ## [1.2.0](https://github.com/VChet/StyleBase/compare/v1.1.1...v1.2.0) (2020-10-04) 544 | 545 | 546 | ### Features 547 | 548 | * **analytics:** add events for StyleCard and AddStyleDialog ([0dd1abb](https://github.com/VChet/StyleBase/commit/0dd1abba65889aa1e7f5296ee58735f3e09e4239)) 549 | * support site search in omnibox ([d801c51](https://github.com/VChet/StyleBase/commit/d801c51991d7ef5f3f047306510af0349d9ca4cc)) 550 | 551 | 552 | ### Bug Fixes 553 | 554 | * **agenda:** remove condition when agenda stops ([2c5e5d7](https://github.com/VChet/StyleBase/commit/2c5e5d7d7465d47e6c36a549cd58e5c4abe2d2f7)) 555 | * **parser:** prioritize image with "preview" in file name ([620bfd6](https://github.com/VChet/StyleBase/commit/620bfd6c734a286b0b5453dff7d09952de7bf89e)) 556 | * **style card:** cut long names ([683b300](https://github.com/VChet/StyleBase/commit/683b300cd540541789daa7041a523b14c15e5328)) 557 | * **style info:** add mixin to replace dash and underscore in style name ([179c058](https://github.com/VChet/StyleBase/commit/179c0581db932d6f40ff6df6e80e0b8fa5e952f0)) 558 | * **style info:** parse and replace emojis in description ([45dc100](https://github.com/VChet/StyleBase/commit/45dc1009e80785d6d8c789536dbdf8c59865434a)) 559 | * **style info:** update links ([021a073](https://github.com/VChet/StyleBase/commit/021a073f5670c65c3025abffddee438d4986e0f8)) 560 | 561 | ### [1.1.1](https://github.com/VChet/StyleBase/compare/v1.1.0...v1.1.1) (2020-10-03) 562 | 563 | 564 | ### Bug Fixes 565 | 566 | * **add style:** fix style name in successful response ([d4e5e91](https://github.com/VChet/StyleBase/commit/d4e5e91624be160b1a0a693c0e9e9089528e20c5)) 567 | * **search bar:** change placeholder text ([fa52b64](https://github.com/VChet/StyleBase/commit/fa52b647f4377e4aecb80c34e6f39abc63158759)) 568 | 569 | ## [1.1.0](https://github.com/VChet/StyleBase/compare/v1.0.0...v1.1.0) (2020-10-03) 570 | 571 | 572 | ### Features 573 | 574 | * **Home:** add owner filter to url ([dd421db](https://github.com/VChet/StyleBase/commit/dd421db992558555b65673ba1c347e8131b4b84f)) 575 | * **search:** add search query to url ([a9cff70](https://github.com/VChet/StyleBase/commit/a9cff70a330f924a66514f2ba6ec7441da507a44)) 576 | * **server:** add maintenance page ([721ec68](https://github.com/VChet/StyleBase/commit/721ec68f75966ce17660ad59273bc60f6c45aed5)) 577 | * **style info:** open style popup with data from url ([0d801fa](https://github.com/VChet/StyleBase/commit/0d801fa32b9314e117eec545631e8a9bed409b2f)) 578 | * **style popup:** add owner and style name to url ([3b7f4a8](https://github.com/VChet/StyleBase/commit/3b7f4a829db680564032ce666a6095af927936cc)) 579 | 580 | 581 | ### Bug Fixes 582 | 583 | * **add style:** show clear button only with text ([422125d](https://github.com/VChet/StyleBase/commit/422125da18a27e49a53c994289f704112f5ab9c9)) 584 | * **agenda:** stop agenda on non-production deployments ([fdfbdc6](https://github.com/VChet/StyleBase/commit/fdfbdc65afe26a90889d68e1211dfea9d0dd415f)) 585 | * **get style:** get style by owner and name instead of _id ([cdfcd45](https://github.com/VChet/StyleBase/commit/cdfcd452a4e36812e794634a761ed9a85f6b7343)) 586 | * **Home:** move container to component ([18bcdfe](https://github.com/VChet/StyleBase/commit/18bcdfe7063801113337cd54d445ab0ba12c003d)) 587 | * **parser:** store repo names with dashes, replace to spaces in view ([6136971](https://github.com/VChet/StyleBase/commit/6136971e98c23a06a4fabfa2728142c90ea56655)) 588 | * **sort options:** remove list margin, compensate bottom padding ([3aecb4c](https://github.com/VChet/StyleBase/commit/3aecb4c82e59945a922f82d48f538d1e20bb200d)) 589 | * **style grid:** make grid fill all available space ([4848482](https://github.com/VChet/StyleBase/commit/484848208277e96bb02a0758a63430a6152dd987)) 590 | * **style info:** make a separate request for style data ([9a87110](https://github.com/VChet/StyleBase/commit/9a871109c74b8c28f84e9d99ab4810abc575b86d)) 591 | * **url:** fix url when changing history state ([4ce6f49](https://github.com/VChet/StyleBase/commit/4ce6f49b6249c9b0b7ae89a467b4667c8b7b630a)) 592 | 593 | ## 1.0.0 (2020-10-02) 594 | 595 | 596 | ### Features 597 | 598 | * Initial release 599 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2020` `StyleBase` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StyleBase 2 | 3 | [![TypeScript](https://img.shields.io/badge/types-TypeScript-007acc)](https://www.typescriptlang.org) 4 | [![CodeClimate](https://api.codeclimate.com/v1/badges/a40a8f663fdbaa36e28a/maintainability)](https://codeclimate.com/github/VChet/StyleBase/maintainability) 5 | [![CC](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org) 6 | 7 | Site for UserCSS theme sharing. Supports GitHub and Codeberg repositories. 8 | 9 | ![Preview](./meta/preview.png) 10 | 11 | ## Development 12 | 13 | ### Server 14 | 15 | 1. Clone repository 16 | 1. Install [Node.js](https://nodejs.org/en/download/package-manager/) 17 | 1. Install all dependencies `npm install` 18 | 1. Copy `config.template.ts`, rename it to `config.ts` and configure 19 | 20 | ```sh 21 | cp config.template.ts config.ts 22 | ``` 23 | 24 | 1. Connect to the database (skip if using remote DB) 25 | - Install [mongoDB](https://www.mongodb.com/download-center/community) 26 | - Start database `mongod` 27 | 1. Start server `npm start` 28 | 1. Build client files `npm run client:build` 29 | 30 | ### [Client](./client/README.md) 31 | 32 | ## License 33 | 34 | This project is licensed under the [MIT License](./LICENSE.md). 35 | 36 | ## Support 37 | 38 | You can support this project on [LiberaPay](https://en.liberapay.com/VChet/) or [Ko-fi](https://ko-fi.com/vchet). 39 | -------------------------------------------------------------------------------- /api/parser.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import repoImages from "repo-images"; 3 | import metaParser from "usercss-meta"; 4 | 5 | import type { AxiosRequestConfig, AxiosResponse } from "axios"; 6 | import type { IStyle } from "../models/Style"; 7 | import type { GitHubRepository, CodebergRepository, File } from "../types/api"; 8 | import type { Provider } from "../types/server"; 9 | 10 | import config from "../config"; 11 | import providers from "./providers"; 12 | 13 | const stylePattern = /\.user\.(css|styl)$/; 14 | 15 | function getProviderData(url: string) { 16 | const { host, pathname } = new URL(url); 17 | if (pathname === "/") throw new Error("Empty repository URL"); 18 | 19 | const provider = providers.find((pr) => pr.host === host); 20 | if (!provider) throw new Error("Unsupported Git hosting provider"); 21 | 22 | return { 23 | provider, 24 | repoUrl: pathname 25 | }; 26 | } 27 | 28 | async function getFolderFiles(provider: Provider, root: Array): Promise> { 29 | const files: Array = []; 30 | files.push(...root.filter((file) => file.type === "file")); 31 | 32 | const folders = root.filter((file) => file.type === "dir"); 33 | if (folders.length) { 34 | await Promise.all(folders.map(async (folder) => { 35 | const contents: AxiosResponse> = await axios.get(folder.url, provider.options); 36 | files.push(...(await getFolderFiles(provider, contents.data))); 37 | })); 38 | } 39 | 40 | return files.filter((file) => stylePattern.test(file.name)); 41 | } 42 | 43 | export async function retrieveRepositoryFiles(url: string) { 44 | const { provider, repoUrl } = getProviderData(url); 45 | const contents: AxiosResponse> = await axios.get( 46 | `${provider.api}/repos${repoUrl}/contents`, 47 | provider.options 48 | ); 49 | return getFolderFiles(provider, contents.data); 50 | } 51 | 52 | async function retrieveStyleMetadata(fileUrl: string, options: AxiosRequestConfig) { 53 | try { 54 | const content = await axios.get(fileUrl, options); 55 | const data = metaParser.parse(content.data); 56 | if (data.errors.length) console.log(data.errors); 57 | return data.metadata; 58 | } catch (error) { 59 | throw new Error(error); 60 | } 61 | } 62 | 63 | async function collectGithubData(repo: GitHubRepository) { 64 | const branch = repo.default_branch; 65 | const images = await repoImages(repo.full_name, { token: config.github.token, branch }); 66 | let preview: string | undefined; 67 | if (images.length) { 68 | let previewObj = images.find((img) => img.path.includes("preview")); 69 | if (!previewObj) previewObj = images.reduce((a, b) => (a.size > b.size ? a : b)); 70 | preview = `https://raw.githubusercontent.com/${repo.full_name}/${branch}/${previewObj.path}`; 71 | } 72 | 73 | return { 74 | url: repo.html_url, 75 | preview, 76 | name: repo.name, 77 | description: repo.description, 78 | owner: { 79 | login: repo.owner.login, 80 | id: repo.owner.id 81 | }, 82 | created: repo.created_at, 83 | lastUpdate: repo.updated_at, 84 | topics: repo.topics, 85 | stargazers: repo.stargazers_count, 86 | watchers: repo.subscribers_count, 87 | forks: repo.forks, 88 | issues: repo.open_issues, 89 | license: (repo.license && repo.license.spdx_id) || "", 90 | isPrivate: repo.private, 91 | isArchived: repo.archived, 92 | isFork: repo.fork 93 | }; 94 | } 95 | 96 | function collectCodebergData(repo: CodebergRepository) { 97 | return { 98 | url: repo.html_url, 99 | name: repo.name, 100 | description: repo.description, 101 | owner: { 102 | login: repo.owner.login, 103 | id: repo.owner.id 104 | }, 105 | created: repo.created_at, 106 | lastUpdate: repo.updated_at, 107 | stargazers: repo.stars_count, 108 | watchers: repo.watchers_count, 109 | forks: repo.forks_count, 110 | issues: repo.open_issues_count, 111 | isPrivate: repo.private, 112 | isArchived: repo.archived, 113 | isFork: repo.fork 114 | }; 115 | } 116 | 117 | export async function retrieveRepositoryData(url: string, usercss: Pick | null = null) { 118 | const { provider, repoUrl } = getProviderData(url); 119 | 120 | const repo = await axios.get(`${provider.api}/repos${repoUrl}`, provider.options); 121 | if (repo.data.private) throw new Error(`${repo.data.name} repository is private`); 122 | if (repo.data.archived) throw new Error(`${repo.data.name} repository is archived`); 123 | 124 | let styleData: Partial; 125 | if (provider.name === "GitHub") { 126 | styleData = await collectGithubData(repo.data); 127 | } else { 128 | styleData = collectCodebergData(repo.data); 129 | } 130 | 131 | if (repo.data.fork) { 132 | styleData.parent = { 133 | name: repo.data.parent.full_name, 134 | url: repo.data.parent.html_url 135 | }; 136 | } 137 | 138 | if (usercss) { 139 | const metadata = await retrieveStyleMetadata(usercss.download_url, provider.options).catch((error) => { 140 | throw new Error(`${repo.data.owner.login}/${repo.data.name}. ${error.message}`); 141 | }); 142 | styleData.usercss = usercss.download_url; 143 | styleData.name = metadata.name; 144 | if (metadata.description) styleData.description = metadata.description; 145 | if (metadata.license) styleData.license = metadata.license; 146 | } 147 | 148 | return styleData; 149 | } 150 | -------------------------------------------------------------------------------- /api/providers.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "../types/server"; 2 | 3 | import config from "../config"; 4 | 5 | export default [ 6 | { 7 | name: "GitHub", 8 | host: "github.com", 9 | api: "https://api.github.com", 10 | idField: "githubId", 11 | options: { 12 | headers: { 13 | Authorization: `token ${config.github.token}`, 14 | Accept: "application/vnd.github.mercy-preview+json" 15 | } 16 | } 17 | }, 18 | { 19 | name: "Codeberg", 20 | host: "codeberg.org", 21 | api: "https://codeberg.org/api/v1", 22 | idField: "codebergId", 23 | options: {} 24 | } 25 | ] as Array; 26 | -------------------------------------------------------------------------------- /api/routes-api.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import mcache from "memory-cache"; 3 | import rateLimit from "express-rate-limit"; 4 | 5 | import type { Request, Response, NextFunction } from "express"; 6 | 7 | import { Style } from "../models/Style"; 8 | 9 | import { 10 | getStyles, 11 | getRepositoryFiles, 12 | getStyleData, 13 | addStyle, 14 | updateAllStyles, 15 | updateStyle, 16 | editStyle, 17 | deleteStyle, 18 | getAllTopics 19 | } from "./styles"; 20 | 21 | import { getUser, getUserStyles } from "./users"; 22 | 23 | export const router = Router(); 24 | 25 | // Cache 26 | const cache = (duration: number) => (req: Request, res: Response, next: NextFunction) => { 27 | const key = `__express__${req.originalUrl}` || req.url; 28 | const cachedBody = mcache.get(key); 29 | if (cachedBody) return res.send(cachedBody); 30 | 31 | res.sendResponse = res.send; 32 | res.send = (body) => { 33 | if (process.env.NODE_ENV !== "production" && res.statusCode === 200) { 34 | mcache.put(key, body, duration * 60 * 1000); 35 | } 36 | return res.sendResponse(body); 37 | }; 38 | next(); 39 | }; 40 | const clearCache = (_req: Request, _res: Response, next: NextFunction) => { 41 | mcache.clear(); 42 | next(); 43 | }; 44 | 45 | // Rate limiter (allow 30 request per 60 minutes) 46 | const rateLimiter = rateLimit({ 47 | windowMs: 60 * 60 * 1000, 48 | max: 30, 49 | message: { 50 | status: 429, 51 | message: "Too many update requests made from this IP, please try again after 1 hour" 52 | }, 53 | skip: () => process.env.NODE_ENV !== "production" 54 | }); 55 | 56 | const isAuthorized = async (req: Request, res: Response, next: NextFunction) => { 57 | const { _id } = req.body; 58 | if (!_id) return res.status(400).json({ error: "Request must contain _id field" }); 59 | const existingStyle = await Style.findById(_id).lean(); 60 | if (!existingStyle) return res.status(404).json({ error: "Style does not exist" }); 61 | req.styleData = existingStyle; 62 | 63 | if (process.env.NODE_ENV !== "production") return next(); 64 | 65 | if (!req.isAuthenticated()) { 66 | return res.status(401).json({ error: "Authentication is required to perform this action" }); 67 | } 68 | const isAdmin = req.user.role === "Admin"; 69 | const isOwner = [req.user.githubId, req.user.codebergId].includes(existingStyle.owner.id); 70 | const userOrgs = req.user.orgs.map((org) => org.id); 71 | const isMember = userOrgs.includes(existingStyle.owner.id); 72 | if (!isAdmin && !isOwner && !isMember) { 73 | return res.status(403).json({ error: "You are not authorized to perform this action" }); 74 | } 75 | next(); 76 | }; 77 | 78 | router.get("/styles/:owner?", cache(10), getStyles); 79 | router.get("/style/files", cache(10), getRepositoryFiles); 80 | router.get("/style", cache(10), getStyleData); 81 | router.post("/style/add", rateLimiter, addStyle); 82 | router.put("/style/update/all", rateLimiter, clearCache, updateAllStyles); 83 | router.put("/style/update", rateLimiter, clearCache, updateStyle); 84 | router.patch("/style/edit", isAuthorized, clearCache, editStyle); 85 | router.delete("/style/delete", isAuthorized, clearCache, deleteStyle); 86 | router.get("/topics", cache(10), getAllTopics); 87 | 88 | router.get("/me", getUser); 89 | router.get("/user/styles", getUserStyles); 90 | 91 | export default router; 92 | -------------------------------------------------------------------------------- /api/rss.ts: -------------------------------------------------------------------------------- 1 | import { Feed } from "feed"; 2 | 3 | import type { Request, Response } from "express"; 4 | 5 | import { Style } from "../models/Style"; 6 | 7 | export default async function getRss(_req: Request, res: Response) { 8 | const siteUrl = "https://stylebase.cc"; 9 | const feed = new Feed({ 10 | title: "StyleBase", 11 | description: "Website styles from various authors", 12 | id: siteUrl, 13 | link: siteUrl, 14 | favicon: `${siteUrl}/favicon.ico`, 15 | author: { 16 | name: "StyleBase", 17 | link: siteUrl, 18 | email: "feedback@stylebase.cc" 19 | }, 20 | copyright: "All styles belong to their first authors" 21 | }); 22 | 23 | const styles = await Style.find({}).lean(); 24 | styles.forEach((style) => { 25 | const content: Array = []; 26 | content.push(`${style.customName || style.name} by ${style.owner.login}.`); 27 | content.push(`Install UserCSS`); 28 | content.push(`Repository`); 29 | 30 | feed.addItem({ 31 | title: style.customName || style.name, 32 | id: style._id, 33 | link: `${siteUrl}/style/${style.styleId}`, 34 | description: style.customDescription || style.description, 35 | content: content.join("
"), 36 | author: [{ name: style.owner.login }], 37 | date: style._id.getTimestamp(), 38 | image: style.customPreview || style.preview 39 | }); 40 | }); 41 | 42 | res.type("application/xml").send(feed.rss2()); 43 | } 44 | -------------------------------------------------------------------------------- /api/styles.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import type { CallbackError, FilterQuery, PaginateOptions } from "mongoose"; 3 | import type { AxiosError } from "axios"; 4 | import type { IStyle } from "../models/Style"; 5 | 6 | import { Style } from "../models/Style"; 7 | import { retrieveRepositoryFiles, retrieveRepositoryData } from "./parser"; 8 | 9 | function handleParserError(res: Response, error: AxiosError & CallbackError) { 10 | if (error.response) { 11 | return res.status(error.response.status).json({ error: error.response.statusText }); 12 | } 13 | if (error.message) { 14 | return res.status(400).json({ error: error.message }); 15 | } 16 | console.log(error); 17 | res.status(500).json({ error: "Unhandled server error" }); 18 | } 19 | 20 | function checkPreviewUrl(url: string) { 21 | const previewUrl = new URL(url); 22 | const imagePattern = /\.(png|gif|jpg|svg|bmp)$/i; 23 | if (!previewUrl.protocol.includes("https:")) throw new Error("Preview must be from a secure source"); 24 | if (!imagePattern.test(previewUrl.pathname)) throw new Error("Preview file must be an image"); 25 | } 26 | 27 | function escapeRegex(text: string) { 28 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 29 | } 30 | 31 | export function getRepositoryFiles(req: Request, res: Response) { 32 | if (!req.query.url || typeof req.query.url !== "string") throw new Error("Invalid repository URL"); 33 | const url = req.query.url.replace(/\/$/, ""); // Trim trailing slash 34 | 35 | retrieveRepositoryFiles(url) 36 | .then((files) => { 37 | if (!files.length) return res.status(400).json({ error: "Repository does not contain UserCSS files" }); 38 | res.status(200).json({ files }); 39 | }) 40 | .catch((error) => { 41 | handleParserError(res, error); 42 | }); 43 | } 44 | 45 | export function getStyles(req: Request, res: Response) { 46 | const { query, page = "1", limit = "16", sort } = req.query as { [key: string]: string }; 47 | const { owner } = req.params; 48 | 49 | let filter: FilterQuery = {}; 50 | if (query) { 51 | const queryRegExp = new RegExp(escapeRegex(query), "gi"); 52 | filter = { 53 | $or: [ 54 | { name: queryRegExp }, 55 | { customName: queryRegExp }, 56 | { "owner.login": queryRegExp }, 57 | { topics: queryRegExp } 58 | ] 59 | }; 60 | } 61 | if (owner) filter["owner.login"] = owner; 62 | 63 | let sortOrder: { [key: string]: 1 | -1 } = { _id: -1 }; 64 | if (sort === "stargazers") { 65 | sortOrder = { stargazers: -1, _id: -1 }; 66 | } else if (sort === "lastUpdate") { 67 | sortOrder = { lastUpdate: -1, _id: -1 }; 68 | } 69 | 70 | const options: PaginateOptions = { 71 | page: parseInt(page, 10), 72 | limit: parseInt(limit, 10), 73 | sort: sortOrder, 74 | customLabels: { totalDocs: "totalStyles", docs: "styles" }, 75 | lean: true, 76 | collation: { locale: "en" } 77 | }; 78 | 79 | Style.paginate(filter, options, (error, data) => { 80 | if (error) return res.status(500).json({ error }); 81 | return res.status(200).json(data); 82 | }); 83 | } 84 | 85 | export function getStyleData(req: Request, res: Response) { 86 | const { styleId } = req.query; 87 | if (!styleId) return res.status(400).json({ error: "Request must contain styleId field" }); 88 | if (typeof styleId !== "string") return res.status(400).json({ error: "Invalid styleId" }); 89 | Style.findOne({ styleId }).lean().exec((error: CallbackError, style) => { 90 | if (error) return res.status(500).json({ error }); 91 | if (!style) return res.status(404).json({ error: "Style not found" }); 92 | return res.status(200).json({ style }); 93 | }); 94 | } 95 | 96 | export function addStyle(req: Request, res: Response) { 97 | const { usercss, customName, customDescription, customPreview } = req.body; 98 | const url = req.body.url.replace(/\/$/, ""); // Trim trailing slash 99 | 100 | Style.findOne({ usercss: usercss.download_url }).lean().exec(async (mongoError: CallbackError, style) => { 101 | if (mongoError) return res.status(500).json({ error: mongoError }); 102 | if (style) return res.status(409).json({ error: "Style was already added to our base" }); 103 | 104 | if (customPreview) { 105 | try { 106 | checkPreviewUrl(customPreview); 107 | } catch (error) { 108 | return res.status(400).json({ error: error.message }); 109 | } 110 | } 111 | 112 | retrieveRepositoryData(url, usercss) 113 | .then((data) => { 114 | const newStyle = new Style({ 115 | ...data, 116 | customName, 117 | customDescription, 118 | customPreview 119 | }); 120 | newStyle.save((saveError) => { 121 | if (saveError) return res.status(500).json({ error: `${saveError.name}: ${saveError.message}` }); 122 | res.status(201).json({ style: newStyle }); 123 | }); 124 | }) 125 | .catch((error) => { 126 | handleParserError(res, error); 127 | }); 128 | }); 129 | } 130 | 131 | export function updateStyle(req: Request, res: Response) { 132 | const { _id } = req.body; 133 | if (!_id) return res.status(400).json({ error: "Request must contain _id field" }); 134 | 135 | Style.findById(_id).lean().exec(async (mongoError: CallbackError, style) => { 136 | if (mongoError) return res.status(500).json({ error: mongoError }); 137 | if (!style) return res.status(404).json({ error: "Style was not found in our base" }); 138 | 139 | retrieveRepositoryData(style.url, { download_url: style.usercss }) 140 | .then((data) => { 141 | Style.findByIdAndUpdate(_id, data, { new: true }, (updateError: CallbackError, result) => { 142 | if (updateError) return res.status(500).json({ error: updateError }); 143 | return res.status(200).json({ result }); 144 | }); 145 | }) 146 | .catch((error) => { 147 | res.status(400).json({ error: error.message }); 148 | }); 149 | }); 150 | } 151 | 152 | export function updateAllStyles(_req: Request, res: Response) { 153 | Style.updateAllStyles() 154 | .then((result) => { 155 | res.status(200).json({ result }); 156 | }) 157 | .catch((error) => { 158 | handleParserError(res, error); 159 | }); 160 | } 161 | 162 | export function editStyle(req: Request, res: Response) { 163 | const { 164 | _id, customName, customDescription, customPreview 165 | } = req.body; 166 | 167 | if (customPreview) { 168 | try { 169 | checkPreviewUrl(customPreview); 170 | } catch (error) { 171 | return res.status(400).json({ error: error.message }); 172 | } 173 | } 174 | 175 | Style.findByIdAndUpdate( 176 | _id, 177 | { $set: { customName, customDescription, customPreview } }, 178 | { new: true }, 179 | (error, style) => { 180 | if (error) return res.status(500).json({ error }); 181 | return res.status(200).json({ style }); 182 | } 183 | ); 184 | } 185 | 186 | export async function deleteStyle(req: Request, res: Response) { 187 | Style.findByIdAndDelete(req.body._id, null, (error, style) => { 188 | if (error) return res.status(500).json({ error }); 189 | return res.status(200).json({ style }); 190 | }); 191 | } 192 | 193 | export function getAllTopics(_req: Request, res: Response) { 194 | const pipeline = [ 195 | { $unwind: "$topics" }, 196 | { $group: { _id: "$topics", count: { $sum: 1 } } }, 197 | { $match: { 198 | $and: [ 199 | { count: { $gte: 3 } }, 200 | { _id: { $nin: ["css", "awesome", "stylish", "stylus", /theme/, /usercss/, /userstyle/] } } 201 | ] 202 | } }, 203 | { $project: { _id: 0, name: "$_id", count: 1 } } 204 | ]; 205 | Style.aggregate(pipeline).exec((error, topics: Array<{ name: string, count: number }>) => { 206 | if (error) return res.status(500).json({ error }); 207 | return res.status(200).json({ topics }); 208 | }); 209 | } 210 | -------------------------------------------------------------------------------- /api/users.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | 3 | import { Style } from "../models/Style"; 4 | 5 | export function getUser(req: Request, res: Response) { 6 | if (!req.user) return res.status(200).json({ error: "User session not found" }); 7 | return res.status(200).json({ user: req.user }); 8 | } 9 | 10 | export function getUserStyles(req: Request, res: Response) { 11 | const { githubId, codebergId } = req.query; 12 | if (!githubId && !codebergId) return res.status(400).json({ error: "Missing user id" }); 13 | 14 | const ids: Array = []; 15 | if (typeof githubId === "string") ids.push(parseInt(githubId, 10)); 16 | if (typeof codebergId === "string") ids.push(parseInt(codebergId, 10)); 17 | Style.find({ "owner.id": { $in: ids } }).lean().exec((error, styles) => { 18 | if (error) return res.status(500).json({ error }); 19 | res.status(200).json({ styles }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /client/.env.template: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=http://localhost 2 | VUE_APP_API_PORT=7540 3 | VUE_APP_GTAG_ID= 4 | VUE_APP_CLOUDFLARE_TOKEN= 5 | -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /tests/unit/coverage/ 4 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | 'plugin:vue/strongly-recommended', 9 | 'plugin:vue/recommended', 10 | 'eslint:recommended', 11 | '@vue/prettier' 12 | ], 13 | parserOptions: { 14 | parser: 'babel-eslint' 15 | }, 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? ['warn', { allow: ['error'] }] : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'vue/component-name-in-template-casing': ['warn', 'PascalCase'], 20 | 'vue/match-component-file-name': ['error', { extensions: ['vue'], shouldMatchCase: true }], 21 | 'prettier/prettier': ['warn', { endOfLine: 'auto' }] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # env files 6 | .env 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /client/.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules/** 2 | /dist/** 3 | /tests/unit/coverage/** 4 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "endOfLine": "crlf", 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # StyleBase Frontend 2 | 3 | ![Vue](https://img.shields.io/github/package-json/dependency-version/VChet/StyleBase/vue?color=41b883&filename=client%2Fpackage.json) 4 | ![Vuex](https://img.shields.io/github/package-json/dependency-version/VChet/StyleBase/vuex?color=41b883&filename=client%2Fpackage.json) 5 | [![CodeClimate](https://api.codeclimate.com/v1/badges/a40a8f663fdbaa36e28a/maintainability)](https://codeclimate.com/github/VChet/StyleBase/maintainability) 6 | [![CC](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org) 7 | 8 | ## Development 9 | 10 | ### Client 11 | 12 | 1. Install all dependencies `npm install` 13 | 1. Copy `.env.template`, rename it to `.env` and configure 14 | 15 | ```sh 16 | cp .env.template .env 17 | ``` 18 | 19 | 1. Start a dev server `npm start` or build files to serve via node server `npm run build` 20 | 21 | ### [Server](../README.md) 22 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylebase-frontend", 3 | "version": "4.6.2", 4 | "author": "VChet", 5 | "contributors": [ 6 | "rudnovd" 7 | ], 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/VChet/StyleBase.git" 12 | }, 13 | "scripts": { 14 | "start": "vue-cli-service serve", 15 | "build": "vue-cli-service build --modern --no-unsafe-inline", 16 | "lint": "vue-cli-service lint" 17 | }, 18 | "engines": { 19 | "node": "^14.16.0" 20 | }, 21 | "dependencies": { 22 | "axios": "^0.21.1", 23 | "core-js": "^3.16.2", 24 | "dayjs": "^1.10.6", 25 | "node-emoji": "^1.11.0", 26 | "normalize.css": "^8.0.1", 27 | "v-click-outside": "^3.1.2", 28 | "vue": "^2.6.14", 29 | "vue-gtag": "^1.16.1", 30 | "vue-router": "^3.5.2", 31 | "vuex": "^3.6.2" 32 | }, 33 | "devDependencies": { 34 | "@vue/cli-plugin-babel": "^4.5.13", 35 | "@vue/cli-plugin-eslint": "^4.5.13", 36 | "@vue/cli-service": "^4.5.13", 37 | "@vue/eslint-config-prettier": "^6.0.0", 38 | "babel-eslint": "^10.1.0", 39 | "eslint": "^7.32.0", 40 | "eslint-plugin-prettier": "^3.4.1", 41 | "eslint-plugin-vue": "^7.16.0", 42 | "prettier": "^2.3.2", 43 | "sass": "^1.38.1", 44 | "sass-loader": "~10.2.0", 45 | "vue-template-compiler": "^2.6.14" 46 | }, 47 | "gitHooks": { 48 | "pre-commit": "vue-cli-service lint" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/public/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/public/192.png -------------------------------------------------------------------------------- /client/public/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/public/512.png -------------------------------------------------------------------------------- /client/public/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/public/apple.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Collection of UserCSS styles | StyleBase 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /client/public/opensearchdescription.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | StyleBase 4 | utf-8 5 | 6 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Disallow: /login$ 4 | Disallow: /logout$ 5 | Disallow: /search/ 6 | 7 | Allow: / 8 | -------------------------------------------------------------------------------- /client/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StyleBase", 3 | "short_name": "StyleBase", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#f4ebdd", 7 | "theme_color": "#f4ebdd", 8 | "icons": [ 9 | { "src": "/192.png", "type": "image/png", "sizes": "192x192" }, 10 | { "src": "/512.png", "type": "image/png", "sizes": "512x512" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /client/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | https://stylebase.cc/ 8 | monthly 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | 52 | -------------------------------------------------------------------------------- /client/src/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 90 | -------------------------------------------------------------------------------- /client/src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 29 | 30 | 67 | -------------------------------------------------------------------------------- /client/src/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 91 | 92 | 222 | -------------------------------------------------------------------------------- /client/src/components/CloseButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 52 | -------------------------------------------------------------------------------- /client/src/components/StyleEditor.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 95 | 96 | 147 | -------------------------------------------------------------------------------- /client/src/components/TopicCloud.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | 54 | -------------------------------------------------------------------------------- /client/src/components/cards/BaseCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 58 | -------------------------------------------------------------------------------- /client/src/components/cards/StyleCard.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 74 | 75 | 140 | -------------------------------------------------------------------------------- /client/src/components/cards/StyleCardSkeleton.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | 34 | 96 | -------------------------------------------------------------------------------- /client/src/components/dialogs/AddStyleDialog.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 125 | 126 | 188 | -------------------------------------------------------------------------------- /client/src/components/dialogs/BaseDialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 68 | 69 | 113 | -------------------------------------------------------------------------------- /client/src/components/dialogs/ConfirmationDialog.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | 49 | -------------------------------------------------------------------------------- /client/src/components/dialogs/HowToUseDialog.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 78 | 79 | 120 | -------------------------------------------------------------------------------- /client/src/components/dialogs/LoginDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | 35 | 58 | -------------------------------------------------------------------------------- /client/src/components/dialogs/PrivacyPolicyDialog.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 69 | 70 | 91 | -------------------------------------------------------------------------------- /client/src/components/dialogs/StyleInfoDialog.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 214 | 215 | 425 | -------------------------------------------------------------------------------- /client/src/images/codeberg-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/images/codeberg-logo.png -------------------------------------------------------------------------------- /client/src/images/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/images/github-logo.png -------------------------------------------------------------------------------- /client/src/images/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/images/no-image.png -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import VueGtag from 'vue-gtag'; 4 | import VueRouter from 'vue-router'; 5 | 6 | import store from './store'; 7 | import router from './router'; 8 | 9 | Vue.config.productionTip = false; 10 | 11 | Vue.use(VueGtag, { 12 | config: { id: process.env.VUE_APP_GTAG_ID, deferScriptLoad: true } 13 | }); 14 | 15 | Vue.use(VueRouter); 16 | 17 | new Vue({ 18 | store, 19 | router, 20 | render: (h) => h(App) 21 | }).$mount('#app'); 22 | -------------------------------------------------------------------------------- /client/src/mixins.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import relativeTime from 'dayjs/plugin/relativeTime'; 3 | import { emojify } from 'node-emoji'; 4 | 5 | import emojiFallback from '@/githubEmoji.json'; 6 | 7 | dayjs.extend(relativeTime); 8 | 9 | export default { 10 | methods: { 11 | compressImage(url) { 12 | const imgUrl = new URL('https://images.weserv.nl'); 13 | imgUrl.search = new URLSearchParams({ url, w: 300, h: 300, q: 60, n: -1 }); 14 | return imgUrl; 15 | }, 16 | pluralize(num, noun, suffix = 's') { 17 | return `${num} ${noun}${num === 1 ? '' : suffix}`; 18 | }, 19 | dateFromNow(date) { 20 | return dayjs(date).fromNow(); 21 | }, 22 | parseEmoji(text) { 23 | const onMissing = (name) => { 24 | const url = emojiFallback[name]; 25 | if (!url) return ''; 26 | return `${name}`; 27 | }; 28 | return emojify(text, onMissing); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /client/src/router.js: -------------------------------------------------------------------------------- 1 | import VueRouter from 'vue-router'; 2 | import Home from '@/views/Home.vue'; 3 | import Profile from '@/views/Profile.vue'; 4 | import HowToUseDialog from '@/components/dialogs/HowToUseDialog.vue'; 5 | import AddStyleDialog from '@/components/dialogs/AddStyleDialog.vue'; 6 | import LoginDialog from '@/components/dialogs/LoginDialog.vue'; 7 | import PrivacyPolicyDialog from '@/components/dialogs/PrivacyPolicyDialog.vue'; 8 | import store from '@/store'; 9 | 10 | const routes = [ 11 | { 12 | path: '/profile', 13 | name: 'Profile', 14 | component: Profile 15 | }, 16 | { 17 | path: '/', 18 | name: 'Home', 19 | component: Home, 20 | children: [ 21 | { path: '/style/:styleId', name: 'StyleModal' }, 22 | { path: '/search/:query', name: 'Search' }, 23 | { path: '/user/:username', name: 'UserFilter' }, 24 | { path: '/how-to-use', name: 'HowToUseDialog', component: HowToUseDialog }, 25 | { path: '/add-style', name: 'AddStyleDialog', component: AddStyleDialog }, 26 | { path: '/login', name: 'LoginDialog', component: LoginDialog }, 27 | { path: '/privacy-policy', name: 'PrivacyPolicyDialog', component: PrivacyPolicyDialog } 28 | ], 29 | beforeEnter: fetchData 30 | }, 31 | { 32 | path: '*', 33 | redirect: { name: 'Home' } 34 | } 35 | ]; 36 | 37 | const router = new VueRouter({ 38 | mode: 'history', 39 | base: process.env.BASE_URL, 40 | routes 41 | }); 42 | 43 | function fetchData(to, _from, next) { 44 | const { query, username, styleId } = to.params; 45 | if (query) { 46 | store.dispatch('styleGrid/setQuery', query, { root: true }); 47 | return next(); 48 | } 49 | if (username) { 50 | store.dispatch('styleGrid/setOwnerFilter', username, { root: true }); 51 | return next(); 52 | } 53 | if (styleId) store.dispatch('styleGrid/getStyle', { styleId }, { root: true }); 54 | store.dispatch('styleGrid/resetFilters', null, { root: true }); 55 | next(); 56 | } 57 | 58 | export default router; 59 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import styleGrid from './modules/styleGrid'; 5 | import user from './modules/user'; 6 | import alert from './modules/alert'; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | styleGrid, 13 | user, 14 | alert 15 | }, 16 | strict: process.env.NODE_ENV === 'development' 17 | }); 18 | -------------------------------------------------------------------------------- /client/src/store/modules/alert/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | flashAlert({ commit }, payload) { 3 | const id = Math.random().toString(36).substr(2, 9); 4 | commit('ADD_ALERT', { ...payload, id }); 5 | setTimeout(() => { 6 | commit('REMOVE_ALERT', id); 7 | }, 10000); 8 | }, 9 | close({ commit }, id) { 10 | commit('REMOVE_ALERT', id); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/store/modules/alert/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getAlerts: (state) => state.alerts 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/store/modules/alert/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions'; 2 | import getters from './getters'; 3 | import mutations from './mutations'; 4 | import state from './state'; 5 | 6 | export default { 7 | namespaced: true, 8 | actions, 9 | getters, 10 | mutations, 11 | state 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/store/modules/alert/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ADD_ALERT(state, payload) { 3 | state.alerts.push(payload); 4 | }, 5 | REMOVE_ALERT(state, id) { 6 | state.alerts = state.alerts.filter((alert) => alert.id !== id); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/src/store/modules/alert/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | alerts: [] 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/store/modules/styleGrid/actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | function setPageData({ selectedStyle: style, ownerFilter, searchQuery }) { 4 | let title = 'Collection of UserCSS styles | StyleBase'; 5 | let description = 'Website styles from various authors. Find and share your UserCSS style at StyleBase.cc'; 6 | let url = '/'; 7 | 8 | if (style.styleId) { 9 | title = `${style.customName || style.name} by ${style.owner.login} | StyleBase`; 10 | description = style.customDescription || style.description; 11 | url = `/style/${style.styleId}`; 12 | } else if (ownerFilter) { 13 | title = `Styles by ${ownerFilter} | StyleBase`; 14 | url = `/user/${ownerFilter}`; 15 | } else if (searchQuery) { 16 | title = `${searchQuery} | StyleBase`; 17 | url = `/search/${searchQuery}`; 18 | } 19 | 20 | document.title = title; 21 | document.head.querySelector('meta[name=description]').content = description; 22 | window.history.replaceState({}, title, url); 23 | } 24 | 25 | export default { 26 | getStyles({ state, commit, dispatch }) { 27 | setPageData(state); 28 | commit('SET_LOADING', true); 29 | 30 | const params = {}; 31 | params.page = state.pagination.page; 32 | params.sort = state.sortOrder; 33 | 34 | let url = '/api/styles/'; 35 | if (state.ownerFilter) url += state.ownerFilter; 36 | if (state.searchQuery) params.query = state.searchQuery; 37 | if (state.pagination.page === 1) commit('SET_STYLES', []); 38 | 39 | axios 40 | .get(url, { params }) 41 | .then((response) => { 42 | const styles = state.styles.concat(response.data.styles); 43 | commit('SET_STYLES', styles); 44 | commit('SET_PAGINATION', { 45 | page: response.data.page, 46 | hasNextPage: response.data.hasNextPage 47 | }); 48 | }) 49 | .catch((error) => { 50 | dispatch('alert/flashAlert', { type: 'error', message: error.response.data.error }, { root: true }); 51 | }) 52 | .finally(() => { 53 | commit('SET_LOADING', false); 54 | }); 55 | }, 56 | getStyle({ dispatch }, { styleId }) { 57 | const params = { styleId }; 58 | axios 59 | .get('/api/style', { params }) 60 | .then((response) => { 61 | dispatch('openStyleModal', response.data.style); 62 | }) 63 | .catch((error) => { 64 | dispatch('alert/flashAlert', { type: 'error', message: error.response.data.error }, { root: true }); 65 | }); 66 | }, 67 | editStyle({ state, dispatch }, payload) { 68 | return axios 69 | .patch('/api/style/edit', payload) 70 | .then((response) => { 71 | const name = response.data.style.customName || response.data.style.name; 72 | dispatch('alert/flashAlert', { type: 'success', message: `"${name}" style updated` }, { root: true }); 73 | if (state.showStyleInfoModal) dispatch('getStyle', response.data.style); 74 | }) 75 | .catch((error) => { 76 | dispatch('alert/flashAlert', { type: 'error', message: error.response.data.error }, { root: true }); 77 | }); 78 | }, 79 | deleteStyle({ state, dispatch }, _id) { 80 | return axios 81 | .delete('/api/style/delete', { data: { _id } }) 82 | .then((response) => { 83 | const name = response.data.style.customName || response.data.style.name; 84 | dispatch('alert/flashAlert', { type: 'success', message: `"${name}" style deleted` }, { root: true }); 85 | if (state.showStyleInfoModal) dispatch('closeStyleModal'); 86 | }) 87 | .catch((error) => { 88 | dispatch('alert/flashAlert', { type: 'error', message: error.response.data.error }, { root: true }); 89 | }); 90 | }, 91 | setPage({ commit, dispatch }, page) { 92 | commit('SET_PAGE', page); 93 | dispatch('getStyles'); 94 | }, 95 | setSortOrder({ commit, dispatch }, sortOption) { 96 | commit('SET_SORT_ORDER', sortOption); 97 | commit('SET_PAGE', 1); 98 | dispatch('getStyles'); 99 | }, 100 | setQuery({ state, commit, dispatch }, query) { 101 | commit('SET_SEARCH_QUERY', query); 102 | if (query.length && query.length < 3) return; 103 | 104 | commit('SET_PAGE', 1); 105 | dispatch('getStyles'); 106 | if (state.showStyleInfoModal) dispatch('closeStyleModal'); 107 | }, 108 | setOwnerFilter({ state, commit, dispatch }, filter) { 109 | if (state.showStyleInfoModal) dispatch('closeStyleModal'); 110 | if (filter) { 111 | commit('SET_OWNER_FILTER', filter); 112 | commit('SET_PAGE', 1); 113 | dispatch('getStyles'); 114 | } 115 | }, 116 | resetFilters({ commit, dispatch }) { 117 | commit('SET_SEARCH_QUERY', ''); 118 | commit('SET_SORT_ORDER', 'stargazers'); 119 | commit('SET_OWNER_FILTER', ''); 120 | commit('SET_PAGE', 1); 121 | dispatch('getStyles'); 122 | }, 123 | setStyleModalVisibility({ commit }, isActive) { 124 | commit('SET_MODAL_VISIBILITY', isActive); 125 | }, 126 | openStyleModal({ state, commit, dispatch }, style) { 127 | commit('SET_SELECTED_STYLE', style); 128 | dispatch('setStyleModalVisibility', true); 129 | setPageData(state); 130 | }, 131 | closeStyleModal({ state, commit, dispatch }) { 132 | commit('SET_SELECTED_STYLE', {}); 133 | dispatch('setStyleModalVisibility', false); 134 | setPageData(state); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /client/src/store/modules/styleGrid/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getState: (state) => state, 3 | getSelectedStyle: (state) => state.selectedStyle 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/store/modules/styleGrid/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions'; 2 | import getters from './getters'; 3 | import mutations from './mutations'; 4 | import state from './state'; 5 | 6 | export default { 7 | namespaced: true, 8 | actions, 9 | getters, 10 | mutations, 11 | state 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/store/modules/styleGrid/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_STYLES(state, styles) { 3 | state.styles = styles; 4 | }, 5 | SET_SELECTED_STYLE(state, style) { 6 | state.selectedStyle = style; 7 | }, 8 | SET_LOADING(state, loading) { 9 | state.isLoading = loading; 10 | }, 11 | SET_PAGINATION(state, pagination) { 12 | state.pagination = pagination; 13 | }, 14 | SET_PAGE(state, page) { 15 | state.pagination.page = page; 16 | }, 17 | SET_SEARCH_QUERY(state, query) { 18 | state.searchQuery = query; 19 | }, 20 | SET_OWNER_FILTER(state, filter) { 21 | state.ownerFilter = filter; 22 | }, 23 | SET_SORT_ORDER(state, orderId) { 24 | state.sortOrder = orderId; 25 | }, 26 | SET_MODAL_VISIBILITY(state, value) { 27 | state.showStyleInfoModal = value; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /client/src/store/modules/styleGrid/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | styles: [], 3 | isLoading: true, 4 | pagination: { 5 | page: 1, 6 | hasNextPage: false 7 | }, 8 | 9 | searchQuery: '', 10 | ownerFilter: '', 11 | 12 | sortOptions: [ 13 | { 14 | id: 'stargazers', 15 | text: 'Most liked' 16 | }, 17 | { 18 | id: 'lastUpdate', 19 | text: 'Recently updated' 20 | }, 21 | { 22 | id: '_id', 23 | text: 'Recently added' 24 | } 25 | ], 26 | sortOrder: 'stargazers', 27 | 28 | selectedStyle: {}, 29 | showStyleInfoModal: false 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default { 4 | getUser({ commit }) { 5 | axios 6 | .get('/api/me') 7 | .then(({ data }) => { 8 | if (!data.error) commit('SET_USER', data.user); 9 | }) 10 | .catch((error) => { 11 | console.error(error); 12 | }); 13 | }, 14 | getUserStyles({ state, dispatch, commit }) { 15 | if (!state.user) return; 16 | const params = { githubId: state.user.githubId, codebergId: state.user.codebergId }; 17 | return axios 18 | .get('/api/user/styles', { params }) 19 | .then(({ data }) => { 20 | if (data.error) { 21 | return dispatch('alert/flashAlert', { type: 'error', message: data.error }, { root: true }); 22 | } 23 | commit('SET_STYLES', data.styles); 24 | }) 25 | .catch(({ response }) => { 26 | dispatch('alert/flashAlert', { type: 'error', message: response.data.error }, { root: true }); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /client/src/store/modules/user/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getUser: (state) => state.user, 3 | getStyles: (state) => state.styles 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/store/modules/user/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions'; 2 | import getters from './getters'; 3 | import mutations from './mutations'; 4 | import state from './state'; 5 | 6 | export default { 7 | namespaced: true, 8 | actions, 9 | getters, 10 | mutations, 11 | state 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/store/modules/user/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_USER(state, user) { 3 | state.user = user; 4 | }, 5 | SET_STYLES(state, styles) { 6 | state.styles = styles; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/src/store/modules/user/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | user: null, 3 | styles: [] 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/styles/buttons.scss: -------------------------------------------------------------------------------- 1 | button, 2 | a { 3 | border-radius: 5px; 4 | text-decoration: none; 5 | color: var(--color-text); 6 | cursor: pointer; 7 | transition: background-color 0.2s, color 0.2s; 8 | 9 | &.disabled, 10 | &:disabled { 11 | opacity: 0.6; 12 | pointer-events: none; 13 | } 14 | } 15 | 16 | a:not(.button) { 17 | &:hover, 18 | &:focus { 19 | color: var(--color-main); 20 | } 21 | } 22 | 23 | a.button { 24 | display: inline-flex; 25 | justify-content: center; 26 | align-items: center; 27 | line-height: 1; 28 | } 29 | 30 | button.link { 31 | padding: 0; 32 | background: none; 33 | border: none; 34 | 35 | &:hover, 36 | &:focus { 37 | color: var(--color-main); 38 | } 39 | } 40 | 41 | .style-button, 42 | .style-button-filled, 43 | .style-button-danger { 44 | width: 170px; 45 | height: 50px; 46 | font-size: 18px; 47 | 48 | @include media-size-mobile { 49 | width: 120px; 50 | height: 45px; 51 | 52 | &.mobile-wide { 53 | width: 100%; 54 | } 55 | } 56 | 57 | .icon { 58 | width: 32px; 59 | padding-right: 0.5rem; 60 | } 61 | } 62 | 63 | .style-button { 64 | border: 1px solid var(--color-main); 65 | background-color: transparent; 66 | font-weight: bold; 67 | color: var(--color-main); 68 | 69 | &:hover { 70 | background-color: var(--color-main-lightest); 71 | } 72 | &:focus { 73 | background-color: var(--color-main-lighter); 74 | } 75 | &:active { 76 | background-color: var(--color-main-light); 77 | } 78 | } 79 | 80 | .style-button-filled { 81 | border: 1px solid var(--color-main); 82 | background-color: var(--color-main); 83 | font-weight: bold; 84 | color: #ffffff; 85 | 86 | &:hover { 87 | background-color: var(--color-main-dark); 88 | border-color: var(--color-main-dark); 89 | } 90 | &:focus { 91 | background-color: var(--color-main-darker); 92 | border-color: var(--color-main-darker); 93 | } 94 | &:active { 95 | background-color: var(--color-main-darkest); 96 | border-color: var(--color-main-darkest); 97 | } 98 | } 99 | 100 | .style-button-danger { 101 | border: 1px solid #dc3545; 102 | background-color: transparent; 103 | font-weight: bold; 104 | color: #dc3545; 105 | 106 | &:hover { 107 | background-color: var(--color-main-lightest); 108 | } 109 | &:focus { 110 | background-color: var(--color-main-lighter); 111 | } 112 | &:active { 113 | background-color: var(--color-main-light); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /client/src/styles/fonts/Gilroy-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/styles/fonts/Gilroy-Bold.woff -------------------------------------------------------------------------------- /client/src/styles/fonts/Gilroy-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/styles/fonts/Gilroy-Bold.woff2 -------------------------------------------------------------------------------- /client/src/styles/fonts/Gilroy-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/styles/fonts/Gilroy-Regular.woff -------------------------------------------------------------------------------- /client/src/styles/fonts/Gilroy-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/client/src/styles/fonts/Gilroy-Regular.woff2 -------------------------------------------------------------------------------- /client/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import '~normalize.css'; 2 | @import './buttons.scss'; 3 | @import './transitions.scss'; 4 | 5 | @font-face { 6 | font-family: Gilroy; 7 | font-weight: normal; 8 | font-style: normal; 9 | font-display: swap; 10 | src: url('~@/styles/fonts/Gilroy-Regular.woff2') format('woff2'), 11 | url('~@/styles/fonts/Gilroy-Regular.woff') format('woff'); 12 | } 13 | @font-face { 14 | font-family: Gilroy; 15 | font-weight: bold; 16 | font-style: normal; 17 | font-display: swap; 18 | src: url('~@/styles/fonts/Gilroy-Bold.woff2') format('woff2'), url('~@/styles/fonts/Gilroy-Bold.woff') format('woff'); 19 | } 20 | 21 | :root { 22 | --color-bg: #faf6f0; 23 | --color-bg-light: #f5e6cc; 24 | --color-text-bg: #ffffff; 25 | --color-text: #47525e; 26 | --color-text-light: #98a7b8; 27 | --color-focus: #ffa500; 28 | --color-border: #e0e0e0; 29 | --color-main-lightest: #f2e8d9; 30 | --color-main-lighter: #ecddc6; 31 | --color-main-light: #e6d1b3; 32 | --color-main: #d6835c; 33 | --color-main-dark: #d17347; 34 | --color-main-darker: #cc6333; 35 | --color-main-darkest: #b85a2e; 36 | } 37 | 38 | [data-theme='dark'] { 39 | --color-bg: #191919; 40 | --color-bg-light: #272727; 41 | --color-text-bg: #222222; 42 | --color-text: #dddddd; 43 | --color-text-light: #949494; 44 | --color-border: #292929; 45 | --color-main-lightest: #333333; 46 | --color-main-lighter: #262626; 47 | --color-main-light: #1a1a1a; 48 | --color-main: #cc6333; 49 | --color-main-dark: #b85a2e; 50 | --color-main-darker: #a35029; 51 | --color-main-darkest: #8f4624; 52 | .invert { 53 | filter: invert(0.8); 54 | } 55 | } 56 | 57 | html, 58 | body, 59 | #app { 60 | min-height: 100vh; 61 | min-width: 320px; 62 | background-color: var(--color-bg); 63 | } 64 | 65 | body.no-scroll { 66 | overflow: hidden; 67 | } 68 | 69 | #app, 70 | ::placeholder { 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | font-family: Gilroy, sans-serif; 74 | font-size: 16px; 75 | color: var(--color-text); 76 | } 77 | 78 | ::placeholder { 79 | color: var(--color-text-light); 80 | } 81 | 82 | body { 83 | margin: 0; 84 | } 85 | 86 | #app { 87 | display: flex; 88 | flex-direction: column; 89 | } 90 | 91 | ul { 92 | padding: 0; 93 | } 94 | 95 | input { 96 | border: 2px solid var(--color-border); 97 | border-radius: 5px; 98 | background-color: var(--color-text-bg); 99 | color: var(--color-text); 100 | outline: 0; 101 | 102 | &:focus { 103 | border-color: var(--color-focus); 104 | } 105 | } 106 | 107 | .container { 108 | padding-right: 1rem; 109 | padding-left: 1rem; 110 | margin-right: auto; 111 | margin-left: auto; 112 | } 113 | @media (min-width: 576px) { 114 | .container { 115 | max-width: 540px; 116 | } 117 | } 118 | 119 | @media (min-width: 768px) { 120 | .container { 121 | max-width: 720px; 122 | } 123 | } 124 | 125 | @media (min-width: 992px) { 126 | .container { 127 | max-width: 960px; 128 | } 129 | } 130 | 131 | @media (min-width: 1200px) { 132 | .container { 133 | max-width: 1140px; 134 | } 135 | } 136 | 137 | @media (min-width: 1400px) { 138 | .container { 139 | max-width: 1320px; 140 | } 141 | } 142 | 143 | // Hidden element for screen-reader 144 | .visually-hidden { 145 | position: absolute; 146 | width: 1px; 147 | height: 1px; 148 | overflow: hidden; 149 | clip: rect(0 0 0 0); 150 | } 151 | 152 | .emoji { 153 | width: 1.5rem; 154 | vertical-align: -0.075em; 155 | } 156 | -------------------------------------------------------------------------------- /client/src/styles/mixins/media.scss: -------------------------------------------------------------------------------- 1 | @mixin media-size-tablet { 2 | @media (max-width: 768px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin media-size-mobile { 8 | @media (max-width: 425px) { 9 | @content; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/styles/transitions.scss: -------------------------------------------------------------------------------- 1 | .transition-dialog { 2 | &-enter-active { 3 | transition: opacity 0.2s ease-in-out; 4 | } 5 | &-leave-active { 6 | transition: opacity 0.2s ease-in; 7 | } 8 | &-enter { 9 | opacity: 0; 10 | } 11 | &-enter-to { 12 | opacity: 1; 13 | } 14 | &-leave-to { 15 | opacity: 0; 16 | .base-dialog { 17 | transition: transform 0.2s ease-in; 18 | transform: translateY(-50vh); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 139 | 140 | 249 | -------------------------------------------------------------------------------- /client/src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 76 | 77 | 140 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | css: { 5 | sourceMap: process.env.NODE_ENV === 'development', 6 | loaderOptions: { 7 | sass: { 8 | additionalData: `@import "~@/styles/mixins/media";` 9 | } 10 | } 11 | }, 12 | 13 | chainWebpack: (config) => { 14 | config.module 15 | .rule('vue') 16 | .use('vue-loader') 17 | .tap((args) => { 18 | args.compilerOptions.whitespace = 'preserve'; 19 | }); 20 | }, 21 | 22 | devServer: { 23 | proxy: { 24 | '^/api': { 25 | target: `${process.env.VUE_APP_API_URL}:${process.env.VUE_APP_API_PORT}` 26 | }, 27 | '^/login': { 28 | target: `${process.env.VUE_APP_API_URL}:${process.env.VUE_APP_API_PORT}` 29 | }, 30 | '^/logout': { 31 | target: `${process.env.VUE_APP_API_URL}:${process.env.VUE_APP_API_PORT}` 32 | }, 33 | '^/rss': { 34 | target: `${process.env.VUE_APP_API_URL}:${process.env.VUE_APP_API_PORT}` 35 | } 36 | } 37 | }, 38 | 39 | outputDir: path.resolve(__dirname, '../public') 40 | }; 41 | -------------------------------------------------------------------------------- /config.template.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import type { Config } from "./types/server"; 3 | 4 | export default { 5 | appPort: "7540", 6 | mongoUrl: "mongodb://localhost/stylebase", 7 | session: { 8 | secret: "SECRET", 9 | name: "stylebase.sid" 10 | }, 11 | github: { 12 | OAuth: { 13 | clientId: "", 14 | clientSecret: "" 15 | }, 16 | token: "" 17 | }, 18 | codeberg: { 19 | OAuth: { 20 | clientId: "", 21 | clientSecret: "" 22 | }, 23 | token: "" 24 | } 25 | } as Config; 26 | -------------------------------------------------------------------------------- /maintenance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maintenance | StyleBase 8 | 9 | 70 | 71 | 72 | 73 |
74 |
75 |

The site is down for maintenance

76 |

Sorry for the inconvenience, we'll be back shortly!

77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /meta/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VChet/StyleBase/4f8155e30285049daab39182982776b974677765/meta/preview.png -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import MongoStore from "connect-mongo"; 3 | import session from "express-session"; 4 | import { Agenda } from "agenda"; 5 | 6 | import passport from "passport"; 7 | import { Strategy as GitHubStrategy } from "passport-github2"; 8 | 9 | import morgan from "morgan"; 10 | import helmet from "helmet"; 11 | import { json, urlencoded } from "express"; 12 | import compression from "compression"; 13 | 14 | import type { Application } from "express"; 15 | import type { IUser } from "./models/User"; 16 | 17 | import config from "./config"; 18 | import { User } from "./models/User"; 19 | import { Style } from "./models/Style"; 20 | import initCollection from "./models/init"; 21 | 22 | // Database 23 | mongoose.connect(config.mongoUrl, { 24 | useCreateIndex: true, 25 | useFindAndModify: false, 26 | useNewUrlParser: true, 27 | useUnifiedTopology: true 28 | }); 29 | mongoose.connection.on("error", console.error.bind(console, "MongoDB connection error:")); 30 | initCollection(); 31 | 32 | // Authentication 33 | passport.use( 34 | new GitHubStrategy({ 35 | clientID: config.github.OAuth.clientId, 36 | clientSecret: config.github.OAuth.clientSecret, 37 | callbackURL: "/github/callback", 38 | proxy: process.env.NODE_ENV === "production", 39 | passReqToCallback: true 40 | }, User.findOrCreate.bind(User, "GitHub")) 41 | ); 42 | passport.use( 43 | "codeberg", 44 | new GitHubStrategy({ 45 | clientID: config.codeberg.OAuth.clientId, 46 | clientSecret: config.codeberg.OAuth.clientSecret, 47 | callbackURL: "/codeberg/callback", 48 | proxy: process.env.NODE_ENV === "production", 49 | authorizationURL: "https://codeberg.org/login/oauth/authorize", 50 | tokenURL: "https://codeberg.org/login/oauth/access_token", 51 | userProfileURL: "https://codeberg.org/api/v1/user", 52 | passReqToCallback: true 53 | }, User.findOrCreate.bind(User, "Codeberg")) 54 | ); 55 | 56 | passport.serializeUser((user: IUser, done) => { 57 | done(null, user._id); 58 | }); 59 | 60 | passport.deserializeUser((id, done) => { 61 | User.findById(id).lean().exec((error: mongoose.CallbackError, user) => { 62 | if (error) return done(error); 63 | if (user) return done(null, user as Express.User); 64 | }); 65 | }); 66 | 67 | // Agenda 68 | const agenda = new Agenda({ db: { address: config.mongoUrl } }); 69 | 70 | if (process.env.NODE_ENV === "production") { 71 | agenda.define("Update all styles", async () => { 72 | const result = await Style.updateAllStyles(); 73 | console.log(`Update successful: ${!!result.ok}. Updated ${result.nModified} of ${result.nMatched}`); 74 | }); 75 | agenda.start().then(() => agenda.every("0 * * * *", "Update all styles")); 76 | } 77 | 78 | // Middleware 79 | export default function addExpressMiddleware(app: Application) { 80 | if (process.env.NODE_ENV === "production") app.enable("trust proxy"); 81 | app.use(morgan("dev")); 82 | app.use(helmet({ 83 | referrerPolicy: { policy: "strict-origin-when-cross-origin" }, 84 | contentSecurityPolicy: { 85 | directives: { 86 | "default-src": ["'self'"], 87 | "img-src": ["'self'", "data:", "https:"], 88 | "style-src": ["'self'", "'unsafe-inline'"], 89 | "script-src": [ 90 | "'self'", "data:", 91 | "https://www.googletagmanager.com", "https://www.google-analytics.com", 92 | "https://cloudflareinsights.com", "https://static.cloudflareinsights.com" 93 | ], 94 | "connect-src": [ 95 | "'self'", 96 | "https://www.google-analytics.com", 97 | "https://cloudflareinsights.com", "https://static.cloudflareinsights.com" 98 | ] 99 | } 100 | } 101 | })); 102 | app.use(json()); 103 | app.use(urlencoded({ extended: true })); 104 | app.use(session({ 105 | store: MongoStore.create({ mongoUrl: config.mongoUrl }), 106 | proxy: process.env.NODE_ENV === "production", 107 | resave: true, 108 | saveUninitialized: false, 109 | cookie: { 110 | maxAge: 3600000 * 24 * 14 111 | }, 112 | rolling: true, 113 | ...config.session 114 | })); 115 | app.use(passport.initialize()); 116 | app.use(passport.session()); 117 | app.use(compression()); 118 | } 119 | -------------------------------------------------------------------------------- /models/Style.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import { customAlphabet } from "nanoid"; 4 | 5 | import type { Document, PaginateModel } from "mongoose"; 6 | 7 | import { retrieveRepositoryData } from "../api/parser"; 8 | 9 | export interface IStyle extends Document { 10 | url: string 11 | usercss: string 12 | preview?: string 13 | name: string 14 | description?: string 15 | owner: { 16 | id: number 17 | login: string 18 | } 19 | created: string 20 | lastUpdate: string 21 | topics: Array 22 | stargazers: number 23 | watchers: number 24 | forks: number 25 | issues: number 26 | license?: string 27 | isPrivate: boolean 28 | isArchived: boolean 29 | isFork: boolean 30 | parent?: { 31 | name: string, 32 | url: string 33 | }, 34 | styleId: string 35 | customName?: string 36 | customDescription?: string 37 | customPreview?: string 38 | } 39 | 40 | export interface IStyleModel extends PaginateModel { 41 | updateAllStyles: () => Promise; 42 | } 43 | 44 | const styleIdAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 45 | 46 | const StyleSchema: Schema = new Schema({ 47 | url: { 48 | type: String, 49 | required: true 50 | }, 51 | usercss: { 52 | type: String, 53 | unique: true 54 | }, 55 | preview: String, 56 | name: String, 57 | description: String, 58 | owner: { 59 | type: { 60 | id: Number, 61 | login: String 62 | } 63 | }, 64 | created: String, 65 | lastUpdate: String, 66 | topics: Array, 67 | stargazers: Number, 68 | watchers: Number, 69 | forks: Number, 70 | issues: Number, 71 | license: String, 72 | isPrivate: Boolean, 73 | isArchived: Boolean, 74 | isFork: Boolean, 75 | parent: { 76 | name: String, 77 | url: String 78 | }, 79 | styleId: { 80 | type: String, 81 | default: () => customAlphabet(styleIdAlphabet, 11)() 82 | }, 83 | customName: String, 84 | customDescription: String, 85 | customPreview: String 86 | }); 87 | 88 | StyleSchema.index({ 89 | name: "text", 90 | customName: "text", 91 | "owner.login": "text", 92 | topics: "text" 93 | }); 94 | StyleSchema.plugin(mongoosePaginate); 95 | 96 | StyleSchema.statics.updateAllStyles = async function (): Promise { 97 | const Style = this; 98 | const styles: Array = await Style.find({}).lean(); 99 | const stylesData = await Promise.allSettled( 100 | styles.map(({ url, usercss }) => retrieveRepositoryData(url, { download_url: usercss })) 101 | ); 102 | 103 | const Bulk = Style.collection.initializeUnorderedBulkOp(); 104 | stylesData.forEach((promise) => { 105 | if (promise.status === "fulfilled") { 106 | const styleData = promise.value; 107 | Bulk.find({ usercss: styleData.usercss }).update({ $set: styleData }); 108 | } else { 109 | console.log(promise.reason.message); 110 | } 111 | }); 112 | return Bulk.execute(); 113 | }; 114 | 115 | export const Style = model("Style", StyleSchema); 116 | -------------------------------------------------------------------------------- /models/User.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import axios from "axios"; 3 | 4 | import type { Document, Model, CallbackError } from "mongoose"; 5 | import type { Profile } from "passport"; 6 | import type { Request } from "express"; 7 | import type { Provider, ProviderName } from "../types/server"; 8 | import type { CodebergOrganization, GitHubOrganization } from "../types/api"; 9 | 10 | import providers from "../api/providers"; 11 | 12 | type Role = "User" | "Admin"; 13 | 14 | export interface Organization { 15 | id: number; 16 | login: string; 17 | } 18 | 19 | export interface IUser extends Document { 20 | username: string; 21 | role: Role; 22 | githubId?: number; 23 | codebergId?: number; 24 | orgs: Array; 25 | } 26 | 27 | export interface IUserModel extends Model { 28 | findOrCreate: ( 29 | providerName: ProviderName, 30 | req: Request, 31 | _accessToken: string, 32 | _refreshToken: string, 33 | profile: Profile, 34 | done: Function 35 | ) => void; 36 | } 37 | 38 | export const UserSchema: Schema = new Schema({ 39 | username: { 40 | type: String, 41 | trim: true 42 | }, 43 | role: { 44 | type: String, 45 | enum: ["Admin", "User"], 46 | default: "User" 47 | }, 48 | githubId: { 49 | type: Number, 50 | unique: true, 51 | sparse: true 52 | }, 53 | codebergId: { 54 | type: Number, 55 | unique: true, 56 | sparse: true 57 | }, 58 | orgs: { 59 | type: { 60 | id: Number, 61 | login: String 62 | }, 63 | default: [] 64 | } 65 | }); 66 | 67 | async function getOrganizations(provider: Provider, username: string): Promise> { 68 | const response = await axios.get(`${provider.api}/users/${username}/orgs`, provider.options); 69 | return response.data.map((org: GitHubOrganization & CodebergOrganization) => { 70 | if (provider.name === "GitHub") { 71 | return { login: org.login, id: org.id }; 72 | } 73 | return { login: org.username, id: org.id }; 74 | }); 75 | } 76 | 77 | UserSchema.statics.findOrCreate = async function ( 78 | providerName: ProviderName, 79 | req: Request, 80 | _accessToken: string, 81 | _refreshToken: string, 82 | profile: Profile, 83 | done: Function 84 | ) { 85 | const User = this; 86 | 87 | const { id, username } = profile; 88 | if (!username) return done(new Error("Missing username")); 89 | 90 | const provider = providers.find((pr) => pr.name === providerName); 91 | if (!provider) return done(new Error(`Unsupported provider: ${providerName}`)); 92 | 93 | const userId: Pick = {}; 94 | userId[provider.idField] = parseInt(id, 10); 95 | 96 | const existingUser = await User.findOne(userId).lean(); 97 | if (!req.isAuthenticated()) { 98 | if (existingUser) return done(null, existingUser); 99 | 100 | const newUser = new User({ ...userId, username, orgs: await getOrganizations(provider, username) }); 101 | newUser.save((saveError: CallbackError) => done(saveError, newUser.toObject())); 102 | } else { 103 | if (existingUser) await User.findByIdAndDelete(existingUser); 104 | User.findByIdAndUpdate( 105 | req.user._id, 106 | { $set: { ...userId, orgs: await getOrganizations(provider, username) } }, 107 | { new: true }, 108 | (updateError: CallbackError, user) => done(updateError, user) 109 | ); 110 | } 111 | }; 112 | 113 | export const User = model("User", UserSchema); 114 | -------------------------------------------------------------------------------- /models/init.ts: -------------------------------------------------------------------------------- 1 | import { Style } from "./Style"; 2 | 3 | export default async function initCollection() { 4 | if (process.env.NODE_ENV === "test") return; 5 | 6 | const styles = await Style.find({}).lean(); 7 | if (styles && styles.length) return styles; 8 | 9 | const initialStyles: Array = [{ 10 | url: "https://github.com/StylishThemes/GitHub-Dark", 11 | usercss: "https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css" 12 | }, { 13 | url: "https://github.com/StylishThemes/StackOverflow-Dark", 14 | usercss: "https://raw.githubusercontent.com/StylishThemes/StackOverflow-Dark/master/stackoverflow-dark.user.css" 15 | }]; 16 | 17 | await Style.insertMany(initialStyles); 18 | console.log(await Style.updateAllStyles()); 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylebase", 3 | "version": "4.6.2", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/VChet/StyleBase.git" 7 | }, 8 | "license": "MIT", 9 | "author": "VChet", 10 | "contributors": [ 11 | "rudnovd" 12 | ], 13 | "scripts": { 14 | "start": "ts-node server.ts", 15 | "server": "nodemon server.ts", 16 | "lint": "eslint --ext ts .", 17 | "fix": "npm run lint -- --fix", 18 | "client:start": "cd client/ && npm start", 19 | "client:build": "cd client/ && npm run build" 20 | }, 21 | "dependencies": { 22 | "@types/compression": "^1.7.1", 23 | "@types/express": "^4.17.13", 24 | "@types/express-rate-limit": "^5.1.3", 25 | "@types/express-session": "^1.17.4", 26 | "@types/memory-cache": "^0.2.1", 27 | "@types/mongoose-paginate-v2": "^1.3.11", 28 | "@types/morgan": "^1.9.3", 29 | "@types/node": "^16.7.1", 30 | "@types/passport-github2": "^1.2.5", 31 | "agenda": "^4.2.1", 32 | "axios": "^0.21.1", 33 | "compression": "^1.7.4", 34 | "connect-mongo": "^4.5.0", 35 | "express": "^4.17.1", 36 | "express-rate-limit": "^5.3.0", 37 | "express-session": "^1.17.2", 38 | "feed": "^4.2.2", 39 | "helmet": "^4.6.0", 40 | "memory-cache": "^0.2.0", 41 | "mongoose": "^5.13.8", 42 | "mongoose-paginate-v2": "^1.4.2", 43 | "morgan": "^1.10.0", 44 | "nanoid": "^3.1.25", 45 | "passport": "^0.4.1", 46 | "passport-github2": "^0.1.12", 47 | "repo-images": "^1.0.1", 48 | "ts-node": "^10.2.1", 49 | "typescript": "^4.3.5", 50 | "usercss-meta": "^0.12.0" 51 | }, 52 | "devDependencies": { 53 | "@typescript-eslint/eslint-plugin": "^4.29.3", 54 | "@typescript-eslint/parser": "^4.29.3", 55 | "eslint": "^7.32.0", 56 | "eslint-config-airbnb-base": "^14.2.1", 57 | "eslint-config-airbnb-typescript": "^14.0.0", 58 | "eslint-plugin-import": "^2.24.1", 59 | "eslint-plugin-sonarjs": "^0.10.0", 60 | "nodemon": "^2.0.12" 61 | }, 62 | "engines": { 63 | "node": "^14.16.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import path from "path"; 3 | import passport from "passport"; 4 | 5 | import getRss from "./api/rss"; 6 | 7 | const router = Router(); 8 | const clientIndex = path.join(__dirname, "public/index.html"); 9 | const maintenance = path.join(__dirname, "maintenance.html"); 10 | 11 | // Authentication 12 | router.get("/login/github", passport.authenticate("github")); 13 | router.get("/login/codeberg", passport.authenticate("codeberg")); 14 | router.get("/logout", (req, res) => { 15 | req.logout(); 16 | res.redirect("/"); 17 | }); 18 | router.get("/github/callback", passport.authenticate("github"), (_req, res) => res.redirect("back")); 19 | router.get("/codeberg/callback", passport.authenticate("codeberg"), (_req, res) => res.redirect("back")); 20 | 21 | // RSS 22 | router.get("/rss", getRss); 23 | 24 | // Client serving 25 | router.get("*", (_req, res) => res.sendFile(clientIndex, (error: NodeJS.ErrnoException) => { 26 | if (error && error.code === "ENOENT") return res.status(503).sendFile(maintenance); 27 | })); 28 | 29 | export default router; 30 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import type { Application } from "express"; 4 | 5 | import config from "./config"; 6 | import addExpressMiddleware from "./middleware"; 7 | import routesApi from "./api/routes-api"; 8 | import routes from "./routes"; 9 | 10 | const app: Application = express(); 11 | const PORT = process.env.PORT || config.appPort; 12 | 13 | addExpressMiddleware(app); 14 | 15 | app.use(express.static("public")); 16 | app.use("/api", routesApi); 17 | app.use(routes); 18 | 19 | app.set("port", PORT); 20 | app.listen(app.get("port"), () => { 21 | console.log(`Server is up and running on port ${PORT}`); 22 | }); 23 | 24 | module.exports = app; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2020.Promise"], 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "allowJs": false, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "experimentalDecorators": true, 13 | "typeRoots": ["./types/"], 14 | "types": [ 15 | "./types/", 16 | "@types/compression", 17 | "@types/express-rate-limit", 18 | "@types/express-session", 19 | "@types/express", 20 | "@types/memory-cache", 21 | "@types/mongoose-paginate-v2", 22 | "@types/morgan", 23 | "@types/node", 24 | "@types/passport-github2" 25 | ] 26 | }, 27 | "include": ["**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /types/api/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface License { 2 | key: string; 3 | name: string; 4 | spdx_id: string; 5 | url: string; 6 | node_id: string; 7 | } 8 | 9 | type FileType = "file" | "dir" | "symlink" | "submodule"; 10 | 11 | export interface File { 12 | name: string; 13 | path: string; 14 | sha: string; 15 | size: number; 16 | url: string; 17 | html_url: string; 18 | git_url: string; 19 | download_url: string; 20 | type: FileType; 21 | _links: { 22 | self: string; 23 | git: string; 24 | html: string; 25 | } 26 | } 27 | 28 | // GitHub 29 | export interface GitHubUser { 30 | id: number; 31 | login: string; 32 | node_id: string; 33 | avatar_url: string; 34 | gravatar_id: string; 35 | url: string; 36 | html_url: string; 37 | followers_url: string; 38 | following_url: string; 39 | gists_url: string; 40 | starred_url: string; 41 | subscriptions_url: string; 42 | organizations_url: string; 43 | repos_url: string; 44 | events_url: string; 45 | received_events_url: string; 46 | type: string; 47 | site_admin: boolean; 48 | } 49 | 50 | export interface GitHubRepository { 51 | id: number; 52 | node_id: string; 53 | name: string; 54 | full_name: string; 55 | private: boolean; 56 | owner: GitHubUser; 57 | html_url: string; 58 | description: string; 59 | fork: boolean; 60 | url: string; 61 | forks_url: string; 62 | keys_url: string; 63 | collaborators_url: string; 64 | teams_url: string; 65 | hooks_url: string; 66 | issue_events_url: string; 67 | events_url: string; 68 | assignees_url: string; 69 | branches_url: string; 70 | tags_url: string; 71 | blobs_url: string; 72 | git_tags_url: string; 73 | git_refs_url: string; 74 | trees_url: string; 75 | statuses_url: string; 76 | languages_url: string; 77 | stargazers_url: string; 78 | contributors_url: string; 79 | subscribers_url: string; 80 | subscription_url: string; 81 | commits_url: string; 82 | git_commits_url: string; 83 | comments_url: string; 84 | issue_comment_url: string; 85 | contents_url: string; 86 | compare_url: string; 87 | merges_url: string; 88 | archive_url: string; 89 | downloads_url: string; 90 | issues_url: string; 91 | pulls_url: string; 92 | milestones_url: string; 93 | notifications_url: string; 94 | labels_url: string; 95 | releases_url: string; 96 | deployments_url: string; 97 | created_at: string; 98 | updated_at: string; 99 | pushed_at: string; 100 | git_url: string; 101 | ssh_url: string; 102 | clone_url: string; 103 | svn_url: string; 104 | homepage: string; 105 | size: number; 106 | stargazers_count: number; 107 | watchers_count: number; 108 | language: string; 109 | has_issues: boolean; 110 | has_projects: boolean; 111 | has_downloads: boolean; 112 | has_wiki: boolean; 113 | has_pages: boolean; 114 | forks_count: number; 115 | archived: boolean; 116 | disabled: boolean; 117 | open_issues_count: number; 118 | license: License; 119 | topics: Array; 120 | forks: number; 121 | open_issues: number; 122 | watchers: number; 123 | default_branch: string; 124 | network_count: number; 125 | subscribers_count: number; 126 | } 127 | 128 | export interface GitHubOrganization { 129 | login: string; 130 | id: number; 131 | node_id: string; 132 | url: string; 133 | repos_url: string; 134 | events_url: string; 135 | hooks_url: string; 136 | issues_url: string; 137 | members_url: string; 138 | public_members_url: string; 139 | avatar_url: string; 140 | description: string; 141 | } 142 | 143 | // Codeberg 144 | export interface CodebergUser { 145 | id: number; 146 | login: string; 147 | full_name: string; 148 | email: string; 149 | avatar_url: string; 150 | language: string; 151 | is_admin: boolean; 152 | last_login: string; 153 | created: string; 154 | username: string; 155 | } 156 | 157 | export interface CodebergRepository { 158 | id: number; 159 | owner: CodebergUser; 160 | name: string; 161 | full_name: string; 162 | description: string; 163 | empty: boolean; 164 | private: boolean; 165 | fork: boolean; 166 | template: boolean; 167 | mirror: boolean; 168 | size: number; 169 | html_url: string; 170 | ssh_url: string; 171 | clone_url: string; 172 | original_url: string; 173 | website: string; 174 | stars_count: number; 175 | forks_count: number; 176 | watchers_count: number; 177 | open_issues_count: number; 178 | open_pr_counter: number; 179 | release_counter: number; 180 | default_branch: string; 181 | archived: boolean; 182 | created_at: string; 183 | updated_at: string; 184 | has_issues: boolean; 185 | has_wiki: boolean; 186 | has_pull_requests: boolean; 187 | has_projects: boolean; 188 | ignore_whitespace_conflicts: boolean; 189 | allow_merge_commits: boolean; 190 | allow_rebase: boolean; 191 | allow_rebase_explicit: boolean; 192 | allow_squash_merge: boolean; 193 | avatar_url: string; 194 | internal: boolean; 195 | } 196 | 197 | export interface CodebergOrganization { 198 | id: number; 199 | username: string; 200 | full_name: string; 201 | avatar_url: string; 202 | description: string; 203 | website: string; 204 | location: string; 205 | visibility: string; 206 | repo_admin_change_team_access: boolean; 207 | } 208 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "repo-images" { 2 | export interface repoOptions { 3 | token?: string; 4 | access_token?: string; 5 | branch?: string; 6 | } 7 | 8 | export interface Image { 9 | path: string; 10 | mode: string; 11 | type: string; 12 | sha: string; 13 | size: number; 14 | url: string; 15 | rawgit: string; 16 | } 17 | 18 | export default function (repo: string, opts: repoOptions, callback?: Function): Promise>; 19 | } 20 | 21 | declare module "usercss-meta" { 22 | /** Options {@link https://github.com/openstyles/usercss-meta#createparser|documentation} */ 23 | interface ParseOptions { 24 | unknownKey?: string; 25 | mandatoryKeys?: Array; 26 | parseKey?: object; 27 | parseVar?: object; 28 | validateKey?: object; 29 | validateVar?: object; 30 | allowErrors?: boolean; 31 | } 32 | 33 | interface ParseError { 34 | name: string; 35 | code: string; 36 | message: string; 37 | index: number; 38 | } 39 | 40 | interface ParseResults { 41 | metadata: { 42 | /** 43 | * The name of UserCSS style. 44 | * The combination of @name and @namespace must be unique for each UserCSS. 45 | */ 46 | name: string; 47 | /** 48 | * The namespace of the UserCSS. 49 | * Helps to distinguish between styles with the same name. 50 | * Usually, the author's nickname or homepage. Can contain spaces and special characters. 51 | */ 52 | namespace: string; 53 | /** 54 | * The version of the style. 55 | * This is used for the update check. 56 | * Follow {@link http://semver.org/|semver} versioning. 57 | */ 58 | version: string; 59 | /** A short significant description. */ 60 | description?: string; 61 | /** Info about the author of the UserCSS style. */ 62 | author?: string; 63 | /** 64 | * The project's homepage. 65 | * This is used in Stylus' Manage and Edit pages to link to UserCSS source. 66 | */ 67 | homepageURL?: string; 68 | /** The URL the user can report issues to the UserCSS author. */ 69 | supportURL?: string; 70 | /** 71 | * When defined, this URL is used when updating the style. 72 | * Otherwise the style is updated from where it was installed. 73 | */ 74 | updateURL?: string; 75 | /** 76 | * License based on the 77 | * {@link https://spdx.org/licenses|SPDX license identifier}. 78 | */ 79 | license?: string; 80 | /** An applied CSS preprocessor. */ 81 | preprocessor?: string; 82 | /** 83 | * A live-switchable variable which will be compiled to valid CSS 84 | * according to the preprocessor that is set. 85 | */ 86 | var?: string; 87 | }; 88 | errors: Array; 89 | } 90 | 91 | interface MetaParser { 92 | parse(text: string, options?: ParseOptions): ParseResults; 93 | } 94 | 95 | const metaParser: MetaParser; 96 | export default metaParser; 97 | } 98 | -------------------------------------------------------------------------------- /types/server/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Send } from "express"; 2 | import type { SessionOptions } from "express-session"; 3 | import type { IStyle } from "../../models/Style"; 4 | import type { IUser } from "../../models/User"; 5 | 6 | export interface Config { 7 | appPort: string; 8 | mongoUrl: string; 9 | session: SessionOptions; 10 | github: { 11 | /** 12 | * OAuth data, obtained from 13 | * {@link https://github.com/settings/developers|GitHub} 14 | */ 15 | OAuth: { 16 | clientId: string; 17 | clientSecret: string; 18 | } 19 | /** 20 | * User token with public_repo scope, obtained from 21 | * {@link https://github.com/settings/tokens|GitHub} 22 | */ 23 | token: string 24 | }, 25 | codeberg: { 26 | /** 27 | * OAuth data, obtained from 28 | * {@link https://codeberg.org/user/settings/applications/oauth2|Codeberg} 29 | */ 30 | OAuth: { 31 | clientId: string; 32 | clientSecret: string; 33 | } 34 | /** 35 | * User token with public_repo scope, obtained from 36 | * {@link https://codeberg.org/user/settings/applications|Codeberg} 37 | */ 38 | token: string; 39 | } 40 | } 41 | 42 | export type ProviderName = "GitHub" | "Codeberg"; 43 | 44 | export interface Provider { 45 | name: ProviderName; 46 | host: string; 47 | api: string; 48 | idField: "githubId" | "codebergId"; 49 | options: { 50 | headers?: { 51 | Authorization: string; 52 | Accept: string; 53 | } 54 | } 55 | } 56 | 57 | declare global { 58 | namespace Express { 59 | interface User extends IUser { } 60 | interface Request { 61 | styleData?: Partial 62 | } 63 | interface Response { 64 | sendResponse: Send 65 | } 66 | } 67 | } 68 | --------------------------------------------------------------------------------