├── .editorconfig ├── .gitignore ├── .idea └── httpRequests │ └── http-requests-log.http ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── .yarn └── releases │ └── yarn-3.3.1.cjs ├── .yarnrc.yml ├── Procfile ├── README.md ├── angular.json ├── app.json ├── apps ├── .gitkeep ├── websheep-api │ ├── jest-yaml-transformer.js │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── .gitkeep │ │ │ ├── authz1 │ │ │ │ ├── index.ts │ │ │ │ ├── pact.spec.ts │ │ │ │ ├── sheep.router.spec.ts │ │ │ │ └── sheep.router.ts │ │ │ ├── authz2 │ │ │ │ ├── farmers.router.spec.ts │ │ │ │ ├── farmers.router.ts │ │ │ │ ├── get-farmer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── patch-farmer.ts │ │ │ │ ├── serialize-farmer.ts │ │ │ │ └── sheep.router.ts │ │ │ ├── csrf1 │ │ │ │ ├── csrf1.spec.ts │ │ │ │ └── index.ts │ │ │ ├── csrf2 │ │ │ │ ├── csrf2.spec.ts │ │ │ │ └── index.ts │ │ │ ├── csrf3 │ │ │ │ └── index.ts │ │ │ ├── database.ts │ │ │ ├── jwt1 │ │ │ │ ├── index.ts │ │ │ │ └── jwt-auth.middleware.ts │ │ │ ├── jwt2 │ │ │ │ └── index.ts │ │ │ └── shared │ │ │ │ ├── bearer-auth.middleware.ts │ │ │ │ ├── cookie-auth.middleware.ts │ │ │ │ ├── docs │ │ │ │ └── docs.router.ts │ │ │ │ ├── farm │ │ │ │ ├── farms.router.ts │ │ │ │ ├── farms.service.ts │ │ │ │ └── ger-farmer-farms.ts │ │ │ │ ├── farmer │ │ │ │ ├── farmers.router.ts │ │ │ │ ├── farmers.service.ts │ │ │ │ ├── get-farmer.ts │ │ │ │ ├── patch-farmer.ts │ │ │ │ └── serialize-farmer.ts │ │ │ │ ├── helpers │ │ │ │ └── any-origin.ts │ │ │ │ ├── is-admin.guard.ts │ │ │ │ ├── is-self.guard.ts │ │ │ │ ├── json-only.middleware.ts │ │ │ │ ├── jwt-auth.middleware.ts │ │ │ │ ├── openapi │ │ │ │ ├── document.ts │ │ │ │ ├── validator.ts │ │ │ │ └── websheep.yaml │ │ │ │ ├── or.guard.ts │ │ │ │ ├── sheep │ │ │ │ ├── add-sheep.ts │ │ │ │ ├── get-farmer-sheep-list.ts │ │ │ │ ├── serialize-sheep.ts │ │ │ │ ├── sheep.router.ts │ │ │ │ └── sheep.service.ts │ │ │ │ ├── token │ │ │ │ ├── authenticate.ts │ │ │ │ ├── create-token-and-set-cookie.ts │ │ │ │ ├── create-token.ts │ │ │ │ ├── delete-token.ts │ │ │ │ ├── tokens.cookie.router.ts │ │ │ │ ├── tokens.jwt.router.ts │ │ │ │ ├── tokens.jwt.service.ts │ │ │ │ ├── tokens.router.ts │ │ │ │ └── tokens.service.ts │ │ │ │ └── with-guard.middleware.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── sheep-0.jpg │ │ │ ├── sheep-1.jpg │ │ │ ├── sheep-10.jpg │ │ │ ├── sheep-11.jpg │ │ │ ├── sheep-12.jpg │ │ │ ├── sheep-13.jpg │ │ │ ├── sheep-14.jpg │ │ │ ├── sheep-15.jpg │ │ │ ├── sheep-16.jpg │ │ │ ├── sheep-17.jpg │ │ │ ├── sheep-18.jpg │ │ │ ├── sheep-19.jpg │ │ │ ├── sheep-2.jpg │ │ │ ├── sheep-20.jpg │ │ │ ├── sheep-3.jpg │ │ │ ├── sheep-4.jpg │ │ │ ├── sheep-5.jpg │ │ │ ├── sheep-6.jpg │ │ │ ├── sheep-7.jpg │ │ │ ├── sheep-8.jpg │ │ │ └── sheep-9.jpg │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── fixtures │ │ │ ├── farmers.ts │ │ │ ├── farms.ts │ │ │ ├── sheep-generator.md │ │ │ └── sheep.ts │ │ ├── main.ts │ │ └── testing │ │ │ └── test-pact-interaction.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tslint.json │ └── webpack.config.js ├── websheep-e2e │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json └── websheep │ ├── browserslist │ ├── jest.config.js │ ├── src │ ├── app │ │ ├── app-route-helper.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── assistant │ │ │ ├── api-selector-form │ │ │ │ ├── api-selector-form.component.html │ │ │ │ ├── api-selector-form.component.scss │ │ │ │ └── api-selector-form.component.ts │ │ │ ├── api-selector │ │ │ │ ├── api-selector.component.html │ │ │ │ ├── api-selector.component.scss │ │ │ │ └── api-selector.component.ts │ │ │ ├── assistant.actions.ts │ │ │ ├── assistant.reducer.ts │ │ │ ├── assistant.selectors.ts │ │ │ ├── assistant │ │ │ │ ├── assistant.component.html │ │ │ │ ├── assistant.component.scss │ │ │ │ └── assistant.component.ts │ │ │ ├── mission-info │ │ │ │ ├── mission-info.component.html │ │ │ │ ├── mission-info.component.scss │ │ │ │ └── mission-info.component.ts │ │ │ ├── mission-list.ts │ │ │ ├── mission.ts │ │ │ └── topic.ts │ │ ├── auth │ │ │ ├── auth.effects.ts │ │ │ ├── is-not-signed-in.guard.ts │ │ │ ├── is-signed-in.guard.ts │ │ │ └── signout.ts │ │ ├── config │ │ │ ├── config.actions.ts │ │ │ ├── config.reducer.ts │ │ │ └── config.selectors.ts │ │ ├── farmer │ │ │ └── farmer.service.ts │ │ ├── http │ │ │ ├── auth.interceptor.ts │ │ │ ├── http-interceptors.module.ts │ │ │ ├── list-response.ts │ │ │ └── prepend-base-url.interceptor.ts │ │ ├── layout │ │ │ ├── layout.actions.ts │ │ │ ├── layout.reducer.ts │ │ │ └── layout.selectors.ts │ │ ├── nav │ │ │ ├── nav.component.html │ │ │ ├── nav.component.scss │ │ │ └── nav.component.ts │ │ ├── reducers │ │ │ └── index.ts │ │ ├── sheep-core │ │ │ └── sheep.ts │ │ ├── sheep-form │ │ │ ├── add-sheep.service.ts │ │ │ ├── sheep-form.component.html │ │ │ ├── sheep-form.component.scss │ │ │ ├── sheep-form.component.ts │ │ │ └── user-farm.service.ts │ │ ├── sheep-list │ │ │ ├── sheep-list-container │ │ │ │ ├── sheep-list-container.component.html │ │ │ │ ├── sheep-list-container.component.scss │ │ │ │ ├── sheep-list-container.component.ts │ │ │ │ ├── user-sheep.service.spec.ts │ │ │ │ └── user-sheep.service.ts │ │ │ ├── sheep-list │ │ │ │ ├── sheep-list.component.html │ │ │ │ ├── sheep-list.component.scss │ │ │ │ └── sheep-list.component.ts │ │ │ └── sheep-preview │ │ │ │ ├── sheep-destination-emoji.pipe.ts │ │ │ │ ├── sheep-preview.component.html │ │ │ │ ├── sheep-preview.component.scss │ │ │ │ └── sheep-preview.component.ts │ │ ├── signin │ │ │ ├── signin-form.component.html │ │ │ ├── signin-form.component.scss │ │ │ ├── signin-form.component.ts │ │ │ └── signin.service.ts │ │ ├── toolbar │ │ │ ├── swagger-logo.png │ │ │ ├── toolbar.component.html │ │ │ ├── toolbar.component.scss │ │ │ └── toolbar.component.ts │ │ ├── typings.d.ts │ │ ├── user │ │ │ ├── user.actions.ts │ │ │ ├── user.reducer.ts │ │ │ └── user.selectors.ts │ │ └── views │ │ │ └── sheep │ │ │ ├── sheep-route-helper.ts │ │ │ └── sheep-views.module.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── lib │ │ ├── item-selector │ │ │ ├── item-selector.component.html │ │ │ ├── item-selector.component.scss │ │ │ └── item-selector.component.ts │ │ └── url-join.ts │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test-setup.ts │ └── testing │ │ └── pact-provider.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tslint.json │ └── vercel.json ├── jest.config.js ├── libs ├── .gitkeep └── pacts │ └── websheep-websheepapi.json ├── nx.json ├── package.json ├── sandbox.config.json ├── tools ├── schematics │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # App tmp files 42 | /db.json 43 | 44 | .yarn/* 45 | !.yarn/patches 46 | !.yarn/plugins 47 | !.yarn/releases 48 | !.yarn/sdks 49 | !.yarn/versions 50 | -------------------------------------------------------------------------------- /.idea/httpRequests/http-requests-log.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 2 | Accept: application/json 3 | Cache-Control: no-cache 4 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 5 | 6 | <> 2019-10-18T022109.200.json 7 | 8 | ### 9 | 10 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 11 | Accept: application/json 12 | Cache-Control: no-cache 13 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 14 | 15 | <> 2019-10-18T022040.404.page 16 | 17 | ### 18 | 19 | GET http://localhost:3333/v1/farmers/karinelemarchand/sheep 20 | Accept: application/json 21 | Cache-Control: no-cache 22 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 23 | 24 | <> 2019-10-18T022039.200.json 25 | 26 | ### 27 | 28 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 29 | Accept: application/json 30 | Cache-Control: no-cache 31 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 32 | 33 | <> 2019-10-18T022034.404.page 34 | 35 | ### 36 | 37 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 38 | Accept: application/json 39 | Cache-Control: no-cache 40 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 41 | 42 | ### 43 | 44 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 45 | Accept: application/json 46 | Cache-Control: no-cache 47 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 48 | 49 | ### 50 | 51 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 52 | Accept: application/json 53 | Cache-Control: no-cache 54 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 55 | 56 | <> 2019-10-18T022031-1.404.page 57 | 58 | ### 59 | 60 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 61 | Accept: application/json 62 | Cache-Control: no-cache 63 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 64 | 65 | <> 2019-10-18T022031.404.page 66 | 67 | ### 68 | 69 | GET http://localhost:3333/v1/farmers/karinelemarchand/farms 70 | Accept: application/json 71 | Cache-Control: no-cache 72 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 73 | 74 | <> 2019-10-18T022024.404.page 75 | 76 | ### 77 | 78 | GET http://localhost:3333/v1/farmers/karinelemarchand/sheep 79 | Accept: application/json 80 | Cache-Control: no-cache 81 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 82 | 83 | <> 2019-10-18T022022.200.json 84 | 85 | ### 86 | 87 | GET http://localhost:3333/v1/farmers/foobar/farms 88 | Accept: application/json 89 | Cache-Control: no-cache 90 | Authorization: Bearer HEmu2fwOBBCT2fXHpmVCaCYCxPoEwKcNgcPNFEfdl3o= 91 | 92 | <> 2019-10-18T022010.404.page 93 | 94 | ### 95 | 96 | GET http://localhost:3333/v1/farmers/foobar/farms 97 | Accept: application/json 98 | Cache-Control: no-cache 99 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 100 | 101 | <> 2019-10-18T021952.401.txt 102 | 103 | ### 104 | 105 | GET http://localhost:3333/v1/farmers/foobar/farms 106 | Accept: application/json 107 | Cache-Control: no-cache 108 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 109 | 110 | <> 2019-10-18T021908-1.401.txt 111 | 112 | ### 113 | 114 | GET http://localhost:3333/v1/farmers/foobar/farms 115 | Accept: application/json 116 | Cache-Control: no-cache 117 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 118 | 119 | <> 2019-10-18T021908.401.txt 120 | 121 | ### 122 | 123 | GET http://localhost:3333/v1/farmers/foobar/farms 124 | Accept: application/json 125 | Cache-Control: no-cache 126 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 127 | 128 | <> 2019-10-18T021907-4.401.txt 129 | 130 | ### 131 | 132 | GET http://localhost:3333/v1/farmers/foobar/farms 133 | Accept: application/json 134 | Cache-Control: no-cache 135 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 136 | 137 | <> 2019-10-18T021907-3.401.txt 138 | 139 | ### 140 | 141 | GET http://localhost:3333/v1/farmers/foobar/farms 142 | Accept: application/json 143 | Cache-Control: no-cache 144 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 145 | 146 | <> 2019-10-18T021907-2.401.txt 147 | 148 | ### 149 | 150 | GET http://localhost:3333/v1/farmers/foobar/farms 151 | Accept: application/json 152 | Cache-Control: no-cache 153 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 154 | 155 | <> 2019-10-18T021907-1.401.txt 156 | 157 | ### 158 | 159 | GET http://localhost:3333/v1/farmers/foobar/farms 160 | Accept: application/json 161 | Cache-Control: no-cache 162 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 163 | 164 | <> 2019-10-18T021907.401.txt 165 | 166 | ### 167 | 168 | GET http://localhost:3333/v1/farmers/foobar/farms 169 | Accept: application/json 170 | Cache-Control: no-cache 171 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 172 | 173 | <> 2019-10-18T021905.401.txt 174 | 175 | ### 176 | 177 | GET http://localhost:3333/v1/farmers/foobar/sheep 178 | Accept: application/json 179 | Cache-Control: no-cache 180 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 181 | 182 | <> 2019-10-18T021858.401.txt 183 | 184 | ### 185 | 186 | GET http://localhost:3333/v1/farmers/foobar/sheep 187 | Accept: application/json 188 | Cache-Control: no-cache 189 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 190 | 191 | <> 2019-10-18T120241.200.json 192 | 193 | ### 194 | 195 | GET http://localhost:3333/v1/farmers/foobar/sheep 196 | Accept: application/json 197 | Cache-Control: no-cache 198 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 199 | 200 | ### 201 | 202 | GET http://localhost:3333/v1/farmers/foobar/sheep 203 | Accept: application/json 204 | Cache-Control: no-cache 205 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM= 206 | 207 | <> 2019-10-18T115933.200.json 208 | 209 | ### 210 | 211 | GET http://localhost:3333/v1/farmers/foobar/sheep 212 | Accept: application/json 213 | Cache-Control: no-cache 214 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM=] 215 | 216 | <> 2019-10-18T115924.401.txt 217 | 218 | ### 219 | 220 | GET http://localhost:3333/v1/farmers/foobar/sheep 221 | Accept: application/json 222 | Cache-Control: no-cache 223 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM=] 224 | 225 | <> 2019-10-18T115904.401.txt 226 | 227 | ### 228 | 229 | GET http://localhost:3333/v1/farmers/foobar/sheep 230 | Accept: application/json 231 | Cache-Control: no-cache 232 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM=] 233 | 234 | <> 2019-10-18T115851.401.txt 235 | 236 | ### 237 | 238 | GET http://localhost:3333/v1/farmers/foobar/sheep 239 | Accept: application/json 240 | Cache-Control: no-cache 241 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM=] 242 | 243 | <> 2019-10-18T115818.401.txt 244 | 245 | ### 246 | 247 | GET http://localhost:3333/v1/farmers/foobar/sheep 248 | Accept: application/json 249 | Cache-Control: no-cache 250 | Authorization: Bearer s+XV5psYpXE6DrfdFsuafHl43ICo1VKhOITFgVpGtlM=] 251 | 252 | <> 2019-10-18T115724.500.page 253 | 254 | ### 255 | 256 | GET http://localhost:3333/v1/farmers/foobar/sheep 257 | Accept: application/json 258 | Cache-Control: no-cache 259 | 260 | <> 2019-10-18T115706.401.txt 261 | 262 | ### 263 | 264 | GET http://localhost:3333/v1/farmers/foobar/sheep 265 | Accept: application/json 266 | Cache-Control: no-cache 267 | 268 | <> 2019-10-18T101627.200.json 269 | 270 | ### 271 | 272 | GET http://localhost:3333/v1/farmers/foobar/sheep 273 | Accept: application/json 274 | Cache-Control: no-cache 275 | 276 | <> 2019-10-18T101554.200.json 277 | 278 | ### 279 | 280 | GET http://localhost:3333/v1/farmers/foobar/sheep 281 | Accept: application/json 282 | Cache-Control: no-cache 283 | 284 | <> 2019-10-18T101336.200.json 285 | 286 | ### 287 | 288 | GET http://localhost:3333/v1/farmers/foobar/sheep 289 | Accept: application/json 290 | Cache-Control: no-cache 291 | 292 | <> 2019-10-18T101329.200.json 293 | 294 | ### 295 | 296 | GET http://localhost:3333/v1/farmers/foobar/sheep 297 | Accept: application/json 298 | Cache-Control: no-cache 299 | 300 | <> 2019-10-18T101218.200.json 301 | 302 | ### 303 | 304 | GET http://localhost:3333/v1/farmers/foobar/sheep 305 | Accept: application/json 306 | Cache-Control: no-cache 307 | 308 | <> 2019-10-18T101139.200.json 309 | 310 | ### 311 | 312 | GET http://localhost:3333/v1/farmers/foobar/sheep 313 | Accept: application/json 314 | Cache-Control: no-cache 315 | 316 | <> 2019-10-18T101059.500.page 317 | 318 | ### 319 | 320 | GET http://localhost:3333/v1/farmers/foobar/sheep 321 | Accept: application/json 322 | Cache-Control: no-cache 323 | 324 | <> 2019-10-18T101058-3.500.page 325 | 326 | ### 327 | 328 | GET http://localhost:3333/v1/farmers/foobar/sheep 329 | Accept: application/json 330 | Cache-Control: no-cache 331 | 332 | <> 2019-10-18T101058-2.500.page 333 | 334 | ### 335 | 336 | GET http://localhost:3333/v1/farmers/foobar/sheep 337 | Accept: application/json 338 | Cache-Control: no-cache 339 | 340 | <> 2019-10-18T101058-1.500.page 341 | 342 | ### 343 | 344 | GET http://localhost:3333/v1/farmers/foobar/sheep 345 | Accept: application/json 346 | Cache-Control: no-cache 347 | 348 | <> 2019-10-18T101058.500.page 349 | 350 | ### 351 | 352 | GET http://localhost:3333/v1/farmers/foobar/sheep 353 | Accept: application/json 354 | Cache-Control: no-cache 355 | 356 | <> 2019-10-18T101057.500.page 357 | 358 | ### 359 | 360 | GET http://localhost:3333/v1/farmers/foobar/sheep 361 | Accept: application/json 362 | Cache-Control: no-cache 363 | 364 | <> 2019-10-18T101056.500.page 365 | 366 | ### 367 | 368 | GET http://localhost:3333/v1/farmers/foobar/sheep 369 | Accept: application/json 370 | Cache-Control: no-cache 371 | 372 | <> 2019-10-18T100951.200.json 373 | 374 | ### 375 | 376 | GET http://localhost:3333/v1/farmers/foobar/sheep 377 | Accept: application/json 378 | Cache-Control: no-cache 379 | 380 | <> 2019-10-18T100903-1.200.json 381 | 382 | ### 383 | 384 | GET http://localhost:3333/v1/farmers/foobar/sheep 385 | Accept: application/json 386 | Cache-Control: no-cache 387 | 388 | <> 2019-10-18T100903.200.json 389 | 390 | ### 391 | 392 | GET http://localhost:3333/v1/farmers/foobar/sheep 393 | Accept: application/json 394 | Cache-Control: no-cache 395 | 396 | <> 2019-10-18T100902.200.json 397 | 398 | ### 399 | 400 | GET http://localhost:3333/v1/farmers/foobar/sheep 401 | Accept: application/json 402 | Cache-Control: no-cache 403 | 404 | <> 2019-10-18T100841-2.200.json 405 | 406 | ### 407 | 408 | GET http://localhost:3333/v1/farmers/foobar/sheep 409 | Accept: application/json 410 | Cache-Control: no-cache 411 | 412 | <> 2019-10-18T100841-1.200.json 413 | 414 | ### 415 | 416 | GET http://localhost:3333/v1/farmers/foobar/sheep 417 | Accept: application/json 418 | Cache-Control: no-cache 419 | 420 | <> 2019-10-18T100841.200.json 421 | 422 | ### 423 | 424 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "ms-vscode.vscode-typescript-tslint-plugin", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.3.1.cjs 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/apps/websheep-api/main 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websheep 2 | 3 | [Open in Codesandbox](https://codesandbox.io/s/github/wishtack/websheep) 4 | 5 | # Local setup 6 | 7 | Install [Node](https://nodejs.org/en/download/) & [Yarn](https://classic.yarnpkg.com/en/docs/install/) 8 | 9 | ``` 10 | yarn install 11 | yarn start 12 | ``` 13 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "projects": { 4 | "websheep": { 5 | "projectType": "application", 6 | "schematics": { 7 | "@nrwl/angular:component": { 8 | "style": "scss" 9 | } 10 | }, 11 | "root": "apps/websheep", 12 | "sourceRoot": "apps/websheep/src", 13 | "prefix": "ws", 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:browser", 17 | "options": { 18 | "outputPath": "dist/apps/websheep", 19 | "index": "apps/websheep/src/index.html", 20 | "main": "apps/websheep/src/main.ts", 21 | "polyfills": "apps/websheep/src/polyfills.ts", 22 | "tsConfig": "apps/websheep/tsconfig.app.json", 23 | "aot": true, 24 | "assets": [ 25 | "apps/websheep/src/favicon.ico", 26 | "apps/websheep/src/assets" 27 | ], 28 | "styles": ["apps/websheep/src/styles.scss"], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "apps/websheep/src/environments/environment.ts", 36 | "with": "apps/websheep/src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "aot": true, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true, 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "6kb", 57 | "maximumError": "10kb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "websheep:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "websheep:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "websheep:build" 78 | } 79 | }, 80 | "lint": { 81 | "builder": "@angular-devkit/build-angular:tslint", 82 | "options": { 83 | "tsConfig": [ 84 | "apps/websheep/tsconfig.app.json", 85 | "apps/websheep/tsconfig.spec.json" 86 | ], 87 | "exclude": ["**/node_modules/**", "!apps/websheep/**"] 88 | } 89 | }, 90 | "test": { 91 | "builder": "@nrwl/jest:jest", 92 | "options": { 93 | "jestConfig": "apps/websheep/jest.config.js", 94 | "tsConfig": "apps/websheep/tsconfig.spec.json", 95 | "setupFile": "apps/websheep/src/test-setup.ts" 96 | } 97 | } 98 | } 99 | }, 100 | "websheep-e2e": { 101 | "root": "apps/websheep-e2e", 102 | "sourceRoot": "apps/websheep-e2e/src", 103 | "projectType": "application", 104 | "architect": { 105 | "e2e": { 106 | "builder": "@nrwl/cypress:cypress", 107 | "options": { 108 | "cypressConfig": "apps/websheep-e2e/cypress.json", 109 | "tsConfig": "apps/websheep-e2e/tsconfig.e2e.json", 110 | "devServerTarget": "websheep:serve" 111 | }, 112 | "configurations": { 113 | "production": { 114 | "devServerTarget": "websheep:serve:production" 115 | } 116 | } 117 | }, 118 | "lint": { 119 | "builder": "@angular-devkit/build-angular:tslint", 120 | "options": { 121 | "tsConfig": ["apps/websheep-e2e/tsconfig.e2e.json"], 122 | "exclude": ["**/node_modules/**", "!apps/websheep-e2e/**"] 123 | } 124 | } 125 | } 126 | }, 127 | "websheep-api": { 128 | "root": "apps/websheep-api", 129 | "sourceRoot": "apps/websheep-api/src", 130 | "projectType": "application", 131 | "prefix": "websheep-api", 132 | "schematics": {}, 133 | "architect": { 134 | "build": { 135 | "builder": "@nrwl/node:build", 136 | "options": { 137 | "outputPath": "dist/apps/websheep-api", 138 | "main": "apps/websheep-api/src/main.ts", 139 | "tsConfig": "apps/websheep-api/tsconfig.app.json", 140 | "assets": ["apps/websheep-api/src/assets"], 141 | "webpackConfig": "apps/websheep-api/webpack.config.js" 142 | }, 143 | "configurations": { 144 | "production": { 145 | "optimization": true, 146 | "extractLicenses": true, 147 | "inspect": false, 148 | "fileReplacements": [ 149 | { 150 | "replace": "apps/websheep-api/src/environments/environment.ts", 151 | "with": "apps/websheep-api/src/environments/environment.prod.ts" 152 | } 153 | ] 154 | } 155 | } 156 | }, 157 | "serve": { 158 | "builder": "@nrwl/node:execute", 159 | "options": { 160 | "buildTarget": "websheep-api:build" 161 | } 162 | }, 163 | "lint": { 164 | "builder": "@angular-devkit/build-angular:tslint", 165 | "options": { 166 | "tsConfig": [ 167 | "apps/websheep-api/tsconfig.app.json", 168 | "apps/websheep-api/tsconfig.spec.json" 169 | ], 170 | "exclude": ["**/node_modules/**", "!apps/websheep-api/**"] 171 | } 172 | }, 173 | "test": { 174 | "builder": "@nrwl/jest:jest", 175 | "options": { 176 | "jestConfig": "apps/websheep-api/jest.config.js", 177 | "tsConfig": "apps/websheep-api/tsconfig.spec.json" 178 | } 179 | } 180 | } 181 | } 182 | }, 183 | "cli": { 184 | "defaultCollection": "@nrwl/angular" 185 | }, 186 | "schematics": { 187 | "@nrwl/angular:application": { 188 | "unitTestRunner": "jest", 189 | "e2eTestRunner": "cypress" 190 | }, 191 | "@nrwl/angular:library": { 192 | "unitTestRunner": "jest" 193 | } 194 | }, 195 | "defaultProject": "websheep" 196 | } 197 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Websheep", 3 | "description": "Websheep", 4 | "repository": "https://github.com/wishtack/Websheep" 5 | } 6 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/websheep-api/jest-yaml-transformer.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | process(src) { 4 | return `module.exports = {default: \`${src}\`}`; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/websheep-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'websheep-api', 3 | preset: '../../jest.config.js', 4 | coverageDirectory: '../../coverage/apps/websheep-api', 5 | transform: { 6 | '^.+\\.yaml$': '/jest-yaml-transformer.js' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/app/.gitkeep -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz1/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cors from 'cors'; 3 | import { Router } from 'express'; 4 | import { bearerAuthMiddleware } from '../shared/bearer-auth.middleware'; 5 | 6 | import { docsRouter } from '../shared/docs/docs.router'; 7 | import { farmsRouter } from '../shared/farm/farms.router'; 8 | import { farmersRouter } from '../shared/farmer/farmers.router'; 9 | import { sheepRouter } from './sheep.router'; 10 | import { tokensRouter } from '../shared/token/tokens.router'; 11 | import { jsonOnly } from '../shared/json-only.middleware'; 12 | 13 | export const authz1Router = Router(); 14 | 15 | authz1Router.use(cors()); 16 | authz1Router.use(jsonOnly()); 17 | authz1Router.use(bodyParser.json()); 18 | 19 | authz1Router.use(docsRouter); 20 | authz1Router.use(tokensRouter); 21 | 22 | authz1Router.use(bearerAuthMiddleware); 23 | 24 | authz1Router.use(farmersRouter); 25 | authz1Router.use(farmsRouter); 26 | authz1Router.use(sheepRouter); 27 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz1/pact.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Express } from 'express'; 3 | import { authz1Router } from '.'; 4 | import { interactions } from '../../../../../libs/pacts/websheep-websheepapi.json'; 5 | import { resetDatabase } from './../database'; 6 | import { farmsService } from './../shared/farm/farms.service'; 7 | import { farmersService } from './../shared/farmer/farmers.service'; 8 | import { sheepService } from './../shared/sheep/sheep.service'; 9 | import { testPactInteraction } from '../../testing/test-pact-interaction'; 10 | 11 | describe('websheep api contract testing', () => { 12 | let app: Express; 13 | let farmGreenId: string; 14 | let stateProviders = { 15 | 'user is farmer Foo': () => { 16 | /* Mock token authentication. */ 17 | jest 18 | .spyOn(farmersService, 'getByToken') 19 | .mockImplementation(() => { 20 | return { id: 'FARMER_FOO' }; 21 | }); 22 | }, 23 | 'farm Green exists': () => { 24 | farmGreenId = farmsService.createFarm({farm: {name: 'Green'}}).id; 25 | }, 26 | 'farm Green has a sheep named Dolly': () => { 27 | sheepService.createSheep({ sheep: {name: 'Dolly', farmId: farmGreenId}}); 28 | }, 29 | 'farm Green has a sheep named Bruce': () => { 30 | sheepService.createSheep({ sheep: {name: 'Bruce', farmId: farmGreenId}}); 31 | }, 32 | 'farmer Foo is farm Green owner': () => { 33 | farmsService.setFarmFarmers({farmId: farmGreenId, farmerIds: ['FARMER_FOO']}) 34 | } 35 | }; 36 | 37 | beforeEach(() => { 38 | app = express(); 39 | app.use(authz1Router); 40 | 41 | resetDatabase(); 42 | }); 43 | 44 | it.each( 45 | interactions.map(interaction => [ 46 | interaction.description, 47 | interaction.providerState || 'no state', 48 | interaction 49 | ]) 50 | )('should check %s given %s', async (description, states, interaction) => { 51 | for (const state of states.split(',')) { 52 | expect(Object.keys(stateProviders)).toContain(state); 53 | await stateProviders[state](); 54 | } 55 | 56 | await testPactInteraction({ app, interaction }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz1/sheep.router.spec.ts: -------------------------------------------------------------------------------- 1 | import { openApiDocument } from './../shared/openapi/document'; 2 | import * as express from 'express'; 3 | // Import this plugin 4 | import jestOpenAPI from 'jest-openapi'; 5 | import { join } from 'path'; 6 | import * as request from 'supertest'; 7 | import { resetDatabase } from '../database'; 8 | import { sheepRouter } from './sheep.router'; 9 | 10 | jestOpenAPI({ 11 | ...openApiDocument, 12 | /* Remove servers otherwise the test crashes because we are using a random server port. */ 13 | servers: [] 14 | }); 15 | 16 | describe('sheep router', () => { 17 | it(`should get farmer's sheep without authorization`, async () => { 18 | const { client, givenUser } = setUp(); 19 | 20 | givenUser('karinelemarchand'); 21 | 22 | const response = await client.get('/farmers/foobar/sheep'); 23 | 24 | expect(response.status).toEqual(200); 25 | expect(response.body.totalCount).toEqual(13); 26 | expect(response.body.items[0].name).toEqual('Adriana'); 27 | expect(response).toSatisfyApiSpec(); 28 | }); 29 | 30 | function setUp() { 31 | let _userId: string; 32 | 33 | const app = express(); 34 | 35 | app.use((req, res, next) => { 36 | req['user'] = _userId ? { id: _userId } : null; 37 | next(); 38 | }); 39 | app.use(sheepRouter); 40 | 41 | resetDatabase(); 42 | 43 | return { 44 | client: request(app), 45 | givenUser(userId: string) { 46 | _userId = userId; 47 | } 48 | }; 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz1/sheep.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { validate } from '../shared/openapi/validator'; 3 | import { addSheep } from '../shared/sheep/add-sheep'; 4 | import { getFarmerSheepList } from '../shared/sheep/get-farmer-sheep-list'; 5 | 6 | export const sheepRouter = Router(); 7 | 8 | sheepRouter.get('/farmers/:farmerId/sheep', getFarmerSheepList); 9 | 10 | sheepRouter.post('/sheep', validate(), addSheep); 11 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/farmers.router.spec.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import { Express } from 'express'; 3 | import * as request from 'supertest'; 4 | import * as express from 'express'; 5 | import { resetDatabase } from '../database'; 6 | import { farmersRouter } from './farmers.router'; 7 | 8 | describe('farmers router', () => { 9 | let app: Express; 10 | 11 | beforeEach(() => { 12 | app = express(); 13 | 14 | app.use(bodyParser.json()); 15 | 16 | /* Suppose user is authenticated. */ 17 | app.use((req, res, next) => { 18 | req['user'] = { 19 | id: 'karinelemarchand' 20 | }; 21 | next(); 22 | }); 23 | 24 | app.use(farmersRouter); 25 | 26 | resetDatabase(); 27 | }); 28 | 29 | it(`should allow farmer to escalate to admin`, async () => { 30 | /* Escalate to isAdmin. */ 31 | const patchResult = await request(app) 32 | .patch('/farmers/karinelemarchand') 33 | .send({ 34 | isAdmin: true 35 | }); 36 | expect(patchResult.status).toEqual(200); 37 | 38 | /* Make sure isAdmin is now true. */ 39 | const getResult = await request(app).get('/farmers/karinelemarchand'); 40 | expect(getResult.status).toEqual(200); 41 | expect(getResult.body).toEqual( 42 | expect.objectContaining({ 43 | isAdmin: true 44 | }) 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/farmers.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { isSelf } from '../shared/is-self.guard'; 3 | import { validate } from '../shared/openapi/validator'; 4 | import { withGuard } from '../shared/with-guard.middleware'; 5 | import { getFarmer } from './get-farmer'; 6 | import { patchFarmer } from './patch-farmer'; 7 | 8 | export const farmersRouter = Router(); 9 | 10 | farmersRouter.get('/farmers/:farmerId', withGuard(isSelf), getFarmer); 11 | farmersRouter.patch('/farmers/:farmerId', withGuard(isSelf), patchFarmer); 12 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/get-farmer.ts: -------------------------------------------------------------------------------- 1 | import { farmersService } from '../shared/farmer/farmers.service'; 2 | import { serializeFarmer } from './serialize-farmer'; 3 | 4 | export function getFarmer(req, res) { 5 | const farmerId = req.user.id; 6 | res.json(serializeFarmer(farmersService.getFarmer({ farmerId }))); 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cors from 'cors'; 3 | import { Router } from 'express'; 4 | import { bearerAuthMiddleware } from '../shared/bearer-auth.middleware'; 5 | 6 | import { docsRouter } from '../shared/docs/docs.router'; 7 | import { tokensRouter } from '../shared/token/tokens.router'; 8 | import { farmersRouter } from './farmers.router'; 9 | import { farmsRouter } from '../shared/farm/farms.router'; 10 | import { sheepRouter } from './sheep.router'; 11 | import { jsonOnly } from '../shared/json-only.middleware'; 12 | 13 | export const authz2Router = Router(); 14 | 15 | authz2Router.use(cors()); 16 | authz2Router.use(jsonOnly()); 17 | authz2Router.use(bodyParser.json()); 18 | 19 | authz2Router.use(docsRouter); 20 | authz2Router.use(tokensRouter); 21 | 22 | authz2Router.use(bearerAuthMiddleware); 23 | 24 | authz2Router.use(farmersRouter); 25 | authz2Router.use(farmsRouter); 26 | authz2Router.use(sheepRouter); 27 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/patch-farmer.ts: -------------------------------------------------------------------------------- 1 | import { farmersService } from '../shared/farmer/farmers.service'; 2 | import { serializeFarmer } from './serialize-farmer'; 3 | 4 | export function patchFarmer(req, res) { 5 | const farmer = farmersService.updateFarmer({ 6 | farmerId: req.params.farmerId, 7 | farmer: req.body 8 | }); 9 | res.json(serializeFarmer(farmer)); 10 | } 11 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/serialize-farmer.ts: -------------------------------------------------------------------------------- 1 | export function serializeFarmer(farmer) { 2 | const serializedFarmer = { 3 | ...farmer 4 | }; 5 | delete serializedFarmer.passwordHash; 6 | return serializedFarmer; 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/authz2/sheep.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { isAdmin } from '../shared/is-admin.guard'; 3 | import { isSelf } from '../shared/is-self.guard'; 4 | import { validate } from '../shared/openapi/validator'; 5 | import { or } from '../shared/or.guard'; 6 | import { addSheep } from '../shared/sheep/add-sheep'; 7 | import { getFarmerSheepList } from '../shared/sheep/get-farmer-sheep-list'; 8 | import { withGuard } from '../shared/with-guard.middleware'; 9 | 10 | export const sheepRouter = Router(); 11 | 12 | sheepRouter.get( 13 | '/farmers/:farmerId/sheep', 14 | withGuard(or([isAdmin, isSelf])), 15 | getFarmerSheepList 16 | ); 17 | 18 | sheepRouter.post('/sheep', validate(), addSheep); 19 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/csrf1/csrf1.spec.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as express from 'express'; 3 | import { Express } from 'express'; 4 | import * as request from 'supertest'; 5 | import { resetDatabase } from '../database'; 6 | import { csrf1Router } from './index'; 7 | 8 | describe('tokens router', () => { 9 | let app: Express; 10 | 11 | beforeEach(() => { 12 | app = express(); 13 | 14 | app.use(csrf1Router); 15 | 16 | resetDatabase(); 17 | }); 18 | 19 | it(`should allow cross origin requests for any origin`, async () => { 20 | const response = await request(app) 21 | .post('/tokens') 22 | .send({ 23 | userName: 'karinelemarchand', 24 | password: '123456' 25 | }); 26 | expect(response.status).toEqual(201); 27 | expect(response.body).toEqual( 28 | expect.objectContaining({ 29 | userId: 'karinelemarchand' 30 | }) 31 | ); 32 | expect(response.headers['access-control-allow-credentials']).toEqual( 33 | 'true' 34 | ); 35 | expect(response.headers['set-cookie'].length).toEqual(1); 36 | expect(response.headers['set-cookie'][0]).toMatch(/^token=/); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/csrf1/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cors from 'cors'; 3 | import * as cookieParser from 'cookie-parser'; 4 | import { Router } from 'express'; 5 | import { cookieAuthMiddleware } from '../shared/cookie-auth.middleware'; 6 | 7 | import { docsRouter } from '../shared/docs/docs.router'; 8 | import { farmsRouter } from '../shared/farm/farms.router'; 9 | import { farmersRouter } from '../shared/farmer/farmers.router'; 10 | import { sheepRouter } from '../shared/sheep/sheep.router'; 11 | import { anyOrigin } from '../shared/helpers/any-origin'; 12 | import { tokensCookieRouter } from '../shared/token/tokens.cookie.router'; 13 | import { jsonOnly } from '../shared/json-only.middleware'; 14 | 15 | export const csrf1Router = Router(); 16 | 17 | csrf1Router.use(jsonOnly()); 18 | csrf1Router.use(bodyParser.json()); 19 | csrf1Router.use(cookieParser()); 20 | 21 | csrf1Router.use( 22 | cors({ 23 | credentials: true, 24 | origin: anyOrigin 25 | }) 26 | ); 27 | 28 | csrf1Router.use(docsRouter); 29 | csrf1Router.use(tokensCookieRouter); 30 | 31 | csrf1Router.use(cookieAuthMiddleware); 32 | 33 | csrf1Router.use(farmersRouter); 34 | csrf1Router.use(farmsRouter); 35 | csrf1Router.use(sheepRouter); 36 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/csrf2/csrf2.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Express } from 'express'; 3 | import * as request from 'supertest'; 4 | import { resetDatabase } from '../database'; 5 | import { farmersService } from '../shared/farmer/farmers.service'; 6 | import { csrf2Router } from './index'; 7 | 8 | describe('tokens router', () => { 9 | let app: Express; 10 | 11 | beforeEach(() => { 12 | app = express(); 13 | 14 | farmersService.getByToken = jest.fn().mockReturnValue({ 15 | id: 'USER_ID' 16 | }); 17 | 18 | app.use(csrf2Router); 19 | 20 | resetDatabase(); 21 | }); 22 | 23 | it(`should allow x-www-form-urlencoded`, async () => { 24 | const response = await request(app) 25 | .post('/sheep') 26 | .set('Cookie', ['token=TOKEN_VALUE']) 27 | .set('Content-Type', 'application/x-www-form-urlencoded') 28 | .send('name=Dolly&farm[id]=FARM_ID'); 29 | 30 | expect(response.status).toEqual(201); 31 | 32 | expect(response.body).toEqual( 33 | expect.objectContaining({ 34 | name: 'Dolly' 35 | }) 36 | ); 37 | 38 | /* ACAO header should be set to default allowed origin. */ 39 | expect(response.headers['access-control-allow-origin']).toEqual( 40 | 'http://localhost:4200' 41 | ); 42 | 43 | /* @todo Validate schema at the end cuz openApiValidator seems to mutate the response. */ 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/csrf2/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cookieParser from 'cookie-parser'; 3 | import * as cors from 'cors'; 4 | import { Router } from 'express'; 5 | import { environment } from '../../environments/environment'; 6 | import { cookieAuthMiddleware } from '../shared/cookie-auth.middleware'; 7 | import { docsRouter } from '../shared/docs/docs.router'; 8 | import { farmsRouter } from '../shared/farm/farms.router'; 9 | import { farmersRouter } from '../shared/farmer/farmers.router'; 10 | import { sheepRouter } from '../shared/sheep/sheep.router'; 11 | import { tokensCookieRouter } from '../shared/token/tokens.cookie.router'; 12 | 13 | export const csrf2Router = Router(); 14 | 15 | /* 16 | * This is an ugly deprecated shortcut for: 17 | * router.use(bodyParser.json()); 18 | * router.use(bodyParser.urlencoded({extended: false})); 19 | */ 20 | csrf2Router.use(bodyParser()); 21 | csrf2Router.use(cookieParser()); 22 | 23 | csrf2Router.use( 24 | cors({ 25 | credentials: true, 26 | origin: environment.appOrigin 27 | }) 28 | ); 29 | 30 | csrf2Router.use(docsRouter); 31 | csrf2Router.use(tokensCookieRouter); 32 | 33 | csrf2Router.use(cookieAuthMiddleware); 34 | 35 | csrf2Router.use(farmersRouter); 36 | csrf2Router.use(farmsRouter); 37 | csrf2Router.use(sheepRouter); 38 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/csrf3/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cookieParser from 'cookie-parser'; 3 | import * as cors from 'cors'; 4 | import { Router } from 'express'; 5 | import { environment } from '../../environments/environment'; 6 | import { cookieAuthMiddleware } from '../shared/cookie-auth.middleware'; 7 | import { docsRouter } from '../shared/docs/docs.router'; 8 | import { farmsRouter } from '../shared/farm/farms.router'; 9 | import { farmersRouter } from '../shared/farmer/farmers.router'; 10 | import { sheepRouter } from '../shared/sheep/sheep.router'; 11 | import { tokensCookieRouter } from '../shared/token/tokens.cookie.router'; 12 | 13 | export const csrf3Router = Router(); 14 | 15 | csrf3Router.use( 16 | bodyParser.json({ 17 | type: 'application/*' 18 | }) 19 | ); 20 | csrf3Router.use(cookieParser()); 21 | 22 | csrf3Router.use( 23 | cors({ 24 | credentials: true, 25 | origin: environment.appOrigin 26 | }) 27 | ); 28 | 29 | csrf3Router.use(docsRouter); 30 | csrf3Router.use(tokensCookieRouter); 31 | 32 | csrf3Router.use(cookieAuthMiddleware); 33 | 34 | csrf3Router.use(farmersRouter); 35 | csrf3Router.use(farmsRouter); 36 | csrf3Router.use(sheepRouter); 37 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/database.ts: -------------------------------------------------------------------------------- 1 | import * as low from 'lowdb'; 2 | import * as FileSync from 'lowdb/adapters/FileSync'; 3 | import { farmers } from '../fixtures/farmers'; 4 | import { farms } from '../fixtures/farms'; 5 | import { sheep } from '../fixtures/sheep'; 6 | 7 | export const databaseFilePath = '/tmp/websheep-db.json'; 8 | 9 | export const database = low(new FileSync(databaseFilePath)); 10 | 11 | export function resetDatabase() { 12 | database 13 | .setState({}) 14 | .defaults({ 15 | farms, 16 | farmers, 17 | sheep, 18 | tokens: [] 19 | }) 20 | .write(); 21 | } 22 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/jwt1/index.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cors from 'cors'; 3 | import { Router } from 'express'; 4 | 5 | import { docsRouter } from '../shared/docs/docs.router'; 6 | import { farmsRouter } from '../shared/farm/farms.router'; 7 | import { farmersRouter } from '../shared/farmer/farmers.router'; 8 | import { sheepRouter } from '../shared/sheep/sheep.router'; 9 | import { tokensJwtRouter } from '../shared/token/tokens.jwt.router'; 10 | import { jwtAuthMiddleware } from './jwt-auth.middleware'; 11 | import { jsonOnly } from '../shared/json-only.middleware'; 12 | 13 | export const jwt1Router = Router(); 14 | 15 | jwt1Router.use(cors()); 16 | jwt1Router.use(jsonOnly()); 17 | jwt1Router.use(bodyParser.json()); 18 | 19 | jwt1Router.use(docsRouter); 20 | jwt1Router.use(tokensJwtRouter); 21 | 22 | jwt1Router.use(jwtAuthMiddleware); 23 | jwt1Router.use(farmersRouter); 24 | jwt1Router.use(farmsRouter); 25 | jwt1Router.use(sheepRouter); 26 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/jwt1/jwt-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { Passport } from 'passport'; 3 | import { Strategy as BearerStrategy } from 'passport-http-bearer'; 4 | import { callbackify } from 'util'; 5 | import { farmersService } from '../shared/farmer/farmers.service'; 6 | 7 | const passport = new Passport(); 8 | 9 | passport.use( 10 | new BearerStrategy( 11 | callbackify(async token => { 12 | const claims = jwt.decode(token, process.env.JWT_SECRET); 13 | if (claims == null) { 14 | return null; 15 | } 16 | return farmersService.getFarmer({ farmerId: claims.sub }); 17 | }) 18 | ) 19 | ); 20 | 21 | export const jwtAuthMiddleware = passport.authenticate('bearer', { 22 | session: false 23 | }); 24 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/jwt2/index.ts: -------------------------------------------------------------------------------- 1 | import { jsonOnly } from './../shared/json-only.middleware'; 2 | import * as bodyParser from 'body-parser'; 3 | import * as cors from 'cors'; 4 | import { Router } from 'express'; 5 | 6 | import { docsRouter } from '../shared/docs/docs.router'; 7 | import { farmsRouter } from '../shared/farm/farms.router'; 8 | import { farmersRouter } from '../shared/farmer/farmers.router'; 9 | import { jwtAuthMiddleware } from '../shared/jwt-auth.middleware'; 10 | import { sheepRouter } from '../shared/sheep/sheep.router'; 11 | import { tokensJwtRouter } from '../shared/token/tokens.jwt.router'; 12 | 13 | export const jwt2Router = Router(); 14 | 15 | jwt2Router.use(cors()); 16 | jwt2Router.use(jsonOnly()); 17 | jwt2Router.use(bodyParser.json()); 18 | 19 | jwt2Router.use(docsRouter); 20 | jwt2Router.use(tokensJwtRouter); 21 | 22 | jwt2Router.use(jwtAuthMiddleware); 23 | jwt2Router.use(farmersRouter); 24 | jwt2Router.use(farmsRouter); 25 | jwt2Router.use(sheepRouter); 26 | 27 | /* Error handler. */ 28 | jwt2Router.use((err, req, res, next) => { 29 | const statusCode = err.statusCode || 500; 30 | res.status(statusCode).json({ 31 | errors: [ 32 | { 33 | name: err.name, 34 | message: err.message, 35 | data: err.data 36 | } 37 | ], 38 | env: process.env 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/bearer-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Passport } from 'passport'; 2 | import { Strategy as BearerStrategy } from 'passport-http-bearer'; 3 | import { callbackify } from 'util'; 4 | import { farmersService } from './farmer/farmers.service'; 5 | 6 | const passport = new Passport(); 7 | 8 | passport.use( 9 | new BearerStrategy( 10 | callbackify(async token => farmersService.getByToken({ token })) 11 | ) 12 | ); 13 | 14 | export const bearerAuthMiddleware = passport.authenticate('bearer', { 15 | session: false 16 | }); 17 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/cookie-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Passport } from 'passport'; 2 | import { Strategy as CookieStrategy } from 'passport-cookie'; 3 | import { callbackify } from 'util'; 4 | import { farmersService } from './farmer/farmers.service'; 5 | 6 | const passport = new Passport(); 7 | 8 | passport.use( 9 | new CookieStrategy( 10 | callbackify(async token => farmersService.getByToken({ token })) 11 | ) 12 | ); 13 | 14 | export const cookieAuthMiddleware = passport.authenticate('cookie', { 15 | session: false 16 | }); 17 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/docs/docs.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as swaggerUi from 'swagger-ui-express'; 3 | import { openApiDocument, openApiRaw } from '../openapi/document'; 4 | 5 | export const docsRouter = Router(); 6 | 7 | docsRouter.get('/', (req, res) => res.redirect(`${req.baseUrl}/docs`)); 8 | docsRouter.use('/docs', swaggerUi.serve); 9 | docsRouter.get('/docs', swaggerUi.setup(openApiDocument)); 10 | docsRouter.get('/docs/specification.yaml', (req, res) => res.send(openApiRaw)); 11 | docsRouter.get('/docs/specification.json', (req, res) => 12 | res.send(openApiDocument) 13 | ); 14 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farm/farms.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { getFarmerFarms } from './ger-farmer-farms'; 3 | import { withGuard } from '../with-guard.middleware'; 4 | import { isSelf } from '../is-self.guard'; 5 | 6 | export const farmsRouter = Router(); 7 | 8 | farmsRouter.get('/farmers/:farmerId/farms', withGuard(isSelf), getFarmerFarms); 9 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farm/farms.service.ts: -------------------------------------------------------------------------------- 1 | import * as shortid from 'shortid'; 2 | import { database } from '../../database'; 3 | 4 | export const farmsService = { 5 | createFarm({ farm }) { 6 | farm = { 7 | ...farm, 8 | id: shortid.generate() 9 | } 10 | database 11 | .get('farms') 12 | .push(farm) 13 | .write(); 14 | return farm; 15 | }, 16 | setFarmFarmers({farmId, farmerIds}) { 17 | return database 18 | .get('farms') 19 | .find({ id: farmId }) 20 | .assign({ 21 | farmerIds 22 | }) 23 | .value(); 24 | }, 25 | getFarmsByFarmerId({ farmerId }: { farmerId: string }) { 26 | return database 27 | .get('farms') 28 | .filter(farm => farm.farmerIds.includes(farmerId)) 29 | .value(); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farm/ger-farmer-farms.ts: -------------------------------------------------------------------------------- 1 | import { farmsService } from './farms.service'; 2 | 3 | export function getFarmerFarms(req, res) { 4 | const { farmerId } = req.params; 5 | 6 | const farms = farmsService.getFarmsByFarmerId({ farmerId }); 7 | 8 | res.json({ 9 | previous: null, 10 | next: null, 11 | totalCount: farms.length, 12 | items: farms 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farmer/farmers.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { getFarmer } from './get-farmer'; 3 | import { patchFarmer } from './patch-farmer'; 4 | import { withGuard } from '../with-guard.middleware'; 5 | import { isSelf } from '../is-self.guard'; 6 | 7 | export const farmersRouter = Router(); 8 | 9 | farmersRouter.get('/farmers/:farmerId', withGuard(isSelf), getFarmer); 10 | farmersRouter.patch('/farmers/:farmerId', withGuard(isSelf), patchFarmer); 11 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farmer/farmers.service.ts: -------------------------------------------------------------------------------- 1 | import { database } from '../../database'; 2 | 3 | export const farmersService = { 4 | getFarmer({ farmerId }: { farmerId: string }) { 5 | return database 6 | .get('farmers') 7 | .find({ id: farmerId }) 8 | .value(); 9 | }, 10 | getByToken({ token }: { token: string }) { 11 | const tokenInfo = database 12 | .get('tokens') 13 | .find({ token }) 14 | .value(); 15 | 16 | if (tokenInfo == null) { 17 | return null; 18 | } 19 | 20 | return this.getFarmer({ farmerId: tokenInfo.userId }); 21 | }, 22 | updateFarmer({ farmerId, farmer }: { farmerId: string; farmer }) { 23 | const data = { ...farmer }; 24 | delete data.id; 25 | delete data.passwordHash; 26 | 27 | return database 28 | .get('farmers') 29 | .find({ id: farmerId }) 30 | .assign(data) 31 | .value(); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farmer/get-farmer.ts: -------------------------------------------------------------------------------- 1 | import { farmersService } from './farmers.service'; 2 | import { serializeFarmer } from './serialize-farmer'; 3 | 4 | export function getFarmer(req, res) { 5 | const farmerId = req.params.farmerId; 6 | res.json(serializeFarmer(farmersService.getFarmer({ farmerId }))); 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farmer/patch-farmer.ts: -------------------------------------------------------------------------------- 1 | import { farmersService } from './farmers.service'; 2 | import { serializeFarmer } from './serialize-farmer'; 3 | 4 | export function patchFarmer(req, res) { 5 | const farmer = farmersService.updateFarmer({ 6 | farmerId: req.params.farmerId, 7 | farmer: { 8 | id: req.body.id, 9 | firstName: req.body.firstName, 10 | lastName: req.body.lastName 11 | } 12 | }); 13 | res.json(serializeFarmer(farmer)); 14 | } 15 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/farmer/serialize-farmer.ts: -------------------------------------------------------------------------------- 1 | export function serializeFarmer(farmer) { 2 | return { 3 | id: farmer.id, 4 | firstName: farmer.firstName, 5 | lastName: farmer.lastName 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/helpers/any-origin.ts: -------------------------------------------------------------------------------- 1 | import { callbackify } from 'util'; 2 | 3 | export const anyOrigin = callbackify(async () => true); 4 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/is-admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from './with-guard.middleware'; 2 | 3 | export const isAdmin: Guard = req => { 4 | return req['user'].isAdmin === true; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/is-self.guard.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from './with-guard.middleware'; 2 | 3 | export const isSelf: Guard = req => { 4 | return req['user'].id === req.params.farmerId; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/json-only.middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | export const jsonOnly = () => (req, res, next) => { 3 | if (['PATCH','POST','PUT'].includes(req.method) && req.header('Content-Type') !== 'application/json') { 4 | res.sendStatus(415); 5 | return; 6 | } 7 | next(); 8 | }; -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/jwt-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { Passport } from 'passport'; 3 | import { Strategy as BearerStrategy } from 'passport-http-bearer'; 4 | import { callbackify } from 'util'; 5 | import { farmersService } from '../shared/farmer/farmers.service'; 6 | 7 | const passport = new Passport(); 8 | 9 | passport.use( 10 | new BearerStrategy( 11 | callbackify(async token => { 12 | try { 13 | const { sub } = jwt.verify(token, process.env.JWT_SECRET); 14 | 15 | return farmersService.getFarmer({ farmerId: sub }); 16 | } catch (e) { 17 | if (e.name !== 'JsonWebTokenError') { 18 | throw e; 19 | } 20 | 21 | return false; 22 | } 23 | }) 24 | ) 25 | ); 26 | 27 | export const jwtAuthMiddleware = passport.authenticate('bearer', { 28 | session: false 29 | }); 30 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/openapi/document.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'yamljs'; 2 | import { environment } from '../../../environments/environment'; 3 | 4 | export const openApiRaw = require('./websheep.yaml').default; 5 | export const openApiDocument = { 6 | ...yaml.parse(openApiRaw), 7 | servers: [ 8 | { 9 | description: 'Broken Access Control 1', 10 | url: '/authz1' 11 | }, 12 | { 13 | description: 'Broken Access Control 2', 14 | url: '/authz2' 15 | }, 16 | { 17 | description: 'C.S.R.F. 1', 18 | url: '/csrf1' 19 | }, 20 | { 21 | description: 'C.S.R.F. 2', 22 | url: '/csrf2' 23 | }, 24 | { 25 | description: 'C.S.R.F. 3', 26 | url: '/csrf3' 27 | }, 28 | { 29 | description: 'J.W.T. 1', 30 | url: '/jwt1' 31 | }, 32 | { 33 | description: 'J.W.T. 2', 34 | url: '/jwt2' 35 | } 36 | ].map(server => ({ 37 | ...server, 38 | url: `${environment.apiServerUrl}${server.url}` 39 | })) 40 | }; 41 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/openapi/validator.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express-serve-static-core'; 2 | import { openApiDocument } from './document'; 3 | import * as OpenApiValidator from 'express-openapi-validator'; 4 | 5 | export const validate = (): RequestHandler[] => 6 | OpenApiValidator.middleware({ 7 | apiSpec: openApiDocument, 8 | validateFormats: 'full', 9 | validateRequests: true, 10 | validateSecurity: false 11 | }); 12 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/openapi/websheep.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.1.0' 2 | info: 3 | version: 1.0.0 4 | title: Websheep 5 | description: | 6 | * [JSON specification](specification.json) 7 | * [YAML specification](specification.yaml) 8 | license: 9 | name: MIT 10 | servers: 11 | - description: Broken Access Control 1 12 | url: http://localhost:3333/authz1 13 | - description: Broken Access Control 2 14 | url: http://localhost:3333/authz2 15 | - description: Bad C.O.R.S. rules 16 | url: http://localhost:3333/csrf1 17 | paths: 18 | # 19 | # Authentication 20 | # 21 | /tokens: 22 | post: 23 | summary: Create a token 24 | tags: 25 | - Authentication 26 | operationId: createToken 27 | requestBody: 28 | description: Farmer credentials 29 | required: true 30 | content: 31 | application/json: 32 | schema: 33 | $ref: '#/components/schemas/Credentials' 34 | responses: 35 | 201: 36 | description: Farmer id and token 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/TokenResponse' 41 | 400: 42 | $ref: '#/components/responses/400' 43 | 44 | /tokens/{tokenId}: 45 | delete: 46 | summary: Destroy token 47 | tags: 48 | - Authentication 49 | operationId: deleteToken 50 | parameters: 51 | - in: path 52 | name: tokenId 53 | required: true 54 | schema: 55 | type: string 56 | description: The token's id 57 | responses: 58 | 204: 59 | description: Token destroyed 60 | security: 61 | - BearerAuth: [] 62 | 63 | # 64 | # Farm 65 | # 66 | /farmers/{farmerId}/farms: 67 | get: 68 | summary: Get farmer's farms 69 | tags: 70 | - Farm 71 | operationId: getFarmerFarms 72 | parameters: 73 | - in: path 74 | name: farmerId 75 | required: true 76 | schema: 77 | type: string 78 | description: The farmer's id 79 | responses: 80 | 200: 81 | description: OK 82 | content: 83 | application/json: 84 | schema: 85 | $ref: '#/components/schemas/FarmListResponse' 86 | security: 87 | - BearerAuth: [] 88 | 89 | # 90 | # Farmer 91 | # 92 | /farmers/{farmerId}: 93 | get: 94 | summary: Get farmer 95 | tags: 96 | - Farmer 97 | operationId: getFarmer 98 | parameters: 99 | - in: path 100 | name: farmerId 101 | required: true 102 | schema: 103 | type: string 104 | description: The farmer's id 105 | responses: 106 | 200: 107 | description: OK 108 | content: 109 | application/json: 110 | schema: 111 | $ref: '#/components/schemas/Farmer' 112 | security: 113 | - BearerAuth: [] 114 | 115 | patch: 116 | summary: Update farmer 117 | tags: 118 | - Farmer 119 | operationId: updateFarmer 120 | parameters: 121 | - in: path 122 | name: farmerId 123 | required: true 124 | schema: 125 | type: string 126 | description: The farmer's id 127 | requestBody: 128 | description: Farmer 129 | required: true 130 | content: 131 | application/json: 132 | schema: 133 | $ref: '#/components/schemas/Farmer' 134 | responses: 135 | 200: 136 | description: OK 137 | content: 138 | application/json: 139 | schema: 140 | $ref: '#/components/schemas/Farmer' 141 | 400: 142 | $ref: '#/components/responses/400' 143 | security: 144 | - BearerAuth: [] 145 | 146 | # 147 | # Sheep 148 | # 149 | /farmers/{farmerId}/sheep: 150 | get: 151 | summary: Get farmer's sheep 152 | tags: 153 | - Sheep 154 | operationId: getFarmerSheep 155 | parameters: 156 | - in: path 157 | name: farmerId 158 | required: true 159 | schema: 160 | type: string 161 | description: The farmer's id 162 | responses: 163 | 200: 164 | description: OK 165 | content: 166 | application/json: 167 | schema: 168 | $ref: '#/components/schemas/SheepListResponse' 169 | security: 170 | - BearerAuth: [] 171 | 172 | /sheep: 173 | post: 174 | summary: Add sheep 175 | tags: 176 | - Sheep 177 | operationId: addSheep 178 | requestBody: 179 | description: Sheep info 180 | required: true 181 | content: 182 | application/json: 183 | schema: 184 | $ref: '#/components/schemas/SheepRequest' 185 | # Allow x-www-form-urlencoded in order to be able to 186 | # bypass C.S.R.F. for exercices csrf2 & csrf3. 187 | application/x-www-form-urlencoded: 188 | schema: 189 | $ref: '#/components/schemas/SheepRequest' 190 | responses: 191 | 201: 192 | description: The created sheep 193 | content: 194 | application/json: 195 | schema: 196 | $ref: '#/components/schemas/SheepWithFarm' 197 | 400: 198 | $ref: '#/components/responses/400' 199 | security: 200 | - BearerAuth: [] 201 | 202 | components: 203 | schemas: 204 | Credentials: 205 | type: object 206 | additionalProperties: false 207 | required: 208 | - userName 209 | - password 210 | properties: 211 | userName: 212 | type: string 213 | example: karinelemarchand 214 | password: 215 | type: string 216 | format: password 217 | example: '123456' 218 | 219 | Farm: 220 | type: object 221 | additionalProperties: false 222 | properties: 223 | id: 224 | type: string 225 | name: 226 | type: string 227 | 228 | FarmListResponse: 229 | allOf: 230 | - $ref: '#/components/schemas/ListResponse' 231 | - type: object 232 | additionalProperties: false 233 | properties: 234 | items: 235 | type: array 236 | items: 237 | $ref: '#/components/schemas/Farm' 238 | 239 | Farmer: 240 | type: object 241 | additionalProperties: false 242 | properties: 243 | id: 244 | type: string 245 | firstName: 246 | type: string 247 | lastName: 248 | type: string 249 | maxLength: 10 250 | 251 | ListResponse: 252 | type: object 253 | additionalProperties: false 254 | properties: 255 | next: 256 | type: string 257 | previous: 258 | type: string 259 | totalCount: 260 | type: number 261 | items: 262 | type: array 263 | # Any type 264 | items: {} 265 | 266 | SheepBase: 267 | type: object 268 | additionalProperties: false 269 | required: 270 | - name 271 | properties: &sheepBaseProperties 272 | age: 273 | type: number 274 | destinations: 275 | type: array 276 | items: 277 | type: string 278 | enum: 279 | - kebab 280 | - wool 281 | eyeColor: 282 | type: string 283 | gender: 284 | type: string 285 | enum: 286 | - female 287 | - male 288 | name: 289 | type: string 290 | pictureUri: 291 | type: string 292 | nullable: true 293 | 294 | SheepRequest: 295 | type: object 296 | additionalProperties: false 297 | properties: 298 | # @hack using anchors because express-openapi-validate fails to check with allOf 299 | <<: *sheepBaseProperties 300 | farmId: 301 | type: string 302 | farm: 303 | deprecated: true 304 | description: '@deprecated use farmId field instead' 305 | type: object 306 | properties: 307 | id: 308 | type: string 309 | 310 | # SheepRequest: 311 | # allOf: 312 | # - $ref: '#/components/schemas/SheepBase' 313 | # - oneOf: 314 | # - type: object 315 | # additionalProperties: false 316 | # properties: 317 | # farmId: 318 | # type: string 319 | # - type: object 320 | # additionalProperties: false 321 | # properties: 322 | # farm: 323 | # deprecated: true 324 | # description: '@deprecated use farmId field instead' 325 | # type: object 326 | # properties: 327 | # id: 328 | # type: string 329 | 330 | Sheep: 331 | type: object 332 | additionalProperties: false 333 | # @hack using anchors because express-openapi-validate fails to check with allOf 334 | properties: &sheepProperties 335 | <<: *sheepBaseProperties 336 | id: 337 | type: string 338 | createdAt: 339 | type: string 340 | format: date-time 341 | farmId: 342 | type: string 343 | 344 | # Sheep: 345 | # allOf: 346 | # - $ref: '#/components/schemas/SheepBase' 347 | # - type: object 348 | # properties: 349 | # id: 350 | # type: string 351 | # createdAt: 352 | # type: string 353 | # format: date-time 354 | 355 | SheepWithFarm: 356 | type: object 357 | additionalProperties: false 358 | properties: 359 | # @hack using anchors because express-openapi-validate fails to check with allOf 360 | <<: *sheepProperties 361 | farm: 362 | $ref: '#/components/schemas/Farm' 363 | 364 | # SheepWithFarm: 365 | # allOf: 366 | # - $ref: '#/components/schemas/Sheep' 367 | # - type: object 368 | # additionalProperties: false 369 | # properties: 370 | # farm: 371 | # $ref: '#/components/schemas/Farm' 372 | 373 | SheepListResponse: 374 | allOf: 375 | - $ref: '#/components/schemas/ListResponse' 376 | - type: object 377 | properties: 378 | items: 379 | type: array 380 | items: 381 | $ref: '#/components/schemas/SheepWithFarm' 382 | 383 | TokenResponse: 384 | type: object 385 | additionalProperties: false 386 | required: 387 | - id 388 | - token 389 | - userId 390 | properties: 391 | id: 392 | type: string 393 | description: Token's id 394 | token: 395 | type: string 396 | description: Token's value 397 | userId: 398 | type: string 399 | description: User's id 400 | 401 | Error: 402 | type: object 403 | properties: 404 | name: 405 | type: string 406 | enum: 407 | - ValidationError 408 | message: 409 | type: string 410 | 411 | responses: 412 | 400: 413 | description: Bad Request 414 | content: 415 | application/json: 416 | schema: 417 | type: object 418 | additionalProperties: false 419 | required: 420 | - errors 421 | properties: 422 | errors: 423 | type: array 424 | items: 425 | $ref: '#/components/schemas/Error' 426 | 427 | securitySchemes: 428 | BearerAuth: 429 | type: http 430 | scheme: bearer 431 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/or.guard.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from './with-guard.middleware'; 2 | 3 | export const or = (guards: Guard[]): Guard => req => { 4 | return guards.some(guard => guard(req)); 5 | }; 6 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/sheep/add-sheep.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { serializeSheep } from './serialize-sheep'; 3 | import { sheepService } from './sheep.service'; 4 | 5 | export function addSheep(req, res: Response) { 6 | const pictureUri = 7 | req.body.pictureUri && 8 | req.body.pictureUri.replace(/^.*:\/\//, '').replace(req.headers.host, ''); 9 | 10 | const sheep = sheepService.createSheep({ 11 | sheep: { 12 | ...req.body, 13 | pictureUri 14 | } 15 | }); 16 | 17 | res.status(201).json(serializeSheep({ sheep, host: req.headers.host })); 18 | } 19 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/sheep/get-farmer-sheep-list.ts: -------------------------------------------------------------------------------- 1 | import { farmsService } from '../farm/farms.service'; 2 | import { serializeSheep } from './serialize-sheep'; 3 | import { sheepService } from './sheep.service'; 4 | 5 | export function getFarmerSheepList(req, res) { 6 | const farmList = farmsService.getFarmsByFarmerId({ 7 | farmerId: req.params.farmerId 8 | }); 9 | 10 | const farmIdList = farmList.map(farm => farm.id); 11 | 12 | const sheepList = sheepService 13 | .getSheepByFarmIdList({ farmIdList }) 14 | .map(sheep => serializeSheep({ sheep, host: req.headers.host })); 15 | 16 | res.json({ 17 | totalCount: sheepList.length, 18 | items: sheepList 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/sheep/serialize-sheep.ts: -------------------------------------------------------------------------------- 1 | export function serializeSheep({ sheep, host }) { 2 | const pictureUri = sheep.pictureUri && `//${host}${sheep.pictureUri}`; 3 | return { 4 | ...sheep, 5 | pictureUri 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/sheep/sheep.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { isSelf } from '../is-self.guard'; 3 | import { validate } from '../openapi/validator'; 4 | import { withGuard } from '../with-guard.middleware'; 5 | import { addSheep } from './add-sheep'; 6 | import { getFarmerSheepList } from './get-farmer-sheep-list'; 7 | 8 | export const sheepRouter = Router(); 9 | 10 | sheepRouter.get( 11 | '/farmers/:farmerId/sheep', 12 | withGuard(isSelf), 13 | getFarmerSheepList 14 | ); 15 | 16 | sheepRouter.post('/sheep', validate(), addSheep); 17 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/sheep/sheep.service.ts: -------------------------------------------------------------------------------- 1 | import * as shortid from 'shortid'; 2 | import { database } from '../../database'; 3 | 4 | export const sheepService = { 5 | getSheepByFarmIdList({ farmIdList }: { farmIdList: string[] }) { 6 | return database 7 | .get('sheep') 8 | .filter(_sheep => farmIdList.includes(_sheep.farmId)) 9 | .value(); 10 | }, 11 | createSheep({ sheep }) { 12 | sheep = { 13 | id: shortid.generate(), 14 | createdAt: new Date().toISOString(), 15 | ...sheep 16 | }; 17 | 18 | /* Handle both "{farmId: FARM_ID}" and legacy "{farm: {id: FARM_ID}}". */ 19 | const farmId = sheep.farmId || (sheep.farm && sheep.farm.id); 20 | 21 | database 22 | .get('sheep') 23 | .push({ 24 | id: sheep.id, 25 | createdAt: sheep.createdAt, 26 | age: sheep.age, 27 | eyeColor: sheep.eyeColor, 28 | gender: sheep.gender, 29 | name: sheep.name, 30 | pictureUri: sheep.pictureUri, 31 | farmId, 32 | destinations: sheep.destinations 33 | }) 34 | .write(); 35 | 36 | return sheep; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { pbkdf2Sync } from 'crypto'; 2 | import { farmersService } from '../farmer/farmers.service'; 3 | import { 4 | TokenInfo, 5 | TokensService, 6 | tokensService as defaultTokensService 7 | } from './tokens.service'; 8 | 9 | export async function authenticate({ 10 | userId, 11 | password, 12 | tokensService = defaultTokensService 13 | }: { 14 | userId: string; 15 | password: string; 16 | tokensService?: TokensService; 17 | }): Promise { 18 | const farmer = farmersService.getFarmer({ farmerId: userId }); 19 | 20 | const passwordHash = pbkdf2Sync( 21 | password, 22 | userId, 23 | 1000, 24 | 32, 25 | 'sha512' 26 | ).toString('base64'); 27 | 28 | if (farmer == null || farmer.passwordHash !== passwordHash) { 29 | return null; 30 | } 31 | 32 | return tokensService.create({ userId }); 33 | } 34 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/create-token-and-set-cookie.ts: -------------------------------------------------------------------------------- 1 | import { authenticate } from './authenticate'; 2 | 3 | export async function createTokenAndSetCookie(req, res) { 4 | const userId = req.body.userName; 5 | const password = req.body.password; 6 | 7 | const tokenInfo = await authenticate({ userId, password }); 8 | 9 | if (tokenInfo == null) { 10 | res.sendStatus(401); 11 | return; 12 | } 13 | 14 | res.cookie('token', tokenInfo.token, { 15 | secure: true, 16 | sameSite: 'None' 17 | }); 18 | 19 | res.status(201).json({ 20 | id: tokenInfo.id, 21 | userId 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/create-token.ts: -------------------------------------------------------------------------------- 1 | import { authenticate } from './authenticate'; 2 | import { TokensService } from './tokens.service'; 3 | 4 | export const createToken = ({ 5 | tokensService 6 | }: { 7 | tokensService?: TokensService; 8 | } = {}) => async (req, res) => { 9 | const userId = req.body.userName; 10 | const password = req.body.password; 11 | 12 | const tokenInfo = await authenticate({ userId, password, tokensService }); 13 | 14 | if (tokenInfo == null) { 15 | res.sendStatus(401); 16 | return; 17 | } 18 | 19 | res.status(201).json({ 20 | ...tokenInfo, 21 | userId 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/delete-token.ts: -------------------------------------------------------------------------------- 1 | import { tokensService } from './tokens.service'; 2 | 3 | export const deleteToken = (req, res) => { 4 | const { tokenId } = req.params; 5 | 6 | const userId = tokensService.getUserId({ tokenId }); 7 | 8 | /* User should be owner of the token. */ 9 | if (userId !== req['user'].id) { 10 | res.sendStatus(403); 11 | return; 12 | } 13 | 14 | tokensService.delete({ tokenId }); 15 | 16 | res.sendStatus(204); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/tokens.cookie.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { cookieAuthMiddleware } from '../cookie-auth.middleware'; 3 | import { deleteToken } from './delete-token'; 4 | import { createTokenAndSetCookie } from './create-token-and-set-cookie'; 5 | 6 | export const tokensCookieRouter = Router(); 7 | 8 | tokensCookieRouter.post('/tokens', createTokenAndSetCookie); 9 | 10 | tokensCookieRouter.delete( 11 | '/tokens/:tokenId', 12 | cookieAuthMiddleware, 13 | deleteToken 14 | ); 15 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/tokens.jwt.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { jwtAuthMiddleware } from '../jwt-auth.middleware'; 3 | import { createToken } from './create-token'; 4 | import { tokensJwtService } from './tokens.jwt.service'; 5 | 6 | export const tokensJwtRouter = Router(); 7 | 8 | tokensJwtRouter.post( 9 | '/tokens', 10 | createToken({ 11 | tokensService: tokensJwtService 12 | }) 13 | ); 14 | 15 | tokensJwtRouter.delete('/tokens/:tokenId', jwtAuthMiddleware, (req, res) => { 16 | /* @todo add jwt token to blacklist. */ 17 | return res.sendStatus(204); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/tokens.jwt.service.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as shortid from 'shortid'; 3 | import { TokenInfo, TokensService } from './tokens.service'; 4 | 5 | export const tokensJwtService: TokensService = { 6 | async create({ userId }: { userId: string }): Promise { 7 | const tokenId = shortid.generate(); 8 | return { 9 | id: tokenId, 10 | token: jwt.sign( 11 | { 12 | aud: 'https://api.websheep.io', 13 | iss: 'https://auth.websheep.io', 14 | jti: tokenId, 15 | sub: userId 16 | }, 17 | process.env.JWT_SECRET, 18 | { expiresIn: '24h' } 19 | ) 20 | }; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/tokens.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { bearerAuthMiddleware } from '../bearer-auth.middleware'; 3 | import { createToken } from './create-token'; 4 | import { deleteToken } from './delete-token'; 5 | 6 | export const tokensRouter = Router(); 7 | 8 | tokensRouter.post('/tokens', createToken()); 9 | 10 | tokensRouter.delete('/tokens/:tokenId', bearerAuthMiddleware, deleteToken); 11 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/token/tokens.service.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import * as shortid from 'shortid'; 3 | import { promisify } from 'util'; 4 | import { database } from '../../database'; 5 | 6 | export interface TokenInfo { 7 | id: string; 8 | token: string; 9 | } 10 | 11 | export async function generateToken(): Promise { 12 | const tokenBuffer = await promisify(randomBytes)(32); 13 | 14 | return { 15 | id: shortid.generate(), 16 | token: tokenBuffer.toString('base64') 17 | }; 18 | } 19 | 20 | export interface TokensService { 21 | create(args: { userId: string }): Promise; 22 | delete?(args: { tokenId: string }); 23 | getUserId?(args: { tokenId: string }): string; 24 | } 25 | 26 | export const tokensService: TokensService = { 27 | async create({ userId }: { userId: string }): Promise { 28 | const tokenInfo = await generateToken(); 29 | 30 | database 31 | .get('tokens') 32 | .push({ 33 | ...tokenInfo, 34 | userId 35 | }) 36 | .write(); 37 | 38 | return tokenInfo; 39 | }, 40 | delete({ tokenId }: { tokenId: string }) { 41 | database 42 | .get('tokens') 43 | .remove({ id: tokenId }) 44 | .write(); 45 | }, 46 | getUserId({ tokenId }: { tokenId: string }) { 47 | return database 48 | .get('tokens') 49 | .find({ id: tokenId }) 50 | .value().userId; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /apps/websheep-api/src/app/shared/with-guard.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export type Guard = (req: Request) => boolean; 4 | 5 | export const withGuard = (permission: Guard) => (req, res, next) => { 6 | return permission(req) ? next() : res.sendStatus(403); 7 | }; 8 | -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-0.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-1.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-10.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-11.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-12.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-13.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-14.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-15.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-16.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-17.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-18.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-19.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-2.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-20.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-3.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-4.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-5.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-6.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-7.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-8.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/assets/sheep-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep-api/src/assets/sheep-9.jpg -------------------------------------------------------------------------------- /apps/websheep-api/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiServerUrl: 'https://websheep.herokuapp.com', 3 | appOrigin: ['https://websheep.yjaaidi.now.sh', 'https://websheep.vercel.app'], 4 | production: true 5 | }; 6 | -------------------------------------------------------------------------------- /apps/websheep-api/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiServerUrl: 'http://localhost:3333', 3 | appOrigin: 'http://localhost:4200', 4 | production: false 5 | }; 6 | -------------------------------------------------------------------------------- /apps/websheep-api/src/fixtures/farmers.ts: -------------------------------------------------------------------------------- 1 | export const farmers = [ 2 | { 3 | id: 'karinelemarchand', 4 | firstName: 'Karine', 5 | lastName: 'Le Marchand', 6 | /* 123456 */ 7 | passwordHash: 'zmkO/W+ZrHp015/8LbDoQ/3DrRIYagekcf8icPqSBVk=', 8 | isAdmin: false 9 | }, 10 | { 11 | id: 'foobar', 12 | firstName: 'foo', 13 | lastName: 'Bar', 14 | /* zTAr@6y60PwJ */ 15 | passwordHash: 'cm+Gzc9HqStHBB7E92lRZ7jDmuwAPfTQUvrQstGCbvs=', 16 | isAdmin: false 17 | }, 18 | { 19 | id: 'johndoe', 20 | firstName: 'John', 21 | lastName: 'DOE', 22 | /* %yP50h^qI02t */ 23 | passwordHash: '494yx+2TfffQcs1c5xg4BQB8ubAd6bPK56e/eX4mTf0=', 24 | isAdmin: false 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /apps/websheep-api/src/fixtures/farms.ts: -------------------------------------------------------------------------------- 1 | export const farms = [ 2 | { 3 | id: 'P4VU2Xsw', 4 | name: `L'amour est dans le pré`, 5 | farmerIds: ['karinelemarchand'] 6 | }, 7 | { 8 | id: 'ks6GN2am', 9 | name: 'Greenland', 10 | farmerIds: ['foobar'] 11 | }, 12 | { 13 | id: 'wF6aEGTGK', 14 | name: 'Magic land', 15 | farmerIds: ['foobar', 'johndoe'] 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /apps/websheep-api/src/fixtures/sheep-generator.md: -------------------------------------------------------------------------------- 1 | 2 | Use with https://www.json-generator.com/ 3 | 4 | ``` 5 | [ 6 | '{{repeat(20, 20)}}', 7 | { 8 | id: '{{objectId()}}', 9 | createdAt: '2019-10-08T13:39:59.335Z', 10 | age: '{{integer(1, 12)}}', 11 | eyeColor: '{{random("blue", "brown", "green")}}', 12 | gender: '{{gender()}}', 13 | name: '{{firstName()}}', 14 | pictureUri: '/assets/sheep-{{index()}}.jpg', 15 | farmId: function (tags) { 16 | var farms = ['P4VU2Xsw', 'ks6GN2am', 'wF6aEGTGK']; 17 | return farms[tags.integer(0, farms.length - 1)]; 18 | }, 19 | destinations: function (tags) { 20 | var destinations = [['kebab'], ['wool'], ['kebab', 'wool']]; 21 | return destinations[tags.integer(0, destinations.length - 1)]; 22 | } 23 | } 24 | ] 25 | ``` 26 | -------------------------------------------------------------------------------- /apps/websheep-api/src/fixtures/sheep.ts: -------------------------------------------------------------------------------- 1 | export const sheep = [ 2 | { 3 | id: '5da97d2976d1264a41c89785', 4 | createdAt: '2019-10-08T13:39:59.335Z', 5 | age: 7, 6 | eyeColor: 'brown', 7 | gender: 'male', 8 | name: 'Irma', 9 | pictureUri: '/assets/sheep-0.jpg', 10 | farmId: 'P4VU2Xsw', 11 | destinations: ['wool'] 12 | }, 13 | { 14 | id: '5da97d2946b6e6adf2587e17', 15 | createdAt: '2019-10-08T13:39:59.335Z', 16 | age: 2, 17 | eyeColor: 'green', 18 | gender: 'female', 19 | name: 'Adriana', 20 | pictureUri: '/assets/sheep-1.jpg', 21 | farmId: 'wF6aEGTGK', 22 | destinations: ['kebab'] 23 | }, 24 | { 25 | id: '5da97d29aa97e289e1c09529', 26 | createdAt: '2019-10-08T13:39:59.335Z', 27 | age: 4, 28 | eyeColor: 'brown', 29 | gender: 'female', 30 | name: 'Villarreal', 31 | pictureUri: '/assets/sheep-2.jpg', 32 | farmId: 'wF6aEGTGK', 33 | destinations: ['kebab'] 34 | }, 35 | { 36 | id: '5da97d291eccfb79b7c84eb4', 37 | createdAt: '2019-10-08T13:39:59.335Z', 38 | age: 9, 39 | eyeColor: 'blue', 40 | gender: 'male', 41 | name: 'Jacquelyn', 42 | pictureUri: '/assets/sheep-3.jpg', 43 | farmId: 'wF6aEGTGK', 44 | destinations: ['kebab', 'wool'] 45 | }, 46 | { 47 | id: '5da97d29c113b377513466a5', 48 | createdAt: '2019-10-08T13:39:59.335Z', 49 | age: 12, 50 | eyeColor: 'brown', 51 | gender: 'female', 52 | name: 'Karla', 53 | pictureUri: '/assets/sheep-4.jpg', 54 | farmId: 'ks6GN2am', 55 | destinations: ['kebab'] 56 | }, 57 | { 58 | id: '5da97d29b97b87de11ddcbd2', 59 | createdAt: '2019-10-08T13:39:59.335Z', 60 | age: 1, 61 | eyeColor: 'green', 62 | gender: 'female', 63 | name: 'Sheila', 64 | pictureUri: '/assets/sheep-5.jpg', 65 | farmId: 'ks6GN2am', 66 | destinations: ['kebab'] 67 | }, 68 | { 69 | id: '5da97d29f94c3582c65fba2f', 70 | createdAt: '2019-10-08T13:39:59.335Z', 71 | age: 11, 72 | eyeColor: 'brown', 73 | gender: 'female', 74 | name: 'Heather', 75 | pictureUri: '/assets/sheep-6.jpg', 76 | farmId: 'P4VU2Xsw', 77 | destinations: ['wool'] 78 | }, 79 | { 80 | id: '5da97d29940780ec4dbbd6c5', 81 | createdAt: '2019-10-08T13:39:59.335Z', 82 | age: 7, 83 | eyeColor: 'brown', 84 | gender: 'female', 85 | name: 'Black', 86 | pictureUri: '/assets/sheep-7.jpg', 87 | farmId: 'ks6GN2am', 88 | destinations: ['wool'] 89 | }, 90 | { 91 | id: '5da97d29516796a8ac43cbac', 92 | createdAt: '2019-10-08T13:39:59.335Z', 93 | age: 4, 94 | eyeColor: 'blue', 95 | gender: 'male', 96 | name: 'Tracie', 97 | pictureUri: '/assets/sheep-8.jpg', 98 | farmId: 'ks6GN2am', 99 | destinations: ['kebab'] 100 | }, 101 | { 102 | id: '5da97d2916a38f2bacfa867f', 103 | createdAt: '2019-10-08T13:39:59.335Z', 104 | age: 11, 105 | eyeColor: 'brown', 106 | gender: 'female', 107 | name: 'Cochran', 108 | pictureUri: '/assets/sheep-9.jpg', 109 | farmId: 'wF6aEGTGK', 110 | destinations: ['kebab'] 111 | }, 112 | { 113 | id: '5da97d2904c86d536239e9e1', 114 | createdAt: '2019-10-08T13:39:59.335Z', 115 | age: 10, 116 | eyeColor: 'brown', 117 | gender: 'male', 118 | name: 'Acosta', 119 | pictureUri: '/assets/sheep-10.jpg', 120 | farmId: 'P4VU2Xsw', 121 | destinations: ['kebab', 'wool'] 122 | }, 123 | { 124 | id: '5da97d298c1614a067603274', 125 | createdAt: '2019-10-08T13:39:59.335Z', 126 | age: 1, 127 | eyeColor: 'brown', 128 | gender: 'male', 129 | name: 'Abbott', 130 | pictureUri: '/assets/sheep-11.jpg', 131 | farmId: 'ks6GN2am', 132 | destinations: ['kebab', 'wool'] 133 | }, 134 | { 135 | id: '5da97d29b6eca4501936d70d', 136 | createdAt: '2019-10-08T13:39:59.335Z', 137 | age: 8, 138 | eyeColor: 'green', 139 | gender: 'male', 140 | name: 'Rhonda', 141 | pictureUri: '/assets/sheep-12.jpg', 142 | farmId: 'P4VU2Xsw', 143 | destinations: ['kebab'] 144 | }, 145 | { 146 | id: '5da97d2960a6c53b0413be59', 147 | createdAt: '2019-10-08T13:39:59.335Z', 148 | age: 8, 149 | eyeColor: 'green', 150 | gender: 'female', 151 | name: 'Hess', 152 | pictureUri: '/assets/sheep-13.jpg', 153 | farmId: 'ks6GN2am', 154 | destinations: ['kebab', 'wool'] 155 | }, 156 | { 157 | id: '5da97d290870efd6b19daa7d', 158 | createdAt: '2019-10-08T13:39:59.335Z', 159 | age: 8, 160 | eyeColor: 'blue', 161 | gender: 'male', 162 | name: 'Ladonna', 163 | pictureUri: '/assets/sheep-14.jpg', 164 | farmId: 'P4VU2Xsw', 165 | destinations: ['wool'] 166 | }, 167 | { 168 | id: '5da97d2927f268a931bb4529', 169 | createdAt: '2019-10-08T13:39:59.335Z', 170 | age: 10, 171 | eyeColor: 'blue', 172 | gender: 'female', 173 | name: 'Angel', 174 | pictureUri: '/assets/sheep-15.jpg', 175 | farmId: 'ks6GN2am', 176 | destinations: ['kebab', 'wool'] 177 | }, 178 | { 179 | id: '5da97d29d920bd5259c2487d', 180 | createdAt: '2019-10-08T13:39:59.335Z', 181 | age: 7, 182 | eyeColor: 'green', 183 | gender: 'female', 184 | name: 'Mccarty', 185 | pictureUri: '/assets/sheep-16.jpg', 186 | farmId: 'P4VU2Xsw', 187 | destinations: ['kebab', 'wool'] 188 | }, 189 | { 190 | id: '5da97d29a7c85be5ee8b6feb', 191 | createdAt: '2019-10-08T13:39:59.335Z', 192 | age: 4, 193 | eyeColor: 'blue', 194 | gender: 'male', 195 | name: 'Olsen', 196 | pictureUri: '/assets/sheep-17.jpg', 197 | farmId: 'P4VU2Xsw', 198 | destinations: ['kebab', 'wool'] 199 | }, 200 | { 201 | id: '5da97d2997e1f92cf8cf0c35', 202 | createdAt: '2019-10-08T13:39:59.335Z', 203 | age: 3, 204 | eyeColor: 'brown', 205 | gender: 'male', 206 | name: 'Imogene', 207 | pictureUri: '/assets/sheep-18.jpg', 208 | farmId: 'wF6aEGTGK', 209 | destinations: ['wool'] 210 | }, 211 | { 212 | id: '5da97d296e9ffdeeaf84ce50', 213 | createdAt: '2019-10-08T13:39:59.335Z', 214 | age: 8, 215 | eyeColor: 'blue', 216 | gender: 'female', 217 | name: 'Angela', 218 | pictureUri: '/assets/sheep-19.jpg', 219 | farmId: 'wF6aEGTGK', 220 | destinations: ['kebab', 'wool'] 221 | } 222 | ]; 223 | -------------------------------------------------------------------------------- /apps/websheep-api/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | **/ 5 | 6 | import * as express from 'express'; 7 | import * as path from 'path'; 8 | import { authz1Router } from './app/authz1'; 9 | import { authz2Router } from './app/authz2'; 10 | import { csrf1Router } from './app/csrf1'; 11 | import { csrf2Router } from './app/csrf2'; 12 | import { csrf3Router } from './app/csrf3'; 13 | import { resetDatabase } from './app/database'; 14 | import { jwt1Router } from './app/jwt1'; 15 | import { jwt2Router } from './app/jwt2'; 16 | 17 | process.env.JWT_SECRET = 'MY_AWESOME_UNIQUE_JWT_SECRET'; 18 | 19 | const app = express(); 20 | 21 | resetDatabase(); 22 | 23 | app.use('/authz1', authz1Router); 24 | app.use('/authz2', authz2Router); 25 | app.use('/csrf1', csrf1Router); 26 | app.use('/csrf2', csrf2Router); 27 | app.use('/csrf3', csrf3Router); 28 | app.use('/jwt1', jwt1Router); 29 | app.use('/jwt2', jwt2Router); 30 | 31 | app.get('/', (req, res) => res.redirect('/authz1')); 32 | app.use('/assets', express.static(path.join(__dirname, 'assets'))); 33 | 34 | /* Error handler. */ 35 | app.use((err, req, res, next) => { 36 | res.status(err.status || 500).json({ 37 | errors: err.errors 38 | }); 39 | }); 40 | 41 | const port = process.env.PORT || 3333; 42 | const server = app.listen(port, () => { 43 | console.log(`Listening at http://localhost:${port}`); 44 | }); 45 | server.on('error', console.error); 46 | -------------------------------------------------------------------------------- /apps/websheep-api/src/testing/test-pact-interaction.ts: -------------------------------------------------------------------------------- 1 | import { Express } from 'express'; 2 | import * as jp from 'jsonpath'; 3 | import * as request from 'supertest'; 4 | 5 | export async function testPactInteraction({ 6 | app, 7 | interaction 8 | }: { 9 | app: Express; 10 | interaction; 11 | }) { 12 | 13 | const res = await request(app) 14 | [interaction.request.method.toLowerCase()](interaction.request.path) 15 | .set(interaction.request.headers || {}); 16 | 17 | /* Use simple object type for jsonpath to work. */ 18 | const response = { 19 | body: res.body, 20 | status: res.status, 21 | headers: res.headers 22 | }; 23 | 24 | expect(response.status).toEqual(interaction.response.status); 25 | expect(response.headers).toEqual( 26 | expect.objectContaining(interaction.response.headers) 27 | ); 28 | jp.nodes(interaction.response.body, '$..*') 29 | /* Ignore objects and keep leaves. */ 30 | .filter(({ value }) => typeof value !== 'object') 31 | .forEach(({ path, value }) => { 32 | /* Add `body` level. */ 33 | path = ['$', 'body', ...path.slice(1)]; 34 | const pathStr = path.join('.').replace(/\.(\d+)/g, '[$1]'); 35 | const matchingRule = interaction.response.matchingRules[pathStr]; 36 | switch (matchingRule ? matchingRule.match : null) { 37 | case 'type': 38 | expect({ 39 | /* Add path for logs. */ 40 | path: pathStr, 41 | value: typeof jp.query(response, pathStr)[0] 42 | }).toEqual({ path: pathStr, value: typeof value }); 43 | break; 44 | case 'regex': 45 | expect({ 46 | /* Add path for logs. */ 47 | path: pathStr, 48 | value: value.match(matchingRule.regex) != null 49 | }).toEqual({ path: pathStr, value: true }); 50 | break; 51 | default: 52 | expect({ 53 | /* Add path for logs. */ 54 | path: pathStr, 55 | value: jp.query(response, pathStr)[0] 56 | }).toEqual({ path: pathStr, value }); 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /apps/websheep-api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts", "src/testing/*.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/websheep-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest", "express"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "resolveJsonModule": true 8 | }, 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/websheep-api/tslint.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../tslint.json", "rules": [] } 2 | -------------------------------------------------------------------------------- /apps/websheep-api/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | return { 3 | ...config, 4 | module: { 5 | ...config.module, 6 | rules: [...config.module.rules, { 7 | test: /\.ya?ml$/, 8 | loader: 'raw-loader' 9 | }] 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /apps/websheep-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "pluginsFile": "./src/plugins/index", 6 | "supportFile": false, 7 | "video": true, 8 | "videosFolder": "../../dist/cypress/apps/websheep-e2e/videos", 9 | "screenshotsFolder": "../../dist/cypress/apps/websheep-e2e/screenshots", 10 | "chromeWebSecurity": false 11 | } 12 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('websheep', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | getGreeting().contains('Welcome to websheep!'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /apps/websheep-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/websheep-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc" 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/websheep-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["cypress", "node"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep-e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../tslint.json", "rules": [] } 2 | -------------------------------------------------------------------------------- /apps/websheep/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /apps/websheep/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'websheep', 3 | preset: '../../jest.config.js', 4 | coverageDirectory: '../../coverage/apps/websheep', 5 | snapshotSerializers: [ 6 | 'jest-preset-angular/AngularSnapshotSerializer.js', 7 | 'jest-preset-angular/HTMLCommentSerializer.js' 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app-route-helper.ts: -------------------------------------------------------------------------------- 1 | export const appRouteHelper = { 2 | SIGNIN_PATH: 'signin', 3 | signinRoute() { 4 | return ['/', this.SIGNIN_PATH]; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { appRouteHelper } from './app-route-helper'; 4 | import { IsNotSignedInGuard } from './auth/is-not-signed-in.guard'; 5 | import { IsSignedInGuard } from './auth/is-signed-in.guard'; 6 | import { 7 | SigninFormComponent, 8 | LoginModule 9 | } from './signin/signin-form.component'; 10 | import { sheepRouteHelper } from './views/sheep/sheep-route-helper'; 11 | 12 | export const appRoutes: Routes = [ 13 | { 14 | path: appRouteHelper.SIGNIN_PATH, 15 | canActivate: [IsNotSignedInGuard], 16 | component: SigninFormComponent 17 | }, 18 | { 19 | path: sheepRouteHelper.BASE_PATH, 20 | canActivate: [IsSignedInGuard], 21 | loadChildren: () => 22 | import('./views/sheep/sheep-views.module').then(m => m.SheepViewsModule) 23 | }, 24 | { 25 | path: '**', 26 | redirectTo: sheepRouteHelper.sheepListRoute().join('/') 27 | } 28 | ]; 29 | 30 | @NgModule({ 31 | declarations: [], 32 | imports: [LoginModule, RouterModule.forRoot(appRoutes)], 33 | exports: [RouterModule] 34 | }) 35 | export class AppRoutingModule {} 36 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | Menu 13 | 14 | power_off 15 | 16 | 17 | 18 | Add Sheep 19 | My Sheep 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .ws-sidenav__link--active { 3 | color: blueviolet; 4 | } 5 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Signout } from './auth/signout'; 6 | import { getApiBaseUrl } from './config/config.selectors'; 7 | import { FarmerService } from './farmer/farmer.service'; 8 | import { hideHackAssistant, showHackAssistant } from './layout/layout.actions'; 9 | import { getIsHackAssistantOpen } from './layout/layout.selectors'; 10 | import { AppState } from './reducers'; 11 | import { sheepRouteHelper } from './views/sheep/sheep-route-helper'; 12 | 13 | @Component({ 14 | selector: 'ws-root', 15 | templateUrl: './app.component.html', 16 | styleUrls: ['./app.component.scss'] 17 | }) 18 | export class AppComponent { 19 | isHackAssistantOpen$ = this._store.select(getIsHackAssistantOpen); 20 | sheepRouteHelper = sheepRouteHelper; 21 | 22 | constructor( 23 | private _farmerService: FarmerService, 24 | private _store: Store, 25 | private _signout: Signout 26 | ) {} 27 | 28 | signOut() { 29 | this._signout.signOut().subscribe(); 30 | } 31 | 32 | setIsHackAssistantOpen(isHackAssistantOpen: boolean) { 33 | this._store.dispatch( 34 | isHackAssistantOpen ? showHackAssistant() : hideHackAssistant() 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/websheep/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { 5 | MatIconModule, 6 | MatListModule, 7 | MatSidenavModule, 8 | MatToolbarModule 9 | } from '@angular/material'; 10 | import { BrowserModule } from '@angular/platform-browser'; 11 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 12 | import { EffectsModule } from '@ngrx/effects'; 13 | import { StoreModule } from '@ngrx/store'; 14 | import { AppRoutingModule } from './app-routing.module'; 15 | 16 | import { AppComponent } from './app.component'; 17 | import { HackAssistantModule } from './assistant/assistant/assistant.component'; 18 | import { AuthEffects } from './auth/auth.effects'; 19 | import { AuthInterceptor } from './http/auth.interceptor'; 20 | import { PrependBaseUrlInterceptor } from './http/prepend-base-url.interceptor'; 21 | import { NavModule } from './nav/nav.component'; 22 | import { metaReducers, reducers } from './reducers'; 23 | import { ToolbarModule } from './toolbar/toolbar.component'; 24 | import { HttpInterceptorsModule } from './http/http-interceptors.module'; 25 | 26 | @NgModule({ 27 | declarations: [AppComponent], 28 | imports: [ 29 | AppRoutingModule, 30 | BrowserModule, 31 | BrowserAnimationsModule, 32 | EffectsModule.forRoot([AuthEffects]), 33 | HttpClientModule, 34 | HttpInterceptorsModule.forRoot(), 35 | NavModule, 36 | StoreModule.forRoot(reducers, { 37 | metaReducers, 38 | runtimeChecks: { 39 | strictStateImmutability: true, 40 | strictActionImmutability: true 41 | } 42 | }), 43 | MatListModule, 44 | MatToolbarModule, 45 | MatIconModule, 46 | FlexModule, 47 | MatSidenavModule, 48 | HackAssistantModule, 49 | ToolbarModule 50 | ], 51 | bootstrap: [AppComponent] 52 | }) 53 | export class AppModule {} 54 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector-form/api-selector-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 | 9 | close 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector-form/api-selector-form.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | font-size: .8em; 3 | } 4 | 5 | .ws-api-selector-form__close { 6 | cursor: pointer; 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector-form/api-selector-form.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | Component, 4 | EventEmitter, 5 | NgModule, 6 | OnDestroy, 7 | OnInit, 8 | Output 9 | } from '@angular/core'; 10 | import { FlexModule } from '@angular/flex-layout'; 11 | import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; 12 | import { 13 | MatFormFieldModule, 14 | MatIconModule, 15 | MatInputModule, 16 | MatRadioModule 17 | } from '@angular/material'; 18 | import { Store } from '@ngrx/store'; 19 | import { Scavenger } from '@wishtack/rx-scavenger'; 20 | import { combineLatest } from 'rxjs'; 21 | import { 22 | selectApiBasePath, 23 | selectApiServerUrl 24 | } from '../../config/config.actions'; 25 | import * as fromConfig from '../../config/config.selectors'; 26 | import { AppState } from '../../reducers'; 27 | 28 | @Component({ 29 | selector: 'ws-api-selector-form', 30 | templateUrl: './api-selector-form.component.html', 31 | styleUrls: ['./api-selector-form.component.scss'] 32 | }) 33 | export class ApiSelectorFormComponent implements OnDestroy, OnInit { 34 | @Output() done = new EventEmitter(); 35 | 36 | apiServerUrl$ = this._store.select(fromConfig.getApiServerUrl); 37 | 38 | apiBaseUrlForm = new FormGroup({ 39 | apiServerUrl: new FormControl() 40 | }); 41 | 42 | private _scavenger = new Scavenger(this); 43 | 44 | constructor(private _store: Store) {} 45 | 46 | ngOnInit() { 47 | this.apiServerUrl$ 48 | .pipe(this._scavenger.collect()) 49 | .subscribe(apiServerUrl => { 50 | this.apiBaseUrlForm.patchValue({ 51 | apiServerUrl 52 | }); 53 | }); 54 | 55 | this.apiBaseUrlForm 56 | .get('apiServerUrl') 57 | .valueChanges.pipe(this._scavenger.collect()) 58 | .subscribe(apiServerUrl => { 59 | this._store.dispatch(selectApiServerUrl({ apiServerUrl })); 60 | }); 61 | } 62 | 63 | ngOnDestroy() {} 64 | 65 | close() { 66 | this.done.emit(); 67 | } 68 | } 69 | 70 | @NgModule({ 71 | declarations: [ApiSelectorFormComponent], 72 | imports: [ 73 | CommonModule, 74 | ReactiveFormsModule, 75 | MatFormFieldModule, 76 | MatInputModule, 77 | MatRadioModule, 78 | MatIconModule, 79 | FlexModule 80 | ], 81 | exports: [ApiSelectorFormComponent] 82 | }) 83 | export class ApiSelectorFormModule {} 84 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector/api-selector.component.html: -------------------------------------------------------------------------------- 1 |
5 | Server:  6 | {{ apiServerUrl$ | async }} 7 | edit 11 | 12 |
13 | 14 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector/api-selector.component.scss: -------------------------------------------------------------------------------- 1 | .ws-api-selector__edit { 2 | cursor: pointer; 3 | } 4 | 5 | .ws-api-selector__edit-button { 6 | font-size: .8em; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/api-selector/api-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, NgModule } from '@angular/core'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { MatIconModule } from '@angular/material'; 5 | import { Store } from '@ngrx/store'; 6 | import { getApiServerUrl } from '../../config/config.selectors'; 7 | import { AppState } from '../../reducers'; 8 | import { ApiSelectorFormModule } from '../api-selector-form/api-selector-form.component'; 9 | 10 | @Component({ 11 | selector: 'ws-api-selector', 12 | templateUrl: './api-selector.component.html', 13 | styleUrls: ['./api-selector.component.scss'] 14 | }) 15 | export class ApiSelectorComponent { 16 | apiServerUrl$ = this._store.select(getApiServerUrl); 17 | isEditing = false; 18 | 19 | constructor(private _store: Store) {} 20 | 21 | edit() { 22 | this.isEditing = true; 23 | } 24 | 25 | stopEditing() { 26 | this.isEditing = false; 27 | } 28 | } 29 | 30 | @NgModule({ 31 | declarations: [ApiSelectorComponent], 32 | imports: [CommonModule, MatIconModule, FlexModule, ApiSelectorFormModule], 33 | exports: [ApiSelectorComponent] 34 | }) 35 | export class ApiSelectorModule {} 36 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | import { Mission } from './mission'; 3 | import { Topic } from './topic'; 4 | 5 | export const selectTopic = createAction( 6 | '[Assistant] Select topic', 7 | props<{ 8 | topic: Topic; 9 | }>() 10 | ); 11 | 12 | export const selectMission = createAction( 13 | '[Assistant] Select mission', 14 | props<{ 15 | mission: Mission; 16 | }>() 17 | ); 18 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | import { selectMission, selectTopic } from './assistant.actions'; 3 | import { Mission } from './mission'; 4 | import { Topic } from './topic'; 5 | 6 | export const assistantFeatureKey = 'assistant'; 7 | 8 | export interface AssistantState { 9 | mission: Mission; 10 | topic: Topic; 11 | } 12 | 13 | export const initialState: AssistantState = { 14 | mission: null, 15 | topic: null 16 | }; 17 | 18 | const _assistantReducer = createReducer( 19 | initialState, 20 | on(selectMission, (state, { mission }) => ({ ...state, mission })), 21 | on(selectTopic, (state, { topic }) => ({ ...state, topic, mission: null })) 22 | ); 23 | 24 | export function assistantReducer( 25 | state: AssistantState, 26 | action: Action 27 | ): AssistantState { 28 | return _assistantReducer(state, action); 29 | } 30 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { assistantFeatureKey, AssistantState } from './assistant.reducer'; 3 | 4 | export const getAssistant = createFeatureSelector( 5 | assistantFeatureKey 6 | ); 7 | 8 | export const getMission = createSelector( 9 | getAssistant, 10 | assistant => assistant.mission 11 | ); 12 | 13 | export const getMissionId = createSelector( 14 | getMission, 15 | mission => mission && mission.id 16 | ); 17 | 18 | export const getTopic = createSelector( 19 | getAssistant, 20 | assistant => assistant.topic 21 | ); 22 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant/assistant.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 10 | 11 | 12 | 19 | 20 | 23 | 24 |
25 | 26 | 27 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant/assistant.component.scss: -------------------------------------------------------------------------------- 1 | .ws-assistant__footer { 2 | color: #888; 3 | margin: 10px; 4 | } 5 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/assistant/assistant.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, NgModule } from '@angular/core'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { MatDividerModule } from '@angular/material'; 5 | import { Store } from '@ngrx/store'; 6 | import { Observable } from 'rxjs'; 7 | import { map } from 'rxjs/operators'; 8 | import { 9 | HackTopicSelectorModule, 10 | IdAndLabel 11 | } from '../../../lib/item-selector/item-selector.component'; 12 | import { getApiBasePath, getApiBaseUrl } from '../../config/config.selectors'; 13 | import { AppState } from '../../reducers'; 14 | import { ApiSelectorModule } from '../api-selector/api-selector.component'; 15 | import { selectMission, selectTopic } from '../assistant.actions'; 16 | import { getMission, getMissionId, getTopic } from '../assistant.selectors'; 17 | import { Mission } from '../mission'; 18 | import { MissionInfoModule } from '../mission-info/mission-info.component'; 19 | import { missionList } from '../mission-list'; 20 | import { Topic } from '../topic'; 21 | 22 | @Component({ 23 | selector: 'ws-assistant', 24 | templateUrl: './assistant.component.html', 25 | styleUrls: ['./assistant.component.scss'] 26 | }) 27 | export class AssistantComponent { 28 | topicAndLabelList: IdAndLabel[] = [ 29 | { 30 | id: Topic.BrokenAccessControl, 31 | label: 'Broken Access Control' 32 | }, 33 | { 34 | id: Topic.Csrf, 35 | label: 'C.S.R.F.' 36 | }, 37 | { 38 | id: Topic.Jwt, 39 | label: 'J.W.T.' 40 | } 41 | ]; 42 | 43 | missionList: Mission[] = missionList; 44 | 45 | missionIdAndLabelList$: Observable; 46 | topic$ = this._store.select(getTopic); 47 | mission$ = this._store.select(getMission); 48 | apiBaseUrl$ = this._store.select(getApiBaseUrl); 49 | missionId$ = this._store.select(getMissionId); 50 | 51 | constructor(private _store: Store) { 52 | this.missionIdAndLabelList$ = this.topic$.pipe( 53 | map(topic => { 54 | return this.missionList 55 | .filter(mission => mission.topic === topic) 56 | .map(mission => ({ 57 | label: mission.title, 58 | id: mission.id 59 | })); 60 | }) 61 | ); 62 | } 63 | 64 | selectTopic(topicId: string) { 65 | this._store.dispatch(selectTopic({ topic: topicId as Topic })); 66 | } 67 | 68 | selectMissionById(missionId: string) { 69 | const mission = this.missionList.find(({ id }) => id === missionId); 70 | this._store.dispatch(selectMission({ mission })); 71 | } 72 | } 73 | 74 | @NgModule({ 75 | declarations: [AssistantComponent], 76 | imports: [ 77 | CommonModule, 78 | HackTopicSelectorModule, 79 | MatDividerModule, 80 | FlexModule, 81 | ApiSelectorModule, 82 | MissionInfoModule 83 | ], 84 | exports: [AssistantComponent] 85 | }) 86 | export class HackAssistantModule {} 87 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/mission-info/mission-info.component.html: -------------------------------------------------------------------------------- 1 | 2 | Goals 3 | 4 | 5 |
    6 |
  • 🎯 {{ goal }}
  • 9 |
10 | 11 | 12 | Hints 13 | 14 | 15 |
    16 |
  • 💡 {{ hint }}
  • 19 |
20 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/mission-info/mission-info.component.scss: -------------------------------------------------------------------------------- 1 | .ws-mission-info__goal { 2 | list-style: none; 3 | } 4 | 5 | .ws-mission-info__hint { 6 | list-style: none; 7 | 8 | &:not(:hover) { 9 | background-color: black; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/mission-info/mission-info.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, NgModule } from '@angular/core'; 3 | import { MatToolbarModule } from '@angular/material'; 4 | import { Mission } from '../mission'; 5 | 6 | @Component({ 7 | selector: 'ws-mission-info', 8 | templateUrl: './mission-info.component.html', 9 | styleUrls: ['./mission-info.component.scss'] 10 | }) 11 | export class MissionInfoComponent { 12 | @Input() mission: Mission; 13 | } 14 | 15 | @NgModule({ 16 | declarations: [MissionInfoComponent], 17 | imports: [CommonModule, MatToolbarModule], 18 | exports: [MissionInfoComponent] 19 | }) 20 | export class MissionInfoModule {} 21 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/mission-list.ts: -------------------------------------------------------------------------------- 1 | import { createMission, Mission } from './mission'; 2 | import { Topic } from './topic'; 3 | 4 | export const missionList: Mission[] = [ 5 | createMission({ 6 | id: 'authz1', 7 | title: 'Catch a sheep herd 1', 8 | topic: Topic.BrokenAccessControl, 9 | goals: [`Grab the names of Foo Bar's sheep`], 10 | hints: [`Foo Bar's user id is "foobar"`], 11 | config: { 12 | apiBasePath: 'authz1' 13 | } 14 | }), 15 | createMission({ 16 | id: 'authz2', 17 | title: 'Catch a sheep herd 2', 18 | topic: Topic.BrokenAccessControl, 19 | goals: [`Grab the names of Foo Bar's sheep`], 20 | hints: [ 21 | `Admins can see all sheep`, 22 | `Analyse API responses body`, 23 | `Check this route: /farmers/:farmerId` 24 | ], 25 | config: { 26 | apiBasePath: 'authz2' 27 | } 28 | }), 29 | createMission({ 30 | id: 'csrf1', 31 | title: 'Sheep Stalker', 32 | topic: Topic.Csrf, 33 | goals: [`Steal karine's sheep from another origin`], 34 | hints: [ 35 | `Analyse API responses headers`, 36 | `Check this route: /farmers/:farmerId/sheep` 37 | ], 38 | config: { 39 | apiBasePath: 'csrf1', 40 | includeCredentials: true 41 | } 42 | }), 43 | createMission({ 44 | id: 'csrf2', 45 | title: 'A sheep named Wolf', 46 | topic: Topic.Csrf, 47 | goals: [`Add a sheep named Wolf to karine's farm`], 48 | hints: [ 49 | `The API might consume media types other than application/json`, 50 | `Try a standard media type`, 51 | `application/x-www-form-urlencoded`, 52 | `You might need to use extended query params: "farm[id]=FARM_ID"` 53 | ], 54 | config: { 55 | apiBasePath: 'csrf2', 56 | includeCredentials: true 57 | } 58 | }), 59 | createMission({ 60 | id: 'csrf3', 61 | title: 'A sheep named Bear', 62 | topic: Topic.Csrf, 63 | goals: [`Add a sheep named Bear to karine's farm`], 64 | hints: [ 65 | `The API might not care about the given content-type header`, 66 | `Try a standard media type`, 67 | `application/x-www-form-urlencoded` 68 | ], 69 | config: { 70 | apiBasePath: 'csrf3', 71 | includeCredentials: true 72 | } 73 | }), 74 | createMission({ 75 | id: 'jwt1', 76 | title: 'Le Petit Chaperon Rouge', 77 | topic: Topic.Jwt, 78 | goals: [`Authenticate as foobar without his password`], 79 | hints: [ 80 | `Check the token in the local storage`, 81 | `Try to forge a similar token`, 82 | `Don't forget to update the userId in the local storage` 83 | ], 84 | config: { 85 | apiBasePath: 'jwt1' 86 | } 87 | }), 88 | createMission({ 89 | id: 'jwt2', 90 | title: 'Mission Impossible', 91 | topic: Topic.Jwt, 92 | goals: [`Authenticate as foobar without his password`], 93 | hints: [ 94 | `Try to add a sheep with invalid data and check error response`, 95 | `Try to forge a token for foobar`, 96 | `Don't forget to update the userId in the local storage` 97 | ], 98 | config: { 99 | apiBasePath: 'jwt2' 100 | } 101 | }) 102 | ]; 103 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/mission.ts: -------------------------------------------------------------------------------- 1 | import { Topic } from './topic'; 2 | 3 | export interface MissionConfig { 4 | apiBasePath: string; 5 | includeCredentials?: boolean; 6 | } 7 | 8 | export interface Mission { 9 | id: string; 10 | title: string; 11 | topic: Topic; 12 | goals?: string[]; 13 | hints?: string[]; 14 | config: MissionConfig; 15 | } 16 | 17 | export function createMission(mission: Mission): Mission { 18 | return { 19 | goals: [], 20 | hints: [], 21 | ...mission, 22 | config: { 23 | /* Default configuration is without including credentials. */ 24 | includeCredentials: false, 25 | ...mission.config 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /apps/websheep/src/app/assistant/topic.ts: -------------------------------------------------------------------------------- 1 | export enum Topic { 2 | BrokenAccessControl = 'broken-access-control', 3 | Csrf = 'csrf', 4 | Jwt = 'jwt' 5 | } 6 | -------------------------------------------------------------------------------- /apps/websheep/src/app/auth/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Actions, createEffect, ofType } from '@ngrx/effects'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { appRouteHelper } from '../app-route-helper'; 6 | import { signout } from '../user/user.actions'; 7 | 8 | @Injectable() 9 | export class AuthEffects { 10 | redirectToSigninForm$ = createEffect( 11 | () => 12 | this._actions$.pipe( 13 | ofType(signout), 14 | switchMap(() => this._router.navigate(appRouteHelper.signinRoute())) 15 | ), 16 | { dispatch: false } 17 | ); 18 | 19 | constructor(private _actions$: Actions, private _router: Router) {} 20 | } 21 | -------------------------------------------------------------------------------- /apps/websheep/src/app/auth/is-not-signed-in.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | RouterStateSnapshot 6 | } from '@angular/router'; 7 | import { Store } from '@ngrx/store'; 8 | import { map } from 'rxjs/operators'; 9 | import { AppState } from '../reducers'; 10 | import * as fromUser from '../user/user.selectors'; 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class IsNotSignedInGuard implements CanActivate { 16 | constructor(private _store: Store) {} 17 | 18 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 19 | return this._store 20 | .select(fromUser.getIsSignedIn) 21 | .pipe(map(isSignedIn => !isSignedIn)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/websheep/src/app/auth/is-signed-in.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | Router, 6 | RouterStateSnapshot 7 | } from '@angular/router'; 8 | import { Store } from '@ngrx/store'; 9 | import { map } from 'rxjs/operators'; 10 | import { appRouteHelper } from '../app-route-helper'; 11 | import { AppState } from '../reducers'; 12 | import * as fromUser from '../user/user.selectors'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class IsSignedInGuard implements CanActivate { 18 | constructor(private _router: Router, private _store: Store) {} 19 | 20 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 21 | return this._store.select(fromUser.getIsSignedIn).pipe( 22 | map(isSignedIn => { 23 | return isSignedIn 24 | ? true 25 | : this._router.createUrlTree(appRouteHelper.signinRoute()); 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/websheep/src/app/auth/signout.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { first, switchMap, tap } from 'rxjs/operators'; 5 | import { AppState } from '../reducers'; 6 | import { signout } from '../user/user.actions'; 7 | import * as fromUser from '../user/user.selectors'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class Signout { 13 | constructor( 14 | private _httpClient: HttpClient, 15 | private _store: Store 16 | ) {} 17 | 18 | signOut() { 19 | return this._store.select(fromUser.getTokenId).pipe( 20 | first(), 21 | switchMap(tokenId => this._httpClient.delete(`/tokens/${tokenId}`)), 22 | tap(() => this._store.dispatch(signout())) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/websheep/src/app/config/config.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | import { ConfigState } from './config.reducer'; 3 | 4 | export const selectApiServerUrl = createAction( 5 | '[Config] Select API server URL', 6 | props<{ 7 | apiServerUrl: string; 8 | }>() 9 | ); 10 | 11 | export const selectApiBasePath = createAction( 12 | '[Config] Select API base path', 13 | props<{ 14 | apiBasePath: string; 15 | }>() 16 | ); 17 | 18 | export const applyConfig = createAction( 19 | '[Config] Apply config', 20 | props<{ config: Partial }>() 21 | ); 22 | -------------------------------------------------------------------------------- /apps/websheep/src/app/config/config.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | import { environment } from '../../environments/environment'; 3 | import { selectMission } from '../assistant/assistant.actions'; 4 | import { selectApiBasePath, selectApiServerUrl } from './config.actions'; 5 | 6 | export const configFeatureKey = 'config'; 7 | 8 | export interface ConfigState { 9 | apiBasePath: string; 10 | apiServerUrl: string; 11 | includeCredentials: boolean; 12 | } 13 | 14 | export const initialState: ConfigState = { 15 | apiBasePath: 'authz1', 16 | apiServerUrl: environment.defaultApiServerUrl, 17 | includeCredentials: false 18 | }; 19 | 20 | const _configReducer = createReducer( 21 | initialState, 22 | on(selectApiBasePath, (state, { apiBasePath }) => ({ 23 | ...state, 24 | apiBasePath 25 | })), 26 | on(selectApiServerUrl, (state, { apiServerUrl }) => ({ 27 | ...state, 28 | apiServerUrl 29 | })), 30 | on(selectMission, (state, { mission }) => ({ 31 | ...state, 32 | ...mission.config 33 | })) 34 | ); 35 | 36 | export function configReducer(state: ConfigState, action: Action): ConfigState { 37 | return _configReducer(state, action); 38 | } 39 | -------------------------------------------------------------------------------- /apps/websheep/src/app/config/config.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { urlJoin } from '../../lib/url-join'; 3 | import { configFeatureKey, ConfigState } from './config.reducer'; 4 | 5 | export const getConfig = createFeatureSelector(configFeatureKey); 6 | 7 | export const getApiBasePath = createSelector( 8 | getConfig, 9 | config => config.apiBasePath 10 | ); 11 | 12 | export const getApiServerUrl = createSelector( 13 | getConfig, 14 | config => config.apiServerUrl 15 | ); 16 | 17 | export const getApiBaseUrl = createSelector( 18 | getApiServerUrl, 19 | getApiBasePath, 20 | (serverUrl, basePath) => urlJoin([serverUrl, basePath]) 21 | ); 22 | 23 | export const getIncludeCredentials = createSelector( 24 | getConfig, 25 | config => config.includeCredentials 26 | ); 27 | -------------------------------------------------------------------------------- /apps/websheep/src/app/farmer/farmer.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable, of } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { AppState } from '../reducers'; 7 | 8 | import * as fromUser from '../user/user.selectors'; 9 | 10 | export interface Farmer { 11 | id: string; 12 | firstName: string; 13 | lastName: string; 14 | } 15 | 16 | @Injectable({ 17 | providedIn: 'root' 18 | }) 19 | export class FarmerService { 20 | currentFarmer$: Observable = this._store 21 | .select(fromUser.getUserId) 22 | .pipe( 23 | switchMap(userId => { 24 | if (userId == null) { 25 | return of(null); 26 | } 27 | return this._httpClient.get(`/farmers/${userId}`); 28 | }) 29 | ); 30 | 31 | constructor( 32 | private _httpClient: HttpClient, 33 | private _store: Store 34 | ) {} 35 | } 36 | -------------------------------------------------------------------------------- /apps/websheep/src/app/http/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpErrorResponse, 3 | HttpEvent, 4 | HttpHandler, 5 | HttpInterceptor, 6 | HttpRequest 7 | } from '@angular/common/http'; 8 | import { Injectable } from '@angular/core'; 9 | import { Store } from '@ngrx/store'; 10 | import { combineLatest, Observable, of, throwError } from 'rxjs'; 11 | import { Signout } from '../auth/signout'; 12 | import { getIncludeCredentials } from '../config/config.selectors'; 13 | import { AppState } from '../reducers'; 14 | import * as fromConfig from '../config/config.selectors'; 15 | import { signout } from '../user/user.actions'; 16 | import * as fromUser from '../user/user.selectors'; 17 | import { catchError, first, map, switchMap, tap } from 'rxjs/operators'; 18 | 19 | @Injectable({ 20 | providedIn: 'root' 21 | }) 22 | export class AuthInterceptor implements HttpInterceptor { 23 | constructor(private _signout: Signout, private _store: Store) {} 24 | 25 | intercept(req: HttpRequest, next: HttpHandler) { 26 | return combineLatest([ 27 | this._store.select(fromConfig.getApiBaseUrl).pipe(first()), 28 | this._store.select(fromUser.getToken).pipe(first()), 29 | this._store.select(getIncludeCredentials).pipe(first()) 30 | ]).pipe( 31 | switchMap(([apiBaseUrl, token, includeCredentials]) => { 32 | if (!req.url.startsWith(apiBaseUrl)) { 33 | return of(req); 34 | } 35 | 36 | if (includeCredentials) { 37 | req = req.clone({ 38 | withCredentials: includeCredentials 39 | }); 40 | } 41 | 42 | if (!token) { 43 | return of(req); 44 | } 45 | 46 | return of( 47 | req.clone({ 48 | setHeaders: { 49 | authorization: `Bearer ${token}` 50 | } 51 | }) 52 | ); 53 | }), 54 | switchMap(_req => next.handle(_req)), 55 | catchError(error => { 56 | if (error instanceof HttpErrorResponse && error.status === 401) { 57 | this._store.dispatch(signout()); 58 | } 59 | return throwError(error); 60 | }) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/websheep/src/app/http/http-interceptors.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { PrependBaseUrlInterceptor } from './prepend-base-url.interceptor'; 4 | import { AuthInterceptor } from './auth.interceptor'; 5 | 6 | @NgModule() 7 | export class HttpInterceptorsModule { 8 | static forRoot(): ModuleWithProviders { 9 | return { 10 | ngModule: HttpInterceptorsModule, 11 | providers: [ 12 | { 13 | provide: HTTP_INTERCEPTORS, 14 | useClass: PrependBaseUrlInterceptor, 15 | multi: true 16 | }, 17 | { 18 | provide: HTTP_INTERCEPTORS, 19 | useClass: AuthInterceptor, 20 | multi: true 21 | } 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /apps/websheep/src/app/http/list-response.ts: -------------------------------------------------------------------------------- 1 | export interface ListResponse { 2 | totalCount?: number; 3 | items: T[]; 4 | } 5 | -------------------------------------------------------------------------------- /apps/websheep/src/app/http/prepend-base-url.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpHandler, 3 | HttpInterceptor, 4 | HttpRequest 5 | } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Store } from '@ngrx/store'; 8 | import { of } from 'rxjs'; 9 | import { first, map, switchMap } from 'rxjs/operators'; 10 | import * as fromConfig from '../config/config.selectors'; 11 | import { urlJoin } from '../../lib/url-join'; 12 | import { AppState } from '../reducers'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class PrependBaseUrlInterceptor implements HttpInterceptor { 18 | constructor(private _store: Store) {} 19 | 20 | intercept(req: HttpRequest, next: HttpHandler) { 21 | const url$ = of(req.url).pipe( 22 | switchMap(url => { 23 | if (!url.startsWith('/')) { 24 | return of(url); 25 | } 26 | 27 | const apiBaseUrl$ = this._store.select(fromConfig.getApiBaseUrl); 28 | 29 | return apiBaseUrl$.pipe( 30 | first(), 31 | map(apiBaseUrl => urlJoin([apiBaseUrl, url])) 32 | ); 33 | }) 34 | ); 35 | 36 | return url$.pipe( 37 | switchMap(url => { 38 | return next.handle(req.clone({ url })); 39 | }) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/websheep/src/app/layout/layout.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@ngrx/store'; 2 | 3 | export const showHackAssistant = createAction('[Layout] Show hack assistant'); 4 | 5 | export const hideHackAssistant = createAction('[Layout] Hide hack assistant'); 6 | -------------------------------------------------------------------------------- /apps/websheep/src/app/layout/layout.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | import { hideHackAssistant, showHackAssistant } from './layout.actions'; 3 | 4 | export const layoutFeatureKey = 'layout'; 5 | 6 | export interface LayoutState { 7 | isHackAssistantOpen: boolean; 8 | } 9 | 10 | export const initialState: LayoutState = { 11 | isHackAssistantOpen: false 12 | }; 13 | 14 | const _layoutReducer = createReducer( 15 | initialState, 16 | on(showHackAssistant, state => ({ ...state, isHackAssistantOpen: true })), 17 | on(hideHackAssistant, state => ({ ...state, isHackAssistantOpen: false })) 18 | ); 19 | 20 | export function layoutReducer(state: LayoutState, action: Action): LayoutState { 21 | return _layoutReducer(state, action); 22 | } 23 | -------------------------------------------------------------------------------- /apps/websheep/src/app/layout/layout.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { layoutFeatureKey, LayoutState } from './layout.reducer'; 3 | 4 | export const getLayout = createFeatureSelector(layoutFeatureKey); 5 | 6 | export const getIsHackAssistantOpen = createSelector( 7 | getLayout, 8 | layout => layout.isHackAssistantOpen 9 | ); 10 | -------------------------------------------------------------------------------- /apps/websheep/src/app/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /apps/websheep/src/app/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav { 6 | width: 200px; 7 | } 8 | 9 | .sidenav .mat-toolbar { 10 | background: inherit; 11 | } 12 | 13 | .mat-toolbar.mat-primary { 14 | position: sticky; 15 | top: 0; 16 | z-index: 1; 17 | } 18 | 19 | .ws-nav__right-sidenav { 20 | min-width: 300px; 21 | width: 50%; 22 | } 23 | -------------------------------------------------------------------------------- /apps/websheep/src/app/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BreakpointObserver, 3 | Breakpoints, 4 | LayoutModule 5 | } from '@angular/cdk/layout'; 6 | import { CommonModule } from '@angular/common'; 7 | import { 8 | Component, 9 | EventEmitter, 10 | Input, 11 | NgModule, 12 | Output 13 | } from '@angular/core'; 14 | import { FlexModule } from '@angular/flex-layout'; 15 | import { 16 | MatButtonModule, 17 | MatIconModule, 18 | MatListModule, 19 | MatSidenavModule, 20 | MatToolbarModule 21 | } from '@angular/material'; 22 | import { Store } from '@ngrx/store'; 23 | import { combineLatest, Observable } from 'rxjs'; 24 | import { map, shareReplay } from 'rxjs/operators'; 25 | import { AppState } from '../reducers'; 26 | import * as fromUser from '../user/user.selectors'; 27 | 28 | @Component({ 29 | selector: 'ws-nav', 30 | templateUrl: './nav.component.html', 31 | styleUrls: ['./nav.component.scss'] 32 | }) 33 | export class NavComponent { 34 | @Input() isRightSidenavOpen = false; 35 | @Output() rightSidenavOpenChange = new EventEmitter(); 36 | 37 | canToggleSidenav$: Observable; 38 | isHandset$: Observable = this._breakpointObserver 39 | .observe(Breakpoints.Handset) 40 | .pipe( 41 | map(result => result.matches), 42 | shareReplay() 43 | ); 44 | isSidenavOpen$: Observable; 45 | isSignedIn$ = this._store.select(fromUser.getIsSignedIn); 46 | 47 | constructor( 48 | private _breakpointObserver: BreakpointObserver, 49 | private _store: Store 50 | ) { 51 | /* User can toggle if user is signed in and device is handset. */ 52 | this.canToggleSidenav$ = combineLatest([ 53 | this.isSignedIn$, 54 | this.isHandset$ 55 | ]).pipe(map(([isSignedIn, isHandset]) => isSignedIn && isHandset)); 56 | 57 | /* Sidenav is open if user is signed in and not handset. */ 58 | this.isSidenavOpen$ = combineLatest([ 59 | this.isSignedIn$, 60 | this.isHandset$ 61 | ]).pipe(map(([isSignedIn, isHandset]) => isSignedIn && !isHandset)); 62 | } 63 | } 64 | 65 | @NgModule({ 66 | declarations: [NavComponent], 67 | exports: [NavComponent], 68 | imports: [ 69 | CommonModule, 70 | LayoutModule, 71 | MatToolbarModule, 72 | MatButtonModule, 73 | MatSidenavModule, 74 | MatIconModule, 75 | MatListModule, 76 | FlexModule 77 | ] 78 | }) 79 | export class NavModule {} 80 | -------------------------------------------------------------------------------- /apps/websheep/src/app/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer, ActionReducerMap, MetaReducer } from '@ngrx/store'; 2 | import { localStorageSync } from 'ngrx-store-localstorage'; 3 | import { 4 | configFeatureKey, 5 | configReducer, 6 | ConfigState 7 | } from '../config/config.reducer'; 8 | import { 9 | assistantFeatureKey, 10 | assistantReducer, 11 | AssistantState 12 | } from '../assistant/assistant.reducer'; 13 | import { 14 | layoutFeatureKey, 15 | layoutReducer, 16 | LayoutState 17 | } from '../layout/layout.reducer'; 18 | import { userFeatureKey, userReducer, UserState } from '../user/user.reducer'; 19 | 20 | export interface AppState { 21 | [assistantFeatureKey]: AssistantState; 22 | [configFeatureKey]: ConfigState; 23 | [layoutFeatureKey]: LayoutState; 24 | [userFeatureKey]: UserState; 25 | } 26 | 27 | export const reducers: ActionReducerMap = { 28 | assistant: assistantReducer, 29 | config: configReducer, 30 | layout: layoutReducer, 31 | user: userReducer 32 | }; 33 | 34 | export function localStorageSyncReducer( 35 | reducer: ActionReducer 36 | ): ActionReducer { 37 | return localStorageSync({ 38 | keys: [assistantFeatureKey, configFeatureKey, userFeatureKey], 39 | rehydrate: true 40 | })(reducer); 41 | } 42 | 43 | export const metaReducers: MetaReducer[] = [localStorageSyncReducer]; 44 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-core/sheep.ts: -------------------------------------------------------------------------------- 1 | export enum SheepKind { 2 | Sheep = 'sheep' 3 | } 4 | 5 | export enum Destination { 6 | Kebab = 'kebab', 7 | Wool = 'wool' 8 | } 9 | 10 | export enum Gender { 11 | Female = 'female', 12 | Male = 'male' 13 | } 14 | 15 | export interface Sheep { 16 | createdAt: Date; 17 | id: string; 18 | kind: SheepKind; 19 | age: number; 20 | eyeColor: string; 21 | gender: Gender; 22 | name: string; 23 | destinations: Destination[]; 24 | pictureUri: string; 25 | } 26 | 27 | export function createSheep(sheep: Partial): Sheep { 28 | return { 29 | id: sheep.id, 30 | createdAt: sheep.createdAt && new Date(sheep.createdAt), 31 | kind: sheep.kind, 32 | age: sheep.age, 33 | eyeColor: sheep.eyeColor, 34 | gender: sheep.gender, 35 | name: sheep.name, 36 | destinations: sheep.destinations, 37 | pictureUri: sheep.pictureUri 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-form/add-sheep.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { tap } from 'rxjs/operators'; 5 | import { Sheep } from '../sheep-core/sheep'; 6 | import { sheepRouteHelper } from '../views/sheep/sheep-route-helper'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AddSheepService { 12 | constructor(private _httpClient: HttpClient, private _router: Router) {} 13 | 14 | addSheep({ sheep }: { sheep: Sheep }) { 15 | return this._httpClient 16 | .post('/sheep', sheep) 17 | .pipe( 18 | tap(() => this._router.navigate(sheepRouteHelper.sheepListRoute())) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-form/sheep-form.component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | 8 | 10 | 15 | 16 | 17 | 18 | 20 | 25 | 26 | 27 | 28 | 30 | 35 | 36 | 37 | 38 | 40 | Gender 41 | 42 | Female 43 | Male 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | Farm 53 | 54 | {{ farm.name }} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | Destinations 66 | 67 | 🍖 Kebab 68 | 🥼 Wool 69 | 70 | 71 | 72 | 73 |
74 | A sheep picture 81 |
82 | 83 |
84 | 85 | 86 | 87 |
88 | 89 | {{ errorMessage }} 90 | 91 |
92 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-form/sheep-form.component.scss: -------------------------------------------------------------------------------- 1 | .ws-sheep-form__field { 2 | min-width: 300px; 3 | } 4 | 5 | .ws-sheep-form__thumbnail { 6 | cursor: pointer; 7 | object-fit: cover; 8 | height: 80px; 9 | width: 80px; 10 | 11 | filter: grayscale(100%); 12 | 13 | &.selected,&:hover { 14 | filter: grayscale(0%); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-form/sheep-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { 5 | FormArray, 6 | FormControl, 7 | FormGroup, 8 | ReactiveFormsModule 9 | } from '@angular/forms'; 10 | import { 11 | MatButtonModule, 12 | MatFormFieldModule, 13 | MatInputModule, 14 | MatOptionModule, 15 | MatSelectModule 16 | } from '@angular/material'; 17 | import { Store } from '@ngrx/store'; 18 | import { Observable, of } from 'rxjs'; 19 | import { filter, map, shareReplay } from 'rxjs/operators'; 20 | import { AppState } from '../reducers'; 21 | import * as fromConfig from '../config/config.selectors'; 22 | import { Destination, Gender } from '../sheep-core/sheep'; 23 | import { AddSheepService } from './add-sheep.service'; 24 | import { UserFarmService } from './user-farm.service'; 25 | 26 | @Component({ 27 | selector: 'ws-sheep-form', 28 | templateUrl: './sheep-form.component.html', 29 | styleUrls: ['./sheep-form.component.scss'] 30 | }) 31 | export class SheepFormComponent implements OnInit { 32 | sheepForm = new FormGroup({ 33 | name: new FormControl('Dolly'), 34 | age: new FormControl(3), 35 | gender: new FormControl(Gender.Female), 36 | eyeColor: new FormControl('blue'), 37 | farm: new FormGroup({ 38 | id: new FormControl() 39 | }), 40 | destinations: new FormControl([Destination.Kebab]), 41 | pictureUri: new FormControl() 42 | }); 43 | 44 | errorMessage$: Observable; 45 | 46 | farmList$ = this._userFarmService.farmList$.pipe( 47 | shareReplay({ refCount: true, bufferSize: 1 }) 48 | ); 49 | 50 | pictureUriList$ = this._store.select(fromConfig.getApiServerUrl).pipe( 51 | map(apiServerUrl => { 52 | return Array.from(Array(20).keys()).map( 53 | i => `${apiServerUrl}/assets/sheep-${i}.jpg` 54 | ); 55 | }) 56 | ); 57 | 58 | constructor( 59 | private _addSheepService: AddSheepService, 60 | private _userFarmService: UserFarmService, 61 | private _store: Store 62 | ) {} 63 | 64 | ngOnInit() { 65 | this.farmList$ 66 | .pipe( 67 | filter(farmList => farmList.length > 0), 68 | map(farmList => farmList[0]) 69 | ) 70 | .subscribe(farm => 71 | this.sheepForm.patchValue({ 72 | farm: { 73 | id: farm.id 74 | } 75 | }) 76 | ); 77 | } 78 | 79 | addSheep() { 80 | this._addSheepService.addSheep({ sheep: this.sheepForm.value }).subscribe(); 81 | } 82 | 83 | selectPictureUri(pictureUri: string) { 84 | this.sheepForm.patchValue({ pictureUri }); 85 | } 86 | } 87 | 88 | @NgModule({ 89 | declarations: [SheepFormComponent], 90 | imports: [ 91 | CommonModule, 92 | FlexModule, 93 | ReactiveFormsModule, 94 | MatFormFieldModule, 95 | MatInputModule, 96 | MatButtonModule, 97 | MatSelectModule 98 | ], 99 | exports: [SheepFormComponent] 100 | }) 101 | export class SheepFormModule {} 102 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-form/user-farm.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | import { first, map, switchMap } from 'rxjs/operators'; 6 | import { ListResponse } from '../http/list-response'; 7 | import { AppState } from '../reducers'; 8 | import * as fromUser from '../user/user.selectors'; 9 | 10 | export interface Farm { 11 | id: string; 12 | name: string; 13 | } 14 | 15 | @Injectable({ 16 | providedIn: 'root' 17 | }) 18 | export class UserFarmService { 19 | farmList$: Observable = this._store.select(fromUser.getUserId).pipe( 20 | first(), 21 | switchMap(userId => 22 | this._httpClient.get>(`/farmers/${userId}/farms`) 23 | ), 24 | map(response => response.items) 25 | ); 26 | 27 | constructor( 28 | private _httpClient: HttpClient, 29 | private _store: Store 30 | ) {} 31 | } 32 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list-container/sheep-list-container.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ errorMessage }} 4 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list-container/sheep-list-container.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep/src/app/sheep-list/sheep-list-container/sheep-list-container.component.scss -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list-container/sheep-list-container.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, NgModule } from '@angular/core'; 3 | import { MatFormFieldModule } from '@angular/material'; 4 | import { Observable, of } from 'rxjs'; 5 | import { catchError, map, mapTo, shareReplay } from 'rxjs/operators'; 6 | import { SheepListModule } from '../sheep-list/sheep-list.component'; 7 | import { UserSheepService } from './user-sheep.service'; 8 | 9 | @Component({ 10 | selector: 'ws-sheep-list-container', 11 | templateUrl: './sheep-list-container.component.html', 12 | styleUrls: ['./sheep-list-container.component.scss'] 13 | }) 14 | export class SheepListContainerComponent { 15 | sheepList$ = this._sheepListService.getUserSheep().pipe( 16 | map(response => 17 | [...response.items].sort( 18 | (a, b) => b.createdAt.getTime() - a.createdAt.getTime() 19 | ) 20 | ), 21 | shareReplay({ 22 | bufferSize: 1, 23 | refCount: true 24 | }) 25 | ); 26 | 27 | errorMessage$: Observable; 28 | 29 | constructor(private _sheepListService: UserSheepService) { 30 | this.errorMessage$ = this.sheepList$.pipe( 31 | mapTo(null), 32 | catchError(() => of('Something went wrong!')) 33 | ); 34 | } 35 | } 36 | 37 | @NgModule({ 38 | declarations: [SheepListContainerComponent], 39 | imports: [CommonModule, SheepListModule, MatFormFieldModule], 40 | exports: [SheepListContainerComponent] 41 | }) 42 | export class SheepListContainerModule {} 43 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list-container/user-sheep.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { provideMockStore } from '@ngrx/store/testing'; 4 | import { provider } from '../../../testing/pact-provider'; 5 | import { HttpInterceptorsModule } from '../../http/http-interceptors.module'; 6 | import { AppState } from '../../reducers'; 7 | import { UserSheepService } from './user-sheep.service'; 8 | import { like, iso8601DateTimeWithMillis } from '@pact-foundation/pact/dsl/matchers'; 9 | 10 | describe('UserSheepService', () => { 11 | beforeAll(async () => { 12 | await provider.setup(); 13 | 14 | await provider.addInteraction({ 15 | state: [ 16 | 'user is farmer Foo', 17 | 'farm Green exists', 18 | 'farm Green has a sheep named Dolly', 19 | 'farm Green has a sheep named Bruce', 20 | 'farmer Foo is farm Green owner', 21 | ].join(','), 22 | uponReceiving: `a request for farmer Foo's sheep`, 23 | withRequest: { 24 | method: 'GET', 25 | path: '/farmers/FARMER_FOO/sheep', 26 | headers: { 27 | authorization: 'Bearer TOKEN' 28 | } 29 | }, 30 | willRespondWith: { 31 | status: 200, 32 | headers: { 'content-type': 'application/json; charset=utf-8' }, 33 | body: { 34 | next: null, 35 | totalCount: 2, 36 | items: [ 37 | { 38 | id: like('Mwy2m8LY'), 39 | createdAt: iso8601DateTimeWithMillis(), 40 | farmId: like('Aasfasd'), 41 | name: 'Dolly' 42 | }, 43 | { 44 | id: like('VxyoabX4'), 45 | createdAt: iso8601DateTimeWithMillis(), 46 | farmId: like('Aasfasd'), 47 | name: 'Bruce' 48 | } 49 | ] 50 | } 51 | } 52 | }); 53 | }); 54 | 55 | beforeEach(() => { 56 | TestBed.configureTestingModule({ 57 | imports: [HttpClientModule, HttpInterceptorsModule.forRoot()], 58 | providers: [ 59 | provideMockStore>({ 60 | initialState: { 61 | config: { 62 | apiServerUrl: `http://${provider.server.options.host}:${provider.server.options.port}`, 63 | apiBasePath: '/', 64 | includeCredentials: false 65 | }, 66 | user: { 67 | token: 'TOKEN', 68 | tokenId: null, 69 | userId: 'FARMER_FOO' 70 | } 71 | } 72 | }) 73 | ] 74 | }); 75 | }); 76 | 77 | let userSheepService: UserSheepService; 78 | beforeEach(() => (userSheepService = TestBed.inject(UserSheepService))); 79 | 80 | afterEach(() => provider.verify()); 81 | 82 | afterAll(() => provider.finalize()); 83 | 84 | it(`should retrieve user's sheep`, async () => { 85 | const response = await userSheepService.getUserSheep().toPromise(); 86 | 87 | expect(response.totalCount).toEqual(2); 88 | expect(response.items).toEqual([ 89 | expect.objectContaining({ 90 | name: 'Dolly' 91 | }), 92 | expect.objectContaining({ 93 | name: 'Bruce' 94 | }) 95 | ]); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list-container/user-sheep.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | import { first, map, switchMap, tap } from 'rxjs/operators'; 6 | import { ListResponse } from '../../http/list-response'; 7 | import { AppState } from '../../reducers'; 8 | import { createSheep, Sheep } from '../../sheep-core/sheep'; 9 | import * as fromUser from '../../user/user.selectors'; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class UserSheepService { 15 | constructor( 16 | private _httpClient: HttpClient, 17 | private _store: Store 18 | ) {} 19 | 20 | getUserSheep(): Observable> { 21 | return this._store.select(fromUser.getUserId).pipe( 22 | first(), 23 | switchMap(farmerId => 24 | this._httpClient 25 | .get>( 26 | `/farmers/${encodeURIComponent(farmerId)}/sheep` 27 | ) 28 | .pipe( 29 | map(response => ({ 30 | ...response, 31 | items: response.items.map(createSheep) 32 | })) 33 | ) 34 | ) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list/sheep-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list/sheep-list.component.scss: -------------------------------------------------------------------------------- 1 | .ws-sheep-list__preview { 2 | margin: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-list/sheep-list.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | Input, 6 | NgModule 7 | } from '@angular/core'; 8 | import { FlexModule } from '@angular/flex-layout'; 9 | import { Sheep } from '../../sheep-core/sheep'; 10 | import { SheepDestinationEmojiPipeModule } from '../sheep-preview/sheep-destination-emoji.pipe'; 11 | import { SheepPreviewModule } from '../sheep-preview/sheep-preview.component'; 12 | 13 | @Component({ 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | selector: 'ws-sheep-list', 16 | templateUrl: './sheep-list.component.html', 17 | styleUrls: ['./sheep-list.component.scss'] 18 | }) 19 | export class SheepListComponent { 20 | @Input() sheepList: Sheep[]; 21 | } 22 | 23 | @NgModule({ 24 | declarations: [SheepListComponent], 25 | imports: [ 26 | CommonModule, 27 | SheepPreviewModule, 28 | SheepDestinationEmojiPipeModule, 29 | FlexModule 30 | ], 31 | exports: [SheepListComponent] 32 | }) 33 | export class SheepListModule {} 34 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-preview/sheep-destination-emoji.pipe.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule, Pipe, PipeTransform } from '@angular/core'; 3 | import { Destination } from '../../sheep-core/sheep'; 4 | 5 | @Pipe({ 6 | name: 'sheepDestinationEmoji' 7 | }) 8 | export class SheepDestinationEmojiPipe implements PipeTransform { 9 | private _emojiMap = new Map([ 10 | [Destination.Kebab, '🍖'], 11 | [Destination.Wool, '🥼'] 12 | ]); 13 | 14 | transform(destination: Destination): string { 15 | return this._emojiMap.get(destination); 16 | } 17 | } 18 | 19 | @NgModule({ 20 | declarations: [SheepDestinationEmojiPipe], 21 | exports: [SheepDestinationEmojiPipe] 22 | }) 23 | export class SheepDestinationEmojiPipeModule {} 24 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-preview/sheep-preview.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ sheep.name }} 5 | {{ sheep.age }} years old 6 | Created on {{ sheep.createdAt | date:'medium' }} 7 | 8 | 9 | 15 | 16 | 17 |
👀 {{ sheep.eyeColor }} eyes
18 |
Goals:
19 |
    20 |
  • {{ destination | sheepDestinationEmoji }} {{ destination }}
  • 22 |
23 |
24 | 25 |
26 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-preview/sheep-preview.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | width: 300px; 5 | } 6 | 7 | .ws-sheep-preview__created-at { 8 | font-size: .8em; 9 | } 10 | 11 | .ws-sheep-preview__eye-color { 12 | font-weight: bold; 13 | } 14 | 15 | .ws-sheep-preview__picture { 16 | height: 250px; 17 | object-fit: cover; 18 | } 19 | -------------------------------------------------------------------------------- /apps/websheep/src/app/sheep-list/sheep-preview/sheep-preview.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, NgModule } from '@angular/core'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { MatCardModule } from '@angular/material'; 5 | import { Sheep } from '../../sheep-core/sheep'; 6 | import { SheepDestinationEmojiPipeModule } from './sheep-destination-emoji.pipe'; 7 | 8 | @Component({ 9 | selector: 'ws-sheep-preview', 10 | templateUrl: './sheep-preview.component.html', 11 | styleUrls: ['./sheep-preview.component.scss'] 12 | }) 13 | export class SheepPreviewComponent { 14 | @Input() sheep: Sheep; 15 | } 16 | 17 | @NgModule({ 18 | declarations: [SheepPreviewComponent], 19 | imports: [ 20 | CommonModule, 21 | MatCardModule, 22 | SheepDestinationEmojiPipeModule, 23 | FlexModule 24 | ], 25 | exports: [SheepPreviewComponent] 26 | }) 27 | export class SheepPreviewModule {} 28 | -------------------------------------------------------------------------------- /apps/websheep/src/app/signin/signin-form.component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | 15 | 16 | 24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 | {{ errorMessage }} 32 | 33 |
34 | 35 |
36 | 37 |
38 |

Other farmers

39 |
    40 |
  • foobar : zTAr@6y60PwJ
  • 41 |
  • johndoe : %yP50h^qI02t
  • 42 |
43 |
44 | -------------------------------------------------------------------------------- /apps/websheep/src/app/signin/signin-form.component.scss: -------------------------------------------------------------------------------- 1 | .ws-login-field { 2 | min-width: 300px; 3 | } 4 | -------------------------------------------------------------------------------- /apps/websheep/src/app/signin/signin-form.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | NgModule, 6 | OnDestroy, 7 | OnInit 8 | } from '@angular/core'; 9 | import { FlexModule } from '@angular/flex-layout'; 10 | import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; 11 | import { MatButtonModule, MatInputModule } from '@angular/material'; 12 | import { Scavenger } from '@wishtack/rx-scavenger'; 13 | import { concat, Observable, Subject } from 'rxjs'; 14 | import { map, shareReplay, switchMap } from 'rxjs/operators'; 15 | import { Credentials, Signin, SigninResult } from './signin.service'; 16 | 17 | @Component({ 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | selector: 'ws-signin-form', 20 | templateUrl: './signin-form.component.html', 21 | styleUrls: ['./signin-form.component.scss'] 22 | }) 23 | export class SigninFormComponent implements OnDestroy, OnInit { 24 | loginForm = new FormGroup({ 25 | userName: new FormControl('karinelemarchand'), 26 | password: new FormControl('123456') 27 | }); 28 | 29 | errorMessage$: Observable; 30 | loginResult$: Observable; 31 | 32 | private _credentialsSubmit$ = new Subject(); 33 | private _scavenger = new Scavenger(this); 34 | 35 | constructor(private _signin: Signin) { 36 | this.loginResult$ = this._credentialsSubmit$.pipe( 37 | /* Login and materialize response so the stream doesn't brake on error. */ 38 | switchMap(credentials => concat(this._signin.signIn(credentials))), 39 | shareReplay({ 40 | bufferSize: 1, 41 | refCount: true 42 | }) 43 | ); 44 | 45 | /* Map errors to error message. */ 46 | this.errorMessage$ = this.loginResult$.pipe( 47 | map(result => (result === SigninResult.Error ? 'Login error' : null)) 48 | ); 49 | } 50 | 51 | ngOnInit() { 52 | this.loginResult$.pipe(this._scavenger.collect()).subscribe(); 53 | } 54 | 55 | ngOnDestroy() {} 56 | 57 | logIn() { 58 | this._credentialsSubmit$.next(this.loginForm.value); 59 | } 60 | } 61 | 62 | @NgModule({ 63 | declarations: [SigninFormComponent], 64 | imports: [ 65 | CommonModule, 66 | MatInputModule, 67 | ReactiveFormsModule, 68 | MatButtonModule, 69 | FlexModule 70 | ], 71 | exports: [SigninFormComponent] 72 | }) 73 | export class LoginModule {} 74 | -------------------------------------------------------------------------------- /apps/websheep/src/app/signin/signin.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { Store } from '@ngrx/store'; 5 | import { concat, Observable, of } from 'rxjs'; 6 | import { first, map, materialize, switchMap, tap } from 'rxjs/operators'; 7 | import { getIncludeCredentials } from '../config/config.selectors'; 8 | import { AppState } from '../reducers'; 9 | import { signinSuccess } from '../user/user.actions'; 10 | import { sheepRouteHelper } from '../views/sheep/sheep-route-helper'; 11 | 12 | export interface Credentials { 13 | userName: string; 14 | password: string; 15 | } 16 | 17 | export interface TokenResponse { 18 | id: string; 19 | token: string; 20 | userId: string; 21 | } 22 | 23 | export enum SigninResult { 24 | Pending = 'pending', 25 | Success = 'success', 26 | Error = 'error' 27 | } 28 | 29 | @Injectable({ 30 | providedIn: 'root' 31 | }) 32 | export class Signin { 33 | constructor( 34 | private _httpClient: HttpClient, 35 | private _router: Router, 36 | private _store: Store 37 | ) {} 38 | 39 | signIn(credentials: Credentials): Observable { 40 | const loginRequest$ = this._httpClient 41 | .post('/tokens', credentials) 42 | .pipe( 43 | tap((tokenResponse: TokenResponse) => 44 | this._store.dispatch( 45 | signinSuccess({ 46 | token: tokenResponse.token, 47 | tokenId: tokenResponse.id, 48 | userId: tokenResponse.userId 49 | }) 50 | ) 51 | ) 52 | ); 53 | 54 | return concat( 55 | of(SigninResult.Pending), 56 | loginRequest$.pipe( 57 | tap(() => this._router.navigate(sheepRouteHelper.sheepListRoute())), 58 | materialize(), 59 | map(notification => { 60 | return notification.kind === 'E' 61 | ? SigninResult.Error 62 | : SigninResult.Success; 63 | }) 64 | ) 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/websheep/src/app/toolbar/swagger-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep/src/app/toolbar/swagger-logo.png -------------------------------------------------------------------------------- /apps/websheep/src/app/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 |
🐑 {{ greetings$ | async}}
2 |
3 | 4 | 5 | 10 | openapi specification logo 11 | 12 | 13 | 14 | settings_applications 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /apps/websheep/src/app/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .ws-swagger-logo__container { 3 | height: 30px; 4 | margin-right: 20px; 5 | 6 | img { 7 | height: 30px; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /apps/websheep/src/app/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FlexModule } from '@angular/flex-layout'; 4 | import { MatIconModule } from '@angular/material'; 5 | import { Store } from '@ngrx/store'; 6 | import { Observable } from 'rxjs'; 7 | import { map } from 'rxjs/operators'; 8 | import { getApiBaseUrl } from '../config/config.selectors'; 9 | import { FarmerService } from '../farmer/farmer.service'; 10 | import { showHackAssistant } from '../layout/layout.actions'; 11 | import { AppState } from '../reducers'; 12 | 13 | @Component({ 14 | selector: 'ws-toolbar', 15 | templateUrl: './toolbar.component.html', 16 | styleUrls: ['./toolbar.component.scss'] 17 | }) 18 | export class ToolbarComponent { 19 | apiBaseUrl$ = this._store.select(getApiBaseUrl); 20 | greetings$: Observable; 21 | swaggerLogoUri = require('./swagger-logo.png'); 22 | 23 | constructor( 24 | private _farmerService: FarmerService, 25 | private _store: Store 26 | ) { 27 | const currentFarmer$ = this._farmerService.currentFarmer$; 28 | this.greetings$ = currentFarmer$.pipe( 29 | map(farmer => 30 | farmer ? `Welcome ${farmer.firstName}` : `Welcome to Websheep` 31 | ) 32 | ); 33 | } 34 | 35 | hack() { 36 | this._store.dispatch(showHackAssistant()); 37 | } 38 | } 39 | 40 | @NgModule({ 41 | declarations: [ToolbarComponent], 42 | imports: [CommonModule, MatIconModule, FlexModule], 43 | exports: [ToolbarComponent] 44 | }) 45 | export class ToolbarModule {} 46 | -------------------------------------------------------------------------------- /apps/websheep/src/app/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare const require; 2 | -------------------------------------------------------------------------------- /apps/websheep/src/app/user/user.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from '@ngrx/store'; 2 | 3 | export const signinSuccess = createAction( 4 | '[User] Signin success', 5 | props<{ 6 | token: string; 7 | tokenId: string; 8 | userId: string; 9 | }>() 10 | ); 11 | 12 | export const signout = createAction('[User] Signout'); 13 | -------------------------------------------------------------------------------- /apps/websheep/src/app/user/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, createReducer, on } from '@ngrx/store'; 2 | import { signinSuccess, signout } from './user.actions'; 3 | 4 | export const userFeatureKey = 'user'; 5 | 6 | export interface UserState { 7 | token: string; 8 | tokenId: string; 9 | userId: string; 10 | } 11 | 12 | export const initialState: UserState = { 13 | token: null, 14 | tokenId: null, 15 | userId: null 16 | }; 17 | 18 | const _userReducer = createReducer( 19 | initialState, 20 | on(signinSuccess, (state, { userId, tokenId, token }) => ({ 21 | ...state, 22 | userId, 23 | token, 24 | tokenId 25 | })), 26 | on(signout, () => initialState) 27 | ); 28 | 29 | export function userReducer(state: UserState, action: Action): UserState { 30 | return _userReducer(state, action); 31 | } 32 | -------------------------------------------------------------------------------- /apps/websheep/src/app/user/user.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { userFeatureKey, UserState } from './user.reducer'; 3 | 4 | export const getUser = createFeatureSelector(userFeatureKey); 5 | 6 | export const getToken = createSelector( 7 | getUser, 8 | user => user.token 9 | ); 10 | 11 | export const getTokenId = createSelector( 12 | getUser, 13 | user => user.tokenId 14 | ); 15 | 16 | export const getUserId = createSelector( 17 | getUser, 18 | user => user.userId 19 | ); 20 | 21 | export const getIsSignedIn = createSelector( 22 | getUserId, 23 | userId => userId != null 24 | ); 25 | -------------------------------------------------------------------------------- /apps/websheep/src/app/views/sheep/sheep-route-helper.ts: -------------------------------------------------------------------------------- 1 | export const sheepRouteHelper = { 2 | BASE_PATH: 'sheep', 3 | SHEEP_ADD_PATH: 'add', 4 | SHEEP_LIST_PATH: 'list', 5 | 6 | sheepListRoute() { 7 | return [this.BASE_PATH, this.SHEEP_LIST_PATH]; 8 | }, 9 | sheepAddRoute() { 10 | return [this.BASE_PATH, this.SHEEP_ADD_PATH]; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /apps/websheep/src/app/views/sheep/sheep-views.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { 5 | SheepFormComponent, 6 | SheepFormModule 7 | } from '../../sheep-form/sheep-form.component'; 8 | import { 9 | SheepListContainerComponent, 10 | SheepListContainerModule 11 | } from '../../sheep-list/sheep-list-container/sheep-list-container.component'; 12 | import { sheepRouteHelper } from './sheep-route-helper'; 13 | 14 | export const sheepRoutes: Routes = [ 15 | { 16 | path: sheepRouteHelper.SHEEP_ADD_PATH, 17 | component: SheepFormComponent 18 | }, 19 | { 20 | path: sheepRouteHelper.SHEEP_LIST_PATH, 21 | component: SheepListContainerComponent 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [ 27 | CommonModule, 28 | SheepListContainerModule, 29 | SheepFormModule, 30 | RouterModule.forChild(sheepRoutes) 31 | ] 32 | }) 33 | export class SheepViewsModule {} 34 | -------------------------------------------------------------------------------- /apps/websheep/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/websheep/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | defaultApiServerUrl: 'https://websheep.herokuapp.com', 3 | production: true 4 | }; 5 | -------------------------------------------------------------------------------- /apps/websheep/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | defaultApiServerUrl: 'http://localhost:3333', 7 | production: false 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /apps/websheep/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/apps/websheep/src/favicon.ico -------------------------------------------------------------------------------- /apps/websheep/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Websheep 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/websheep/src/lib/item-selector/item-selector.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ titlePrefix }}:  6 | {{ selectedLabel }} 7 | 8 | {{ description }} 9 | 10 | 11 | 12 | {{ itemAndLabel.label }} 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/websheep/src/lib/item-selector/item-selector.component.scss: -------------------------------------------------------------------------------- 1 | .ws-hack-topic-selector__title-prefix { 2 | font-weight: bold; 3 | } 4 | -------------------------------------------------------------------------------- /apps/websheep/src/lib/item-selector/item-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | Output, 10 | SimpleChanges 11 | } from '@angular/core'; 12 | import { MatExpansionModule, MatListModule } from '@angular/material'; 13 | 14 | export interface IdAndLabel { 15 | label: string; 16 | id: string; 17 | } 18 | 19 | @Component({ 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | selector: 'ws-item-selector', 22 | templateUrl: './item-selector.component.html', 23 | styleUrls: ['./item-selector.component.scss'] 24 | }) 25 | export class ItemSelectorComponent implements OnChanges { 26 | @Input() selectedId: string; 27 | @Input() idAndLabelList: IdAndLabel[]; 28 | @Input() description: string; 29 | @Input() titlePrefix: string; 30 | @Output() selectedIdChange = new EventEmitter(); 31 | isExpanded: boolean; 32 | selectedLabel: string; 33 | 34 | ngOnChanges(changes: SimpleChanges) { 35 | if (changes.selectedId) { 36 | this.isExpanded = this.selectedId == null; 37 | this.selectedLabel = this._getItemLabel(this.selectedId); 38 | } 39 | } 40 | 41 | private _getItemLabel(id: string) { 42 | if (id == null || this.idAndLabelList == null) { 43 | return null; 44 | } 45 | 46 | const idAndLabel = this.idAndLabelList.find(args => args.id === id); 47 | 48 | return idAndLabel ? idAndLabel.label : null; 49 | } 50 | } 51 | 52 | @NgModule({ 53 | declarations: [ItemSelectorComponent], 54 | imports: [CommonModule, MatListModule, MatExpansionModule], 55 | exports: [ItemSelectorComponent] 56 | }) 57 | export class HackTopicSelectorModule {} 58 | -------------------------------------------------------------------------------- /apps/websheep/src/lib/url-join.ts: -------------------------------------------------------------------------------- 1 | export function urlJoin(blocks) { 2 | return blocks 3 | .map(block => block.replace(/^\/+/, '').replace(/\/+$/, '')) 4 | .join('/'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/websheep/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | -------------------------------------------------------------------------------- /apps/websheep/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /apps/websheep/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; 3 | 4 | html, body { height: 100%; } 5 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 6 | 7 | .ws-scrollable { 8 | overflow-y: scroll; 9 | } 10 | 11 | .ws-cursor-pointer { 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /apps/websheep/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /apps/websheep/src/testing/pact-provider.ts: -------------------------------------------------------------------------------- 1 | import { Pact } from '@pact-foundation/pact'; 2 | 3 | export const _projectRootPath = `${__dirname}/../../../../`; 4 | 5 | export const provider = new Pact({ 6 | consumer: 'WebSheep', 7 | provider: 'WebSheepApi', 8 | cors: true, 9 | pactfileWriteMode: 'update', 10 | log: `${_projectRootPath}/dist/pact-logs`, 11 | dir: `${_projectRootPath}/libs/pacts` 12 | }); 13 | -------------------------------------------------------------------------------- /apps/websheep/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["**/*.ts"], 9 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/websheep/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/websheep/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "ws", "camelCase"], 5 | "component-selector": [true, "element", "ws", "kebab-case"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/websheep/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "websheep", 4 | "routes": [ 5 | { 6 | "handle": "filesystem" 7 | }, 8 | { 9 | "src": "/(.*)", 10 | "dest": "/index.html" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], 3 | transform: { 4 | '^.+\\.(ts|js|html)$': 'ts-jest' 5 | }, 6 | resolver: '@nrwl/jest/plugins/resolver', 7 | moduleFileExtensions: ['ts', 'js', 'html'], 8 | coverageReporters: ['html'], 9 | passWithNoTests: true 10 | }; 11 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/libs/.gitkeep -------------------------------------------------------------------------------- /libs/pacts/websheep-websheepapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "WebSheep" 4 | }, 5 | "provider": { 6 | "name": "WebSheepApi" 7 | }, 8 | "interactions": [ 9 | { 10 | "description": "a request for farmer Foo's sheep", 11 | "providerState": "user is farmer Foo,farm Green exists,farm Green has a sheep named Dolly,farm Green has a sheep named Bruce,farmer Foo is farm Green owner", 12 | "request": { 13 | "method": "GET", 14 | "path": "/farmers/FARMER_FOO/sheep", 15 | "headers": { 16 | "authorization": "Bearer TOKEN" 17 | } 18 | }, 19 | "response": { 20 | "status": 200, 21 | "headers": { 22 | "content-type": "application/json; charset=utf-8" 23 | }, 24 | "body": { 25 | "next": null, 26 | "totalCount": 2, 27 | "items": [ 28 | { 29 | "id": "Mwy2m8LY", 30 | "createdAt": "2015-08-06T16:53:10.123+01:00", 31 | "farmId": "Aasfasd", 32 | "name": "Dolly" 33 | }, 34 | { 35 | "id": "VxyoabX4", 36 | "createdAt": "2015-08-06T16:53:10.123+01:00", 37 | "farmId": "Aasfasd", 38 | "name": "Bruce" 39 | } 40 | ] 41 | }, 42 | "matchingRules": { 43 | "$.body.items[0].id": { 44 | "match": "type" 45 | }, 46 | "$.body.items[0].createdAt": { 47 | "match": "regex", 48 | "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3,}([+-][0-2]\\d:[0-5]\\d|Z)$" 49 | }, 50 | "$.body.items[0].farmId": { 51 | "match": "type" 52 | }, 53 | "$.body.items[1].id": { 54 | "match": "type" 55 | }, 56 | "$.body.items[1].createdAt": { 57 | "match": "regex", 58 | "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3,}([+-][0-2]\\d:[0-5]\\d|Z)$" 59 | }, 60 | "$.body.items[1].farmId": { 61 | "match": "type" 62 | } 63 | } 64 | }, 65 | "metadata": null 66 | } 67 | ], 68 | "metadata": { 69 | "pactSpecification": { 70 | "version": "2.0.0" 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "websheep", 3 | "implicitDependencies": { 4 | "angular.json": "*", 5 | "package.json": "*", 6 | "tsconfig.json": "*", 7 | "tslint.json": "*", 8 | "nx.json": "*" 9 | }, 10 | "projects": { 11 | "websheep-e2e": { 12 | "tags": [] 13 | }, 14 | "websheep": { 15 | "tags": [] 16 | }, 17 | "websheep-api": { 18 | "tags": [] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websheep", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "deploy": "run-p deploy:*", 7 | "deploy:websheep": "run-s build:websheep:prod _vercel:websheep", 8 | "deploy:websheep-api": "git push heroku master", 9 | "ng": "ng", 10 | "nx": "nx", 11 | "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", 12 | "start": "run-p start:*", 13 | "start:websheep": "ng serve websheep", 14 | "start:websheep-api": "ng serve websheep-api", 15 | "build": "yarn run build:websheep-api:prod", 16 | "build:websheep": "ng build websheep", 17 | "build:websheep:prod": "ng build websheep --prod", 18 | "build:websheep-api:prod": "ng build websheep-api --prod", 19 | "test": "ng test", 20 | "test:websheep-api": "ng test websheep-api --watch", 21 | "lint": "nx workspace-lint && ng lint", 22 | "e2e": "ng e2e", 23 | "affected:apps": "nx affected:apps", 24 | "affected:libs": "nx affected:libs", 25 | "affected:build": "nx affected:build", 26 | "affected:e2e": "nx affected:e2e", 27 | "affected:test": "nx affected:test", 28 | "affected:lint": "nx affected:lint", 29 | "affected:dep-graph": "nx affected:dep-graph", 30 | "affected": "nx affected", 31 | "format": "nx format:write", 32 | "format:write": "nx format:write", 33 | "format:check": "nx format:check", 34 | "update": "ng update @nrwl/workspace", 35 | "update:check": "ng update", 36 | "workspace-schematic": "nx workspace-schematic", 37 | "dep-graph": "nx dep-graph", 38 | "help": "nx help", 39 | "_vercel:websheep": "cp apps/websheep/vercel.json dist/apps/websheep && vercel dist/apps/websheep", 40 | "_vercel:websheep-api": "vercel --local-config apps/websheep-api/vercel.json" 41 | }, 42 | "private": true, 43 | "dependencies": { 44 | "@angular/animations": "^9.0.0-next.11", 45 | "@angular/cdk": "~8.2.3", 46 | "@angular/common": "^9.0.0-next.11", 47 | "@angular/compiler": "^9.0.0-next.11", 48 | "@angular/core": "^9.0.0-next.11", 49 | "@angular/flex-layout": "^8.0.0-beta.27", 50 | "@angular/forms": "^9.0.0-next.11", 51 | "@angular/material": "~8.2.3", 52 | "@angular/platform-browser": "^9.0.0-next.11", 53 | "@angular/platform-browser-dynamic": "^9.0.0-next.11", 54 | "@angular/router": "^9.0.0-next.11", 55 | "@ngrx/effects": "^8.4.0", 56 | "@ngrx/store": "^8.4.0", 57 | "@nrwl/angular": "8.6.0", 58 | "@wishtack/rx-scavenger": "^1.0.5", 59 | "body-parser": "^1.19.0", 60 | "cookie-parser": "^1.4.4", 61 | "core-js": "^2.5.4", 62 | "cors": "^2.8.5", 63 | "express": "4.17.1", 64 | "express-openapi-validator": "^4.10.11", 65 | "hammerjs": "^2.0.8", 66 | "jsonwebtoken": "^8.5.1", 67 | "lowdb": "^1.0.0", 68 | "ngrx-store-localstorage": "^8.0.0", 69 | "npm-run-all": "^4.1.5", 70 | "passport": "^0.4.0", 71 | "passport-cookie": "^1.0.6", 72 | "passport-http-bearer": "^1.0.1", 73 | "pbkdf2": "^3.0.17", 74 | "rxjs": "~6.5.3", 75 | "shortid": "^2.2.15", 76 | "swagger-ui-express": "^4.1.2", 77 | "yamljs": "^0.3.0", 78 | "zone.js": "^0.10.2" 79 | }, 80 | "devDependencies": { 81 | "@angular-devkit/build-angular": "^0.900.0-next.11", 82 | "@angular/cli": "9.0.0-next.11", 83 | "@angular/compiler-cli": "^9.0.0-next.11", 84 | "@angular/language-service": "^9.0.0-next.11", 85 | "@ngrx/schematics": "^8.4.0", 86 | "@nrwl/cypress": "8.6.0", 87 | "@nrwl/express": "^8.6.0", 88 | "@nrwl/jest": "8.6.0", 89 | "@nrwl/node": "8.6.0", 90 | "@nrwl/workspace": "8.6.0", 91 | "@pact-foundation/pact": "^9.5.0", 92 | "@types/express": "4.17.0", 93 | "@types/jest": "24.0.9", 94 | "@types/node": "~8.9.4", 95 | "@wishtack/schematics": "^1.1.2", 96 | "codelyzer": "~5.0.1", 97 | "cypress": "3.4.1", 98 | "dotenv": "6.2.0", 99 | "eslint": "6.1.0", 100 | "jest": "24.1.0", 101 | "jest-openapi": "^0.14.2", 102 | "jest-preset-angular": "7.0.0", 103 | "jsonpath": "^1.0.2", 104 | "prettier": "1.18.2", 105 | "supertest": "^4.0.2", 106 | "ts-jest": "24.0.0", 107 | "ts-node": "~7.0.0", 108 | "tslint": "~5.11.0", 109 | "typescript": "~3.5.3" 110 | }, 111 | "packageManager": "yarn@3.3.1", 112 | "engines": { 113 | "node": "16.x" 114 | }, 115 | "volta": { 116 | "node": "16.19.0" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmicode/websheep/96f5eceb6d641e2da341416b0be52e7030a8a27a/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2017", "dom"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": {} 19 | }, 20 | "exclude": ["node_modules", "tmp"] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/@nrwl/workspace/src/tslint", 4 | "node_modules/codelyzer" 5 | ], 6 | "rules": { 7 | "arrow-return-shorthand": true, 8 | "callable-types": true, 9 | "class-name": true, 10 | "deprecation": { 11 | "severity": "warn" 12 | }, 13 | "forin": true, 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "interface-over-type-literal": true, 16 | "member-access": false, 17 | "member-ordering": [ 18 | true, 19 | { 20 | "order": [ 21 | "static-field", 22 | "instance-field", 23 | "static-method", 24 | "instance-method" 25 | ] 26 | } 27 | ], 28 | "no-arg": true, 29 | "no-bitwise": true, 30 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 31 | "no-construct": true, 32 | "no-debugger": true, 33 | "no-duplicate-super": true, 34 | "no-empty": false, 35 | "no-empty-interface": true, 36 | "no-eval": true, 37 | "no-inferrable-types": [true, "ignore-params"], 38 | "no-misused-new": true, 39 | "no-non-null-assertion": true, 40 | "no-shadowed-variable": true, 41 | "no-string-literal": false, 42 | "no-string-throw": true, 43 | "no-switch-case-fall-through": true, 44 | "no-unnecessary-initializer": true, 45 | "no-unused-expression": true, 46 | "no-var-keyword": true, 47 | "object-literal-sort-keys": false, 48 | "prefer-const": true, 49 | "radix": true, 50 | "triple-equals": [true, "allow-null-check"], 51 | "unified-signatures": true, 52 | "variable-name": false, 53 | "nx-enforce-module-boundaries": [ 54 | true, 55 | { 56 | "allow": [], 57 | "depConstraints": [ 58 | { 59 | "sourceTag": "*", 60 | "onlyDependOnLibsWithTags": ["*"] 61 | } 62 | ] 63 | } 64 | ], 65 | "directive-selector": [true, "attribute", "app", "camelCase"], 66 | "component-selector": [true, "element", "app", "kebab-case"], 67 | "no-conflicting-lifecycle": true, 68 | "no-host-metadata-property": true, 69 | "no-input-rename": true, 70 | "no-inputs-metadata-property": true, 71 | "no-output-native": true, 72 | "no-output-on-prefix": true, 73 | "no-output-rename": true, 74 | "no-outputs-metadata-property": true, 75 | "template-banana-in-box": true, 76 | "template-no-negated-async": true, 77 | "use-lifecycle-interface": true, 78 | "use-pipe-transform-interface": true 79 | } 80 | } 81 | --------------------------------------------------------------------------------