├── .gitignore ├── LICENSE ├── README.md ├── demo.adoc ├── notes-api ├── .gitignore ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ ├── docker │ │ ├── app.yml │ │ └── postgresql.yml │ ├── kotlin │ │ └── com │ │ │ └── okta │ │ │ └── developer │ │ │ └── notes │ │ │ ├── DataInitializer.kt │ │ │ ├── DemoApplication.kt │ │ │ ├── LogoutController.kt │ │ │ ├── RestConfiguration.kt │ │ │ ├── RouteController.kt │ │ │ ├── SecurityConfiguration.kt │ │ │ └── UserController.kt │ └── resources │ │ ├── application-dev.properties │ │ └── application-prod.properties │ └── test │ └── kotlin │ └── com │ └── okta │ └── developer │ └── notes │ └── DemoApplicationTests.kt └── notes ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ ├── app.po.ts │ ├── login.po.ts │ └── notes.e2e-spec.ts └── tsconfig.json ├── karma.conf.js ├── nginx.config ├── package-lock.json ├── package.json ├── src ├── _variables.scss ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── note │ │ ├── model.json │ │ ├── note-edit │ │ │ ├── note-edit.component.html │ │ │ ├── note-edit.component.spec.ts │ │ │ └── note-edit.component.ts │ │ ├── note-filter.ts │ │ ├── note-list │ │ │ ├── note-list.component.html │ │ │ ├── note-list.component.spec.ts │ │ │ ├── note-list.component.ts │ │ │ └── sortable.directive.ts │ │ ├── note.module.ts │ │ ├── note.routes.ts │ │ ├── note.service.spec.ts │ │ ├── note.service.ts │ │ └── note.ts │ └── shared │ │ ├── auth.service.ts │ │ └── user.ts ├── assets │ ├── .gitkeep │ └── images │ │ └── angular.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── proxy.conf.js ├── styles.scss └── test.ts ├── static.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | *.env 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020-Present Okta, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular + Spring Boot Docker Example 2 | 3 | This example shows how to deploy Angular and Spring Boot apps with Docker. Specifically, it shows how to: 4 | 5 | * Build Angular containers with `Dockerfile` 6 | * Combine Angular and Spring Boot in a JAR 7 | * Build Docker images with Jib 8 | * Build Docker images with Buildpacks 9 | 10 | Please read [Angular + Docker with a Big Hug from Spring Boot](https://developer.okta.com/blog/2020/06/17/angular-docker-spring-boot) to see how this example was created. This blog post is the 4th in a series. Other blog posts in this series include: 11 | 12 | 1. [Build a CRUD App with Angular 9 and Spring Boot](https://developer.okta.com/blog/2020/01/06/crud-angular-9-spring-boot-2) 13 | 2. [Build Beautiful Angular Apps with Bootstrap](https://developer.okta.com/blog/2020/03/02/angular-bootstrap) 14 | 3. [Angular Deployment with a Side of Spring Boot](https://developer.okta.com/blog/2020/05/29/angular-deployment) 15 | 16 | **Prerequisites:** 17 | 18 | * [Node 12](https://nodejs.org/)+ 19 | * [Java 11](https://adoptopenjdk.net/)+ 20 | * An [Okta Developer Account](https://developer.okta.com/signup/) 21 | 22 | > [Okta](https://developer.okta.com/) has Authentication and User Management APIs that reduce development time with instant-on, scalable user infrastructure. Okta's intuitive API and expert support make it easy for developers to authenticate, manage and secure users and roles in any application. 23 | 24 | * [Getting Started](#getting-started) 25 | * [Links](#links) 26 | * [Help](#help) 27 | * [License](#license) 28 | 29 | ## Getting Started 30 | 31 | To install this example application, run the following commands: 32 | 33 | ```bash 34 | git clone https://github.com/oktadeveloper/okta-angular-spring-boot-docker-example.git 35 | cd okta-angular-spring-boot-docker-example 36 | ``` 37 | 38 | This will get a copy of the project installed locally. To install all of its dependencies and start each app, follow the instructions below. 39 | 40 | ### Spring Boot Configuration 41 | 42 | To create a new OIDC app for Spring Boot on Okta: 43 | 44 | 1. Log in to your developer account, navigate to **Applications**, and click on **Add Application**. 45 | 2. Select **Web** and click **Next**. 46 | 3. Give the application a name and add `http://localhost:8080/login/oauth2/code/okta` as a login redirect URI. 47 | 4. Click **Done**. 48 | 49 | Create an `okta.env` file in the `notes-api` directory and copy your settings into it. 50 | 51 | ```bash 52 | export OKTA_OAUTH2_ISSUER=https://{yourOktaDomain}/oauth2/default 53 | export OKTA_OAUTH2_CLIENT_ID={yourClientId} 54 | export OKTA_OAUTH2_CLIENT_SECRET={yourClientSecret} 55 | ``` 56 | 57 | **NOTE:** The value of `{yourOktaDomain}` should be something like `dev-123456.okta.com`. Make sure you don't include `-admin` in the value! 58 | 59 | Start your Spring Boot app by navigating to the `notes-api` directory, sourcing this file, and starting your app. 60 | 61 | ```bash 62 | cd notes-api 63 | source okta.env 64 | ./gradlew bootRun -Pprod 65 | ``` 66 | 67 | For instructions on how to build Docker images and deploy these applications to the cloud, please read the [blog post](https://developer.okta.com/blog/2020/06/17/angular-docker-spring-boot)! 68 | 69 | ## Links 70 | 71 | This example uses the following open source libraries from Okta: 72 | 73 | * [Okta Spring Boot Starter](https://github.com/okta/okta-spring-boot) 74 | * [Okta Angular SDK](https://github.com/okta/okta-oidc-js/tree/master/packages/okta-angular) 75 | 76 | ## Help 77 | 78 | Please post any questions as comments on the [blog post](https://developer.okta.com/blog/2020/06/17/angular-docker-spring-boot), or visit our [Okta Developer Forums](https://devforum.okta.com/). 79 | 80 | ## License 81 | 82 | Apache 2.0, see [LICENSE](LICENSE). 83 | -------------------------------------------------------------------------------- /demo.adoc: -------------------------------------------------------------------------------- 1 | :experimental: 2 | // Define unicode for Apple Command key. 3 | :commandkey: ⌘ 4 | :toc: macro 5 | 6 | == Angular + Docker Demo Steps 7 | 8 | In this demo, I'll show how to use Docker to create an image for your Angular app and deploy it to Heroku. Then, I’ll show how to combine Angular and Spring Boot into the same JAR artifact for deployment. You’ll learn how to Dockerize the combined apps using Jib and Cloud Native Buildpacks. Finally, I’ll show you how to deploy your Docker image to Heroku, Knative on Google Cloud, and Cloud Foundry. 9 | 10 | **Prerequisites:** 11 | 12 | * https://adoptopenjdk.net/[Java 11]+ 13 | * https://nodejs.org/[Node 12]+ 14 | * https://docs.docker.com/get-docker/[Docker] 15 | * An https://developer.okta.com/signup/[Okta Developer Account] 16 | 17 | TIP: The brackets at the end of some steps indicate the IntelliJ Live Templates to use. You can find the template definitions at https://github.com/mraible/idea-live-templates[mraible/idea-live-templates]. 18 | 19 | toc::[] 20 | 21 | === Create an Angular + Spring Boot App 22 | 23 | . Clone the Angular + Bootstrap example app. 24 | + 25 | [source,shell] 26 | ---- 27 | git clone https://github.com/oktadeveloper/okta-angular-deployment-example.git \ 28 | okta-angular-spring-boot-docker-example 29 | ---- 30 | 31 | ==== Secure Your Angular + Spring Boot App with OIDC 32 | 33 | . https://id.heroku.com/login[Log in to Heroku] and create a new app (e.g., `bootiful-angular`). 34 | 35 | . After creating your app, click on the **Resources** tab and add the **Okta** add-on. 36 | + 37 | _Mention that you'll need a credit card to provision add-ons._ 38 | 39 | . Go to your app's **Settings** tab and click the **Reveal Config Vars** button. 40 | 41 | . Create an `okta.env` file in the `notes-api` directory and copy your Oktas config vars into it, where `$OKTA_*` is the value from Heroku. 42 | + 43 | [source,shell] 44 | ---- 45 | export OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER 46 | export OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID_WEB 47 | export OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET_WEB 48 | ---- 49 | + 50 | NOTE: If you're on Windows without https://docs.microsoft.com/en-us/windows/wsl/install-win10[Windows Subsystem for Linux] installed, create an `okta.bat` file and use `SET` instead of `export`. 51 | 52 | . Start your Spring Boot app from the `notes-api` directory. 53 | + 54 | [source,shell] 55 | ---- 56 | source okta.env 57 | ./gradlew bootRun 58 | ---- 59 | + 60 | TIP: Show how to configure environment variables in IDEA for `DemoApplication`. 61 | 62 | . Configure Angular for OIDC authentication by modifying its `auth-routing.module.ts` to use the generated issuer and **SPA** client ID. 63 | 64 | . Install the Angular app's dependencies and start it. 65 | + 66 | [source,shell] 67 | ---- 68 | npm i 69 | ng serve 70 | ---- 71 | 72 | . Log in to `http://localhost:4200` and show how it logs you in straight-away. 73 | 74 | . Log out and show how you can use the credentials from Heroku's config vars to log in. 75 | 76 | . Commit your changes to Git. 77 | + 78 | [source,shell] 79 | ---- 80 | git commit -am "Add Okta OIDC Configuration" 81 | ---- 82 | 83 | === Create a Docker Container for Your Angular App 84 | 85 | . Create a `Dockerfile` that uses Node and Nginx as a web server. [`ng-docker`] 86 | + 87 | [source,docker] 88 | .notes/Dockerfile 89 | ---- 90 | FROM node:14.1-alpine AS builder 91 | 92 | WORKDIR /opt/web 93 | COPY package.json package-lock.json ./ 94 | RUN npm install 95 | 96 | ENV PATH="./node_modules/.bin:$PATH" 97 | 98 | COPY . ./ 99 | RUN ng build --prod 100 | 101 | FROM nginx:1.17-alpine 102 | COPY nginx.config /etc/nginx/conf.d/default.conf 103 | COPY --from=builder /opt/web/dist/notes /usr/share/nginx/html 104 | ---- 105 | 106 | . Create `nginx.config` to make Nginx SPA-aware. [`ng-nginx`] 107 | + 108 | [source,config] 109 | .notes/nginx.config 110 | ---- 111 | server { 112 | listen 80; 113 | server_name _; 114 | 115 | root /usr/share/nginx/html; 116 | index index.html; 117 | 118 | location / { 119 | try_files $uri /index.html; 120 | } 121 | } 122 | ---- 123 | 124 | . Build your Docker image. 125 | + 126 | [source,shell] 127 | ---- 128 | docker build -t ng-notes . 129 | ---- 130 | 131 | . Run it locally on port 4200 using the `docker run` command. 132 | + 133 | [source,shell] 134 | ---- 135 | docker run -p 4200:80 ng-notes 136 | ---- 137 | 138 | . You can add these Docker commands as scripts to your `package.json` file. 139 | + 140 | [source,json] 141 | ---- 142 | "docker": "docker build -t ng-notes .", 143 | "ng-notes": "docker run -p 4200:80 ng-notes" 144 | ---- 145 | 146 | NOTE: The `docker run` command will serve up the production version of the Angular app, which has its backend configured to point to `\https://bootiful-angular.herokuapp.com` on Heroku. You'll need to deploy your Spring Boot app to a similar public URL for Angular + Docker to work. 147 | 148 | ==== Deploy Spring Boot to Heroku 149 | 150 | . Open a terminal and log in to your Heroku account. 151 | + 152 | [source,shell] 153 | ---- 154 | heroku login 155 | ---- 156 | 157 | . You should already have a Heroku app that you added Okta to. Let's use it for hosting Spring Boot. Run `heroku apps` and you'll see the one you created. 158 | + 159 | [source,shell] 160 | ---- 161 | heroku apps 162 | ---- 163 | 164 | . Associate your existing Git repo with the app on Heroku. 165 | + 166 | [source,shell] 167 | ---- 168 | heroku git:remote -a $APP_NAME 169 | ---- 170 | 171 | . Set the `APP_BASE` config variable to point to the `notes-api` directory and add buildpacks. 172 | + 173 | [source,shell] 174 | ---- 175 | heroku config:set APP_BASE=notes-api 176 | heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo 177 | heroku buildpacks:add heroku/gradle 178 | ---- 179 | 180 | . Attach a PostgreSQL database to your app. 181 | + 182 | [source,shell] 183 | ---- 184 | heroku addons:create heroku-postgresql 185 | ---- 186 | 187 | . Override the `GRADLE_TASK` config var. 188 | + 189 | [source,shell] 190 | ---- 191 | heroku config:set GRADLE_TASK="bootJar -Pprod" 192 | ---- 193 | 194 | . Run the following command and remove `_WEB` from the two Okta variables that have it. 195 | + 196 | [source,shell] 197 | ---- 198 | heroku config:edit 199 | ---- 200 | 201 | . Deploy to Heroku. 202 | + 203 | [source,shell] 204 | ---- 205 | git push heroku main:master 206 | ---- 207 | 208 | . Run `heroku open` to open your app and show authentication works. 209 | 210 | . By default, JPA is configured to create your database schema each time. Change it to simply validate. 211 | + 212 | [source,shell] 213 | ---- 214 | heroku config:set SPRING_JPA_HIBERNATE_DDL_AUTO=validate 215 | ---- 216 | 217 | . Configure your Angular app to use your Heroku-deployed Spring Boot app for its production URL. 218 | + 219 | [source,typescript] 220 | ---- 221 | export const environment = { 222 | production: true, 223 | apiUrl: 'https://.herokuapp.com' 224 | }; 225 | ---- 226 | 227 | . Add `\http://localhost:4200` as an allowed origin on Heroku. 228 | + 229 | [source,shell] 230 | ---- 231 | heroku config:set ALLOWED_ORIGINS=http://localhost:4200 232 | ---- 233 | 234 | . Rebuild your Angular Docker container and run it. 235 | + 236 | [source,shell] 237 | ---- 238 | npm run docker 239 | npm run ng-notes 240 | ---- 241 | 242 | . Open your browser to `http://localhost:4200`, log in, and confirm you can add notes. Verify data exists on Heroku at `/api/notes`. 243 | 244 | === Deploy Angular + Docker to Heroku 245 | 246 | . If your project has a `Dockerfile`, you can deploy your app directly using the Heroku Container Registry! 247 | 248 | . Make sure you're in the `notes` directory, then log in to Heroku's Container Registry. 249 | + 250 | [source,shell] 251 | ---- 252 | heroku container:login 253 | ---- 254 | 255 | . Create a new app. 256 | + 257 | [source,shell] 258 | ---- 259 | heroku create 260 | ---- 261 | 262 | . Add the Git URL as a new remote named `docker`. 263 | + 264 | [source,shell] 265 | ---- 266 | git remote add docker https://git.heroku.com/.git 267 | ---- 268 | 269 | . Update `nginx.config` so it reads from a `$PORT` environment variable if it's set, otherwise default it to 80. 270 | + 271 | [source,config] 272 | ---- 273 | server { 274 | listen ${PORT:-80}; 275 | server_name _; 276 | 277 | root /usr/share/nginx/html; 278 | index index.html; 279 | 280 | location / { 281 | try_files $$uri /index.html; 282 | } 283 | } 284 | ---- 285 | 286 | . Update your `Dockerfile` so it uses https://github.com/a8m/envsubst[a8m/envsubst], which allows default variables. 287 | + 288 | [source,docker] 289 | ---- 290 | FROM node:14.1-alpine AS builder 291 | 292 | WORKDIR /opt/web 293 | COPY package.json package-lock.json ./ 294 | RUN npm install 295 | 296 | ENV PATH="./node_modules/.bin:$PATH" 297 | 298 | COPY . ./ 299 | RUN ng build --prod 300 | 301 | FROM nginx:1.17-alpine 302 | RUN apk --no-cache add curl 303 | RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m` -o envsubst && \ 304 | chmod +x envsubst && \ 305 | mv envsubst /usr/local/bin 306 | COPY ./nginx.config /etc/nginx/nginx.template 307 | CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/nginx.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] 308 | COPY --from=builder /opt/web/dist/notes /usr/share/nginx/html 309 | ---- 310 | 311 | . Then, push your Docker image to Heroku's Container Registry. 312 | + 313 | [source,shell] 314 | ---- 315 | heroku container:push web --remote docker 316 | ---- 317 | 318 | . Release the image of your app: 319 | + 320 | [source,shell] 321 | ---- 322 | heroku container:release web --remote docker 323 | ---- 324 | 325 | . And open the app in your browser: 326 | + 327 | [source,shell] 328 | ---- 329 | heroku open --remote docker 330 | ---- 331 | 332 | . Update your Spring Boot app to add your new app as an allowed origin. 333 | + 334 | [source,shell] 335 | ---- 336 | heroku config:edit --remote heroku 337 | ---- 338 | 339 | . You'll also need to add your app's URL to Okta as a valid redirect URI. 340 | 341 | . Log in and show previously created note. 342 | 343 | ==== A-Rated Security Headers for Nginx in Docker 344 | 345 | . Test your freshly-deployed Angular app with https://securityheaders.com/[securityheaders.com]. 346 | 347 | . Fix your score by modifying `nginx.config` to add security headers. [`headers-nginx`] 348 | + 349 | [source,config] 350 | ---- 351 | server { 352 | listen ${PORT:-80}; 353 | server_name _; 354 | 355 | root /usr/share/nginx/html; 356 | index index.html; 357 | 358 | location / { 359 | try_files $$uri /index.html; 360 | } 361 | 362 | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-ancestors 'none'; connect-src 'self' https://*.okta.com https://*.herokuapp.com"; 363 | add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin"; 364 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; 365 | add_header X-Content-Type-Options nosniff; 366 | add_header X-Frame-Options DENY; 367 | add_header X-XSS-Protection "1; mode=block"; 368 | add_header Feature-Policy "accelerometer 'none'; camera 'none'; microphone 'none'"; 369 | } 370 | ---- 371 | 372 | . Then, redeploy. 373 | + 374 | [source,shell] 375 | ---- 376 | heroku container:push web --remote docker 377 | heroku container:release web --remote docker 378 | ---- 379 | 380 | . Test again. You should get an **A** this time! 381 | 382 | === Combine Your Angular + Spring Boot App into a Single JAR 383 | 384 | Now I'll show you how to combine Angular + Spring Boot into a single JAR for production. It'll make it easier deploy because 1) single artifact, 2) no CORS, and 3) no access tokens stored in the browser. 385 | 386 | ==== Update Your Angular App’s Authentication Mechanism 387 | 388 | . Create a new `AuthService` for gathering authentication information from Spring Boot. [`ng-authservice`] 389 | + 390 | ==== 391 | [source,typescript] 392 | .notes/src/app/shared/auth.service.ts 393 | ---- 394 | import { Injectable } from '@angular/core'; 395 | import { Location } from '@angular/common'; 396 | import { BehaviorSubject, Observable } from 'rxjs'; 397 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 398 | import { environment } from '../../environments/environment'; 399 | import { User } from './user'; 400 | import { map } from 'rxjs/operators'; 401 | 402 | const headers = new HttpHeaders().set('Accept', 'application/json'); 403 | 404 | @Injectable({ 405 | providedIn: 'root' 406 | }) 407 | export class AuthService { 408 | $authenticationState = new BehaviorSubject(false); 409 | 410 | constructor(private http: HttpClient, private location: Location) { 411 | } 412 | 413 | getUser(): Observable { 414 | return this.http.get(`${environment.apiUrl}/user`, {headers}).pipe( 415 | map((response: User) => { 416 | if (response !== null) { 417 | this.$authenticationState.next(true); 418 | return response; 419 | } 420 | }) 421 | ); 422 | } 423 | 424 | isAuthenticated(): Promise { 425 | return this.getUser().toPromise().then((user: User) => { // <1> 426 | return user !== undefined; 427 | }).catch(() => { 428 | return false; 429 | }) 430 | } 431 | 432 | login(): void { 433 | location.href = 434 | `${location.origin}${this.location.prepareExternalUrl('oauth2/authorization/okta')}`; // <2> 435 | } 436 | 437 | logout(): void { 438 | const redirectUri = `${location.origin}${this.location.prepareExternalUrl('/')}`; 439 | 440 | this.http.post(`${environment.apiUrl}/api/logout`, {}).subscribe((response: any) => { // <3> 441 | location.href = response.logoutUrl + '?id_token_hint=' + response.idToken 442 | + '&post_logout_redirect_uri=' + redirectUri; 443 | }); 444 | } 445 | } 446 | ---- 447 | <.> Talk to the `/users` endpoint to determine authenticated status. A username will be return if the user is logged in. 448 | <.> When the user clicks a login button, redirect them to a Spring Security endpoint to do the OAuth dance. 449 | <.> Logout using the `/api/logout` endpoint, which returns the Okta Logout API URL and a valid ID token. 450 | ==== 451 | 452 | . Create a `user.ts` file in the same directory. 453 | + 454 | [source,typescript] 455 | ---- 456 | export class User { 457 | sub: number; 458 | fullName: string; 459 | } 460 | ---- 461 | 462 | . Update `app.component.ts` to use your new `AuthService`. 463 | + 464 | [source,typescript] 465 | ---- 466 | import { Component, OnInit } from '@angular/core'; 467 | import { AuthService } from './shared/auth.service'; 468 | 469 | @Component({ 470 | selector: 'app-root', 471 | templateUrl: './app.component.html', 472 | styleUrls: ['./app.component.scss'] 473 | }) 474 | export class AppComponent implements OnInit { 475 | title = 'Notes'; 476 | isAuthenticated: boolean; 477 | isCollapsed = true; 478 | 479 | constructor(public auth: AuthService) { 480 | } 481 | 482 | async ngOnInit() { 483 | this.isAuthenticated = await this.auth.isAuthenticated(); 484 | this.auth.$authenticationState.subscribe( 485 | (isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated 486 | ); 487 | } 488 | } 489 | ---- 490 | 491 | . Remove `OktaAuthModule` and its related code from `app.component.spec.ts` and `home.component.spec.ts`. Add `HttpClientTestingModule` to their `TestBed` imports. 492 | 493 | . Change the buttons in `app.component.html` to reference the `auth` service and its methods. 494 | + 495 | [source,html] 496 | ---- 497 | 499 | 501 | ---- 502 | 503 | . Update `home.component.ts` to use `AuthService` too. 504 | + 505 | [source,typescript] 506 | ---- 507 | import { Component, OnInit } from '@angular/core'; 508 | import { AuthService } from '../shared/auth.service'; 509 | 510 | @Component({ 511 | selector: 'app-home', 512 | templateUrl: './home.component.html', 513 | styleUrls: ['./home.component.scss'] 514 | }) 515 | export class HomeComponent implements OnInit { 516 | isAuthenticated: boolean; 517 | 518 | constructor(public auth: AuthService) { 519 | } 520 | 521 | async ngOnInit() { 522 | this.isAuthenticated = await this.auth.isAuthenticated(); 523 | } 524 | } 525 | ---- 526 | 527 | . Delete `auth-routing.module.ts` and `shared/okta`. 528 | 529 | . Modify `app.module.ts` to remove the `AuthRoutingModule` import, add `HomeComponent` as a declaration, and import `HttpClientModule`. 530 | + 531 | [source,typescript] 532 | ---- 533 | import { BrowserModule } from '@angular/platform-browser'; 534 | import { NgModule } from '@angular/core'; 535 | 536 | import { AppRoutingModule } from './app-routing.module'; 537 | import { AppComponent } from './app.component'; 538 | import { NoteModule } from './note/note.module'; 539 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 540 | import { HomeComponent } from './home/home.component'; 541 | import { HttpClientModule } from '@angular/common/http'; 542 | 543 | @NgModule({ 544 | declarations: [ 545 | AppComponent, 546 | HomeComponent 547 | ], 548 | imports: [ 549 | BrowserModule, 550 | AppRoutingModule, 551 | HttpClientModule, 552 | NoteModule, 553 | NgbModule 554 | ], 555 | providers: [], 556 | bootstrap: [AppComponent] 557 | }) 558 | export class AppModule { } 559 | ---- 560 | 561 | . Add the route for `HomeComponent` to `app-routing.module.ts`. 562 | + 563 | [source,typescript] 564 | ---- 565 | import { HomeComponent } from './home/home.component'; 566 | 567 | const routes: Routes = [ 568 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 569 | { 570 | path: 'home', 571 | component: HomeComponent 572 | } 573 | ]; 574 | ---- 575 | 576 | . Change both `environments.ts` and `environments.prod.ts` to use a blank `apiUrl`. 577 | + 578 | [source,typescript] 579 | ---- 580 | apiUrl: '' 581 | ---- 582 | 583 | . Create a `src/proxy.conf.js` file to proxy requests to Spring Boot. [`ng-proxy`] 584 | + 585 | [source,javascript] 586 | ---- 587 | const PROXY_CONFIG = [ 588 | { 589 | context: ['/user', '/api', '/oauth2', '/login'], 590 | target: 'http://localhost:8080', 591 | secure: false, 592 | logLevel: 'debug' 593 | } 594 | ] 595 | 596 | module.exports = PROXY_CONFIG; 597 | ---- 598 | 599 | . Add this file as a `proxyConfig` option in `angular.json`. 600 | + 601 | [source,json] 602 | ---- 603 | "serve": { 604 | "builder": "@angular-devkit/build-angular:dev-server", 605 | "options": { 606 | "browserTarget": "notes:build", 607 | "proxyConfig": "src/proxy.conf.js" 608 | }, 609 | ... 610 | }, 611 | ---- 612 | 613 | . Remove Okta's Angular SDK and OktaDev Schematics from your Angular project. 614 | + 615 | [source,shell] 616 | ---- 617 | npm uninstall @okta/okta-angular @oktadev/schematics 618 | ---- 619 | 620 | ==== Configure Spring Boot to Host an Angular SPA 621 | 622 | In your Spring Boot app, you'll need to change it to build your Angular app, configure it to be SPA-aware, and adjust security settings for static file access. 623 | 624 | . Delete `HomeController.kt`. It's no longer needed since Angular will be served up at `/`. 625 | 626 | . Create a `RouteController.kt` that routes all requests to `index.html`. [`boot-spa`] 627 | + 628 | [source,kotlin] 629 | ---- 630 | package com.okta.developer.notes 631 | 632 | import org.springframework.stereotype.Controller 633 | import org.springframework.web.bind.annotation.RequestMapping 634 | import javax.servlet.http.HttpServletRequest 635 | 636 | @Controller 637 | class RouteController { 638 | 639 | @RequestMapping(value = ["/{path:[^\\.]*}"]) 640 | fun redirect(request: HttpServletRequest): String { 641 | return "forward:/" 642 | } 643 | } 644 | ---- 645 | 646 | . Modify `SecurityConfiguration.kt` to allow anonymous access to static web files, the `/user` info endpoint, and to add additional security headers. 647 | + 648 | [source,kotlin] 649 | ---- 650 | package com.okta.developer.notes 651 | 652 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 653 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 654 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 655 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository 656 | import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter 657 | import org.springframework.security.web.util.matcher.RequestMatcher 658 | 659 | @EnableWebSecurity 660 | class SecurityConfiguration : WebSecurityConfigurerAdapter() { 661 | 662 | override fun configure(http: HttpSecurity) { 663 | //@formatter:off 664 | http 665 | .authorizeRequests() 666 | .antMatchers("/**/*.{js,html,css}").permitAll() 667 | .antMatchers("/", "/user").permitAll() 668 | .anyRequest().authenticated() 669 | .and() 670 | .oauth2Login() 671 | .and() 672 | .oauth2ResourceServer().jwt() 673 | 674 | ... 675 | 676 | http.headers() 677 | .contentSecurityPolicy("script-src 'self'; report-to /csp-report-endpoint/") 678 | .and() 679 | .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN) 680 | .and() 681 | .featurePolicy("accelerometer 'none'; camera 'none'; microphone 'none'") 682 | 683 | //@formatter:on 684 | } 685 | } 686 | ---- 687 | 688 | . Update the `user()` method in `UserController.kt` to make `OidcUser` optional. 689 | + 690 | [source,kotlin] 691 | ---- 692 | @GetMapping("/user") 693 | fun user(@AuthenticationPrincipal user: OidcUser?): OidcUser? { 694 | return user; 695 | } 696 | ---- 697 | 698 | . Add a `LogoutController` that will handle expiring the session and logging out from Okta. [`boot-logout`] 699 | + 700 | [source,kotlin] 701 | ---- 702 | package com.okta.developer.notes 703 | 704 | import org.springframework.http.ResponseEntity 705 | import org.springframework.security.core.annotation.AuthenticationPrincipal 706 | import org.springframework.security.oauth2.client.registration.ClientRegistration 707 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository 708 | import org.springframework.security.oauth2.core.oidc.OidcIdToken 709 | import org.springframework.web.bind.annotation.PostMapping 710 | import org.springframework.web.bind.annotation.RestController 711 | import javax.servlet.http.HttpServletRequest 712 | 713 | @RestController 714 | class LogoutController(val clientRegistrationRepository: ClientRegistrationRepository) { 715 | 716 | val registration: ClientRegistration = clientRegistrationRepository.findByRegistrationId("okta"); 717 | 718 | @PostMapping("/api/logout") 719 | fun logout(request: HttpServletRequest, 720 | @AuthenticationPrincipal(expression = "idToken") idToken: OidcIdToken): ResponseEntity<*> { 721 | val logoutUrl = this.registration.providerDetails.configurationMetadata["end_session_endpoint"] 722 | val logoutDetails: MutableMap = HashMap() 723 | logoutDetails["logoutUrl"] = logoutUrl.toString() 724 | logoutDetails["idToken"] = idToken.tokenValue 725 | request.session.invalidate() 726 | return ResponseEntity.ok().body>(logoutDetails) 727 | } 728 | } 729 | ---- 730 | 731 | . Add a `server.port` property to `application-prod.properties` that uses a `PORT` environment variable, if it's set. 732 | + 733 | [source,properties] 734 | ---- 735 | server.port=${PORT:8080} 736 | ---- 737 | 738 | . Modify `application*.properties` so the email is returned by `${principle.name}`. 739 | + 740 | [source,properties] 741 | ---- 742 | spring.security.oauth2.client.provider.okta.user-name-attribute=preferred_username 743 | ---- 744 | 745 | . Remove the `allowed.origins` property from both files too. 746 | 747 | . Remove the body of `DemoApplication` since CORS is no longer needed. 748 | 749 | ==== Modify Gradle to Build a Single JAR 750 | 751 | . Import `NpmTask` and add the Node Gradle plugin to `build.gradle.kts`. 752 | + 753 | [source,kotlin] 754 | ---- 755 | import com.moowork.gradle.node.npm.NpmTask 756 | 757 | plugins { 758 | ... 759 | id("com.github.node-gradle.node") version "2.2.4" 760 | ... 761 | } 762 | ---- 763 | 764 | . Define the location of your Angular app and configuration for the Node plugin. [`gradle-spa`] 765 | + 766 | [source,kotlin] 767 | ---- 768 | val spa = "${projectDir}/../notes"; 769 | 770 | node { 771 | version = "12.16.2" 772 | nodeModulesDir = file(spa) 773 | } 774 | ---- 775 | 776 | . Add a `buildWeb` task: [`gradle-web`] 777 | + 778 | [source,kotlin] 779 | ---- 780 | val buildWeb = tasks.register("buildNpm") { 781 | dependsOn(tasks.npmInstall) 782 | setNpmCommand("run", "build") 783 | setArgs(listOf("--", "--prod")) 784 | inputs.dir("${spa}/src") 785 | inputs.dir(fileTree("${spa}/node_modules").exclude("${spa}/.cache")) 786 | outputs.dir("${spa}/dist") 787 | } 788 | ---- 789 | 790 | . Modify the `processResources` task to build Angular when `-Pprod` is passed in. [`gradle-resources`] 791 | + 792 | [source,kotlin] 793 | ---- 794 | tasks.processResources { 795 | rename("application-${profile}.properties", "application.properties") 796 | if (profile == "prod") { 797 | dependsOn(buildWeb) 798 | from("${spa}/dist/notes") { 799 | into("static") 800 | } 801 | } 802 | } 803 | ---- 804 | 805 | . Build both apps using `./gradlew bootJar -Pprod`. 806 | 807 | . Run it with the following commands to ensure everything works. 808 | + 809 | [source,shell] 810 | ---- 811 | docker-compose -f src/main/docker/postgresql.yml up -d 812 | source okta.env 813 | java -jar build/libs/*.jar 814 | ---- 815 | 816 | === Dockerize Angular + Spring Boot with Jib 817 | 818 | . Add Jib's Gradle plugin for building Docker containers. 819 | + 820 | [source,kotlin] 821 | ---- 822 | plugins { 823 | ... 824 | id("com.google.cloud.tools.jib") version "2.4.0" 825 | } 826 | ---- 827 | 828 | . Add Jib configuration to specify your image name and the active Spring profile. [`gradle-jib`] 829 | + 830 | [source,kotlin] 831 | ---- 832 | jib { 833 | to { 834 | image = "/bootiful-angular" 835 | } 836 | container { 837 | environment = mapOf("SPRING_PROFILES_ACTIVE" to profile) 838 | } 839 | } 840 | ---- 841 | 842 | . Build a Docker image with Jib. 843 | + 844 | [source,shell] 845 | ---- 846 | ./gradlew jibDockerBuild -Pprod 847 | ---- 848 | 849 | ==== Run Your Spring Boot Docker App with Docker Compose 850 | 851 | . In theory, you should be able to run the following command to run your app. 852 | + 853 | [source,shell] 854 | ---- 855 | docker run --publish=8080:8080 /bootiful-angular 856 | ---- 857 | + 858 | However, it won't work because there's no Okta environment variables specified. You could pass them in via the command line, but that's a pain. Docker Compose to the rescue! 859 | 860 | . Copy `notes-api/okta.env` to `src/main/docker/.env` and change it to remove `export ` at the beginning of each line. 861 | 862 | . Create `src/main/docker/app.yml`. 863 | + 864 | [source,yaml] 865 | ---- 866 | version: '2' 867 | services: 868 | boot-app: 869 | image: /bootiful-angular 870 | environment: 871 | - SPRING_DATASOURCE_URL=jdbc:postgresql://notes-postgresql:5432/notes 872 | - OKTA_OAUTH2_ISSUER=${OKTA_OAUTH2_ISSUER} 873 | - OKTA_OAUTH2_CLIENT_ID=${OKTA_OAUTH2_CLIENT_ID} 874 | - OKTA_OAUTH2_CLIENT_SECRET=${OKTA_OAUTH2_CLIENT_SECRET} 875 | ports: 876 | - 8080:8080 877 | depends_on: 878 | - notes-postgresql 879 | notes-postgresql: 880 | extends: 881 | file: postgresql.yml 882 | service: notes-postgresql 883 | ---- 884 | 885 | . Create a symlink in the `note-api` directory so you can run Docker Compose from there. 886 | 887 | ln -s src/main/docker/.env 888 | 889 | . Start your Docker container. 890 | 891 | docker-compose -f src/main/docker/app.yml up 892 | 893 | ==== Deploy Your Spring Boot + Angular Container to Docker Hub 894 | 895 | . https://hub.docker.com/signup[Create a Docker Hub account] if you don't have one. 896 | 897 | . Run `docker login` to log in to your account, then use the `jib` task to build *and* deploy your image. 898 | + 899 | [source,shell] 900 | ---- 901 | ./gradlew jib -Pprod 902 | ---- 903 | 904 | . Rejoice in how Jib makes it so you don't need a `Dockerfile`! 905 | 906 | === Heroku 💜 Spring Boot + Docker 907 | 908 | . To deploy as a container to Heroku, create a new app and add it as a Git remote. 909 | + 910 | [source,shell] 911 | ---- 912 | heroku create 913 | git remote add jib https://git.heroku.com/.git 914 | ---- 915 | 916 | . Add PostgreSQL to this app and configure it for Spring Boot using the following commands: 917 | + 918 | [source,shell] 919 | ---- 920 | heroku addons:create heroku-postgresql --remote jib 921 | heroku config:get DATABASE_URL --remote jib 922 | heroku config:set SPRING_DATASOURCE_URL=jdbc:postgresql:// --remote jib 923 | heroku config:set SPRING_DATASOURCE_USERNAME= --remote jib 924 | heroku config:set SPRING_DATASOURCE_PASSWORD= --remote jib 925 | ---- 926 | 927 | . Add Okta to your app. 928 | + 929 | [source,shell] 930 | ---- 931 | heroku addons:create okta --remote jib 932 | ---- 933 | 934 | . Modify the Okta environment variables to remove the `_WEB` on the two keys that have it. 935 | + 936 | [source,shell] 937 | ---- 938 | heroku config:edit --remote jib 939 | ---- 940 | 941 | . Run the commands below to deploy the image you deployed to Docker Hub. 942 | + 943 | [source,shell] 944 | ---- 945 | docker tag /bootiful-angular registry.heroku.com//web 946 | docker push registry.heroku.com//web 947 | heroku container:release web --remote jib 948 | ---- 949 | 950 | . You can watch the logs to see if it started successfully. 951 | + 952 | [source,shell] 953 | ---- 954 | heroku logs --tail --remote jib 955 | ---- 956 | 957 | . After it starts, set the JPA configuration so it only validates the schema. 958 | + 959 | [source,shell] 960 | ---- 961 | heroku config:set SPRING_JPA_HIBERNATE_DDL_AUTO=validate --remote jib 962 | ---- 963 | 964 | . Make sure your Dockerfied Angular + Spring Boot app works and test its headers on https://securityheaders.com[securityheaders.com]. 965 | 966 | === Knative 💙 Spring Boot + Docker 967 | 968 | . Create a https://cloud.google.com/[Google Cloud account] and click **Get started for free**. 969 | 970 | . Go to https://console.cloud.google.com/[Google Cloud Console] and create a new project. 971 | 972 | . Click on the Terminal icon in the top right to open a Cloud Shell terminal for your project 973 | 974 | . Enable Cloud and Container APIs: 975 | + 976 | [source,shell] 977 | ---- 978 | gcloud services enable \ 979 | cloudapis.googleapis.com \ 980 | container.googleapis.com \ 981 | containerregistry.googleapis.com 982 | ---- 983 | 984 | . Then set your default zone and region: 985 | + 986 | [source,shell] 987 | ---- 988 | gcloud config set compute/zone us-central1-c 989 | gcloud config set compute/region us-central1 990 | ---- 991 | 992 | . Create a Kubernetes cluster: 993 | + 994 | [source,shell] 995 | ---- 996 | gcloud beta container clusters create knative \ 997 | --addons=HorizontalPodAutoscaling,HttpLoadBalancing \ 998 | --machine-type=n1-standard-4 \ 999 | --cluster-version=1.15 \ 1000 | --enable-stackdriver-kubernetes --enable-ip-alias \ 1001 | --enable-autoscaling --min-nodes=5 --num-nodes=5 --max-nodes=10 \ 1002 | --enable-autorepair \ 1003 | --scopes cloud-platform 1004 | ---- 1005 | 1006 | . Set up a cluster administrator and install Istio. 1007 | + 1008 | [source,shell] 1009 | ---- 1010 | kubectl create clusterrolebinding cluster-admin-binding \ 1011 | --clusterrole=cluster-admin \ 1012 | --user=$(gcloud config get-value core/account) 1013 | 1014 | kubectl apply -f \ 1015 | https://github.com/knative/serving/raw/v0.14.0/third_party/istio-1.5.1/istio-crds.yaml 1016 | 1017 | while [[ $(kubectl get crd gateways.networking.istio.io -o jsonpath='{.status.conditions[?(@.type=="Established")].status}') != 'True' ]]; do 1018 | echo "Waiting on Istio CRDs"; sleep 1 1019 | done 1020 | 1021 | kubectl apply -f \ 1022 | https://github.com/knative/serving/raw/v0.14.0/third_party/istio-1.5.1/istio-minimal.yaml 1023 | ---- 1024 | 1025 | . Install Knative: 1026 | + 1027 | [source,shell] 1028 | ---- 1029 | kubectl apply --selector knative.dev/crd-install=true -f \ 1030 | https://github.com/knative/serving/releases/download/v0.14.0/serving.yaml 1031 | 1032 | kubectl apply -f \ 1033 | https://github.com/knative/serving/releases/download/v0.14.0/serving.yaml 1034 | 1035 | while [[ $(kubectl get svc istio-ingressgateway -n istio-system \ 1036 | -o 'jsonpath={.status.loadBalancer.ingress[0].ip}') == '' ]]; do 1037 | echo "Waiting on external IP"; sleep 1 1038 | done 1039 | ---- 1040 | 1041 | . You'll need a domain to enable HTTPS, so set that up and point it to the cluster's IP address. 1042 | + 1043 | [source,shell] 1044 | ---- 1045 | export IP_ADDRESS=$(kubectl get svc istio-ingressgateway -n istio-system \ 1046 | -o 'jsonpath={.status.loadBalancer.ingress[0].ip}') 1047 | echo $IP_ADDRESS 1048 | 1049 | kubectl apply -f - <` placeholders to match your values first. 1118 | + 1119 | [source,shell] 1120 | ---- 1121 | kubectl apply -f - < 1157 | volumeMounts: 1158 | - mountPath: /var/lib/postgresql/data 1159 | name: pgdata 1160 | subPath: data 1161 | volumes: 1162 | - name: pgdata 1163 | persistentVolumeClaim: 1164 | claimName: pgdata 1165 | --- 1166 | apiVersion: v1 1167 | kind: Service 1168 | metadata: 1169 | name: pgservice 1170 | spec: 1171 | ports: 1172 | - port: 5432 1173 | name: pgservice 1174 | clusterIP: None 1175 | selector: 1176 | service: postgres 1177 | --- 1178 | apiVersion: serving.knative.dev/v1alpha1 1179 | kind: Service 1180 | metadata: 1181 | name: bootiful-angular 1182 | spec: 1183 | template: 1184 | spec: 1185 | containers: 1186 | - image: /bootiful-angular 1187 | env: 1188 | - name: SPRING_DATASOURCE_URL 1189 | value: jdbc:postgresql://pgservice:5432/bootiful-angular 1190 | - name: SPRING_DATASOURCE_USERNAME 1191 | value: bootiful-angular 1192 | - name: SPRING_DATASOURCE_PASSWORD 1193 | value: 1194 | - name: OKTA_OAUTH2_ISSUER 1195 | value: 1196 | - name: OKTA_OAUTH2_CLIENT_ID 1197 | value: 1198 | - name: OKTA_OAUTH2_CLIENT_SECRET 1199 | value: 1200 | EOF 1201 | ---- 1202 | 1203 | . Get the URL of your app. 1204 | + 1205 | [source,shell] 1206 | ---- 1207 | kubectl get ksvc bootiful-angular 1208 | ---- 1209 | 1210 | . Verify your app is running, then add redirect URIs on Okta, and log in. 1211 | 1212 | . Run the command below to change it so Hibernate doesn't try to recreate your schema on restart. 1213 | + 1214 | [source,shell] 1215 | ---- 1216 | kubectl apply -f - </bootiful-angular 1226 | env: 1227 | - name: SPRING_DATASOURCE_URL 1228 | value: jdbc:postgresql://pgservice:5432/bootiful-angular 1229 | - name: SPRING_DATASOURCE_USERNAME 1230 | value: bootiful-angular 1231 | - name: SPRING_DATASOURCE_PASSWORD 1232 | value: 1233 | - name: OKTA_OAUTH2_ISSUER 1234 | value: 1235 | - name: OKTA_OAUTH2_CLIENT_ID 1236 | value: 1237 | - name: OKTA_OAUTH2_CLIENT_SECRET 1238 | value: 1239 | - name: SPRING_JPA_HIBERNATE_DDL_AUTO 1240 | value: validate 1241 | EOF 1242 | ---- 1243 | 1244 | === Cloud Foundry 💚 Spring Boot + Docker 1245 | 1246 | . Create a https://run.pivotal.io/[Pivotal Web Services account]. 1247 | 1248 | . Install the https://docs.cloudfoundry.org/cf-cli/install-go-cli.html[Cloud Foundry CLI]. 1249 | 1250 | brew install cloudfoundry/tap/cf-cli 1251 | 1252 | . Run the following commands, where `secure-notes` is a unique name for your app. 1253 | + 1254 | [source,shell] 1255 | ---- 1256 | cf login 1257 | 1258 | # Deploy the image from Docker Hub 1259 | cf push --no-start -o /bootiful-angular secure-notes 1260 | 1261 | # Create a PostgreSQL instance 1262 | cf cs elephantsql turtle secure-notes-psql 1263 | 1264 | # Bind the app to the PostgreSQL instance 1265 | cf bs secure-notes secure-notes-psql 1266 | 1267 | # Display the credentials from the PostgreSQL instance 1268 | cf env secure-notes 1269 | ---- 1270 | 1271 | . To get your PostgreSQL URL run the following command where `secure-notes` is your app name. 1272 | 1273 | cf env secure-notes 1274 | + 1275 | Make sure to replace `postgres://` with `jdbc:postgresql://` when setting the datasource URL. 1276 | 1277 | . Set environment variables for connecting to PostgreSQL and Okta.s 1278 | + 1279 | [source,shell] 1280 | ---- 1281 | export APP_NAME= 1282 | cf set-env $APP_NAME SPRING_DATASOURCE_DRIVER_CLASS_NAME org.postgresql.Driver 1283 | cf set-env $APP_NAME SPRING_DATASOURCE_URL 1284 | cf set-env $APP_NAME SPRING_DATASOURCE_USERNAME 1285 | cf set-env $APP_NAME SPRING_DATASOURCE_PASSWORD 1286 | cf set-env $APP_NAME OKTA_OAUTH2_ISSUER 1287 | cf set-env $APP_NAME OKTA_OAUTH2_CLIENT_ID 1288 | cf set-env $APP_NAME OKTA_OAUTH2_CLIENT_SECRET 1289 | cf restage $APP_NAME 1290 | ---- 1291 | 1292 | . Run `cf start secure-notes` and your app should be available at `\http://.cfapps.io`. 1293 | 1294 | . You'll need to add its URL (+ `/login/oauth2/code/okta`) as a **Login redirect URI** and **Logout redirect URI** on Okta in order to log in. 1295 | 1296 | . You'll also want to configure JPA so it doesn't recreate the schema on each restart. 1297 | + 1298 | [source,shell] 1299 | ---- 1300 | cf set-env $APP_NAME SPRING_JPA_HIBERNATE_DDL_AUTO validate 1301 | ---- 1302 | 1303 | === Use Cloud Native Buildpacks to Build Docker Images 1304 | 1305 | https://buildpacks.io/[Cloud Native Buildpacks] is an initiative that was started by Pivotal and Heroku in early 2018. It has a https://github.com/buildpacks/pack[`pack` CLI] that allows you to build Docker images using buildpacks. 1306 | 1307 | Unfortunately, `pack` doesn't have great support for monorepos (especially in sub-directories) yet. I was unable to make it work with this app structure. 1308 | 1309 | Spring Boot 2.3 to the rescue! 1310 | 1311 | === Easy Docker Images with Spring Boot 2.3 1312 | 1313 | https://spring.io/blog/2020/05/15/spring-boot-2-3-0-available-now[Spring Boot 2.3.0 is now available] and with it comes built-in Docker support. It leverages Cloud Native Buildpacks, just like the `pack` CLI. 1314 | 1315 | Spring Boot's Maven and Gradle plugins both have new commands: 1316 | 1317 | - `./mvnw spring-boot:build-image` 1318 | - `./gradlew bootBuildImage` 1319 | 1320 | The https://paketo.io/[Paketo] Java buildpack is used by default to create images. 1321 | 1322 | By default, Spring Boot will use your `$artifactId:$version` for the image name. That is, `notes-api:0.0.1-SNAPSHOT`. You can override this with an `--imageName` parameter. 1323 | 1324 | . Build and run the image with the commands below. 1325 | + 1326 | [source,shell] 1327 | ---- 1328 | ./gradlew bootBuildImage --imageName mraible/bootiful-angular -Pprod 1329 | docker-compose -f src/main/docker/app.yml up 1330 | ---- 1331 | 1332 | . Open a browser to `http://localhost:8080`, log in, and add notes. Pretty neat, don't you think!? 😃 1333 | 1334 | TIP: Learn more in https://twitter.com/phillip_webb[Phil Webb's] https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1[Creating Docker images with Spring Boot 2.3.0.M1] blog post or his excellent https://spring.io/blog/2020/06/18/what-s-new-in-spring-boot-2-3[What's new in Spring Boot 2.3] video. 1335 | 1336 | == Containers FTW! 1337 | 1338 | ⚡️ Find the code on GitHub: https://github.com/oktadeveloper/okta-angular-spring-boot-docker-example[@oktadeveloper/okta-angular-spring-boot-docker-example]. 1339 | 1340 | 🚀 Read the blog post: https://developer.okta.com/blog/2020/06/17/angular-docker-spring-boot[Angular + Docker with a Big Hug from Spring Boot]. 1341 | -------------------------------------------------------------------------------- /notes-api/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Okta ### 35 | *.env 36 | -------------------------------------------------------------------------------- /notes-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import com.moowork.gradle.node.npm.NpmTask 3 | 4 | plugins { 5 | id("org.springframework.boot") version "2.3.1.RELEASE" 6 | id("io.spring.dependency-management") version "1.0.9.RELEASE" 7 | id("se.patrikerdes.use-latest-versions") version "0.2.14" 8 | id("com.github.ben-manes.versions") version "0.28.0" 9 | id("com.github.node-gradle.node") version "2.2.4" 10 | id("com.google.cloud.tools.jib") version "2.4.0" 11 | kotlin("jvm") version "1.3.61" 12 | kotlin("plugin.spring") version "1.3.61" 13 | kotlin("plugin.jpa") version "1.3.61" 14 | } 15 | 16 | group = "com.okta.developer" 17 | version = "0.0.1-SNAPSHOT" 18 | java.sourceCompatibility = JavaVersion.VERSION_11 19 | 20 | val spa = "${projectDir}/../notes"; 21 | 22 | node { 23 | version = "12.16.2" 24 | nodeModulesDir = file(spa) 25 | } 26 | 27 | repositories { 28 | mavenCentral() 29 | } 30 | 31 | dependencies { 32 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 33 | implementation("org.springframework.boot:spring-boot-starter-data-rest") 34 | implementation("org.springframework.boot:spring-boot-starter-web") 35 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 36 | implementation("com.okta.spring:okta-spring-boot-starter:1.4.0") 37 | implementation("org.jetbrains.kotlin:kotlin-reflect") 38 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 39 | if (project.hasProperty("prod")) { 40 | runtimeOnly("org.postgresql:postgresql") 41 | } else { 42 | runtimeOnly("com.h2database:h2") 43 | } 44 | testImplementation("org.springframework.boot:spring-boot-starter-test") { 45 | exclude(group = "org.junit.vintage", module = "junit-vintage-engine") 46 | } 47 | } 48 | 49 | tasks.withType { 50 | useJUnitPlatform() 51 | } 52 | 53 | tasks.withType { 54 | kotlinOptions { 55 | freeCompilerArgs = listOf("-Xjsr305=strict") 56 | jvmTarget = "1.8" 57 | } 58 | } 59 | 60 | val buildWeb = tasks.register("buildNpm") { 61 | dependsOn(tasks.npmInstall) 62 | setNpmCommand("run", "build") 63 | setArgs(listOf("--", "--prod")) 64 | inputs.dir("${spa}/src") 65 | inputs.dir(fileTree("${spa}/node_modules").exclude("${spa}/.cache")) 66 | outputs.dir("${spa}/dist") 67 | } 68 | 69 | val profile = if (project.hasProperty("prod")) "prod" else "dev" 70 | 71 | tasks.bootRun { 72 | args("--spring.profiles.active=${profile}") 73 | } 74 | 75 | tasks.processResources { 76 | rename("application-${profile}.properties", "application.properties") 77 | if (profile == "prod") { 78 | dependsOn(buildWeb) 79 | from("${spa}/dist/notes") { 80 | into("static") 81 | } 82 | } 83 | } 84 | 85 | jib { 86 | to { 87 | image = "mraible/bootiful-angular" 88 | } 89 | container { 90 | environment = mapOf("SPRING_PROFILES_ACTIVE" to profile) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /notes-api/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/okta-angular-spring-boot-docker-example/c6405578e4248a58ebc363148bd008f9424e3304/notes-api/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /notes-api/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /notes-api/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /notes-api/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /notes-api/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "notes-api" 2 | -------------------------------------------------------------------------------- /notes-api/src/main/docker/app.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | boot-app: 4 | image: mraible/bootiful-angular 5 | environment: 6 | - SPRING_DATASOURCE_URL=jdbc:postgresql://notes-postgresql:5432/notes 7 | - OKTA_OAUTH2_ISSUER=${OKTA_OAUTH2_ISSUER} 8 | - OKTA_OAUTH2_CLIENT_ID=${OKTA_OAUTH2_CLIENT_ID} 9 | - OKTA_OAUTH2_CLIENT_SECRET=${OKTA_OAUTH2_CLIENT_SECRET} 10 | ports: 11 | - 8080:8080 12 | depends_on: 13 | - notes-postgresql 14 | notes-postgresql: 15 | extends: 16 | file: postgresql.yml 17 | service: notes-postgresql 18 | -------------------------------------------------------------------------------- /notes-api/src/main/docker/postgresql.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | notes-postgresql: 4 | image: postgres:12.1 5 | environment: 6 | - POSTGRES_USER=notes 7 | - POSTGRES_PASSWORD= 8 | ports: 9 | - 5432:5432 10 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/DataInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.boot.ApplicationArguments 4 | import org.springframework.boot.ApplicationRunner 5 | import org.springframework.context.annotation.Profile 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | @Profile("dev") 10 | class DataInitializer(val repository: NotesRepository) : ApplicationRunner { 11 | 12 | @Throws(Exception::class) 13 | override fun run(args: ApplicationArguments) { 14 | for (x in 0..1000) { 15 | repository.save(Note(title = "Note ${x}", username = "matt.raible@okta.com")) 16 | } 17 | repository.findAll().forEach { println(it) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/DemoApplication.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.boot.runApplication 6 | import org.springframework.data.domain.Page 7 | import org.springframework.data.domain.Pageable 8 | import org.springframework.data.jpa.repository.JpaRepository 9 | import org.springframework.data.rest.core.annotation.HandleBeforeCreate 10 | import org.springframework.data.rest.core.annotation.RepositoryEventHandler 11 | import org.springframework.data.rest.core.annotation.RepositoryRestResource 12 | import org.springframework.security.core.context.SecurityContextHolder 13 | import org.springframework.stereotype.Component 14 | import javax.persistence.Entity 15 | import javax.persistence.GeneratedValue 16 | import javax.persistence.Id 17 | 18 | @SpringBootApplication 19 | class DemoApplication 20 | 21 | fun main(args: Array) { 22 | runApplication(*args) 23 | } 24 | 25 | @Entity 26 | data class Note(@Id @GeneratedValue var id: Long? = null, 27 | var title: String? = null, 28 | var text: String? = null, 29 | @JsonIgnore var username: String? = null) 30 | 31 | @RepositoryRestResource 32 | interface NotesRepository : JpaRepository { 33 | fun findAllByUsername(name: String, pageable: Pageable): Page 34 | fun findAllByUsernameAndTitleContainingIgnoreCase(name: String, title: String, pageable: Pageable): Page 35 | } 36 | 37 | @Component 38 | @RepositoryEventHandler(Note::class) 39 | class AddUserToNote { 40 | 41 | @HandleBeforeCreate 42 | fun handleCreate(note: Note) { 43 | val username: String = SecurityContextHolder.getContext().getAuthentication().name 44 | println("Creating note: $note with user: $username") 45 | note.username = username 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/LogoutController.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.http.ResponseEntity 4 | import org.springframework.security.core.annotation.AuthenticationPrincipal 5 | import org.springframework.security.oauth2.client.registration.ClientRegistration 6 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository 7 | import org.springframework.security.oauth2.core.oidc.OidcIdToken 8 | import org.springframework.web.bind.annotation.PostMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | import javax.servlet.http.HttpServletRequest 11 | 12 | @RestController 13 | class LogoutController(val clientRegistrationRepository: ClientRegistrationRepository) { 14 | 15 | val registration: ClientRegistration = clientRegistrationRepository.findByRegistrationId("okta"); 16 | 17 | @PostMapping("/api/logout") 18 | fun logout(request: HttpServletRequest, 19 | @AuthenticationPrincipal(expression = "idToken") idToken: OidcIdToken): ResponseEntity<*> { 20 | val logoutUrl = this.registration.providerDetails.configurationMetadata["end_session_endpoint"] 21 | val logoutDetails: MutableMap = HashMap() 22 | logoutDetails["logoutUrl"] = logoutUrl.toString() 23 | logoutDetails["idToken"] = idToken.tokenValue 24 | request.session.invalidate() 25 | return ResponseEntity.ok().body>(logoutDetails) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/RestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.data.rest.core.config.RepositoryRestConfiguration 5 | import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer 6 | 7 | @Configuration 8 | class RestConfiguration : RepositoryRestConfigurer { 9 | override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration?) { 10 | config?.exposeIdsFor(Note::class.java) 11 | config?.setBasePath("/api") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/RouteController.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.stereotype.Controller 4 | import org.springframework.web.bind.annotation.RequestMapping 5 | import javax.servlet.http.HttpServletRequest 6 | 7 | @Controller 8 | class RouteController { 9 | 10 | @RequestMapping(value = ["/{path:[^\\.]*}"]) 11 | fun redirect(request: HttpServletRequest): String { 12 | return "forward:/" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/SecurityConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 4 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 5 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 6 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository 7 | import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter 8 | import org.springframework.security.web.util.matcher.RequestMatcher 9 | 10 | @EnableWebSecurity 11 | class SecurityConfiguration : WebSecurityConfigurerAdapter() { 12 | override fun configure(http: HttpSecurity) { 13 | //@formatter:off 14 | http 15 | .authorizeRequests() 16 | .antMatchers("/**/*.{js,html,css}").permitAll() 17 | .antMatchers("/", "/user").permitAll() 18 | .anyRequest().authenticated() 19 | .and() 20 | .oauth2Login() 21 | .and() 22 | .oauth2ResourceServer().jwt() 23 | 24 | http.requiresChannel() 25 | .requestMatchers(RequestMatcher { 26 | r -> r.getHeader("X-Forwarded-Proto") != null 27 | }).requiresSecure() 28 | 29 | http.csrf() 30 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 31 | 32 | http.headers() 33 | .contentSecurityPolicy("script-src 'self'; report-to /csp-report-endpoint/") 34 | .and() 35 | .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN) 36 | .and() 37 | .featurePolicy("accelerometer 'none'; camera 'none'; microphone 'none'") 38 | 39 | //@formatter:on 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /notes-api/src/main/kotlin/com/okta/developer/notes/UserController.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.springframework.data.domain.Page 4 | import org.springframework.data.domain.Pageable 5 | import org.springframework.security.core.annotation.AuthenticationPrincipal 6 | import org.springframework.security.oauth2.core.oidc.user.OidcUser 7 | import org.springframework.web.bind.annotation.GetMapping 8 | import org.springframework.web.bind.annotation.RestController 9 | import java.security.Principal 10 | 11 | @RestController 12 | class UserController(val repository: NotesRepository) { 13 | 14 | @GetMapping("/user/notes") 15 | fun notes(principal: Principal, title: String?, pageable: Pageable): Page { 16 | println("Fetching notes for user: ${principal.name}") 17 | return if (title.isNullOrEmpty()) { 18 | repository.findAllByUsername(principal.name, pageable) 19 | } else { 20 | println("Searching for title: ${title}") 21 | repository.findAllByUsernameAndTitleContainingIgnoreCase(principal.name, title, pageable) 22 | } 23 | } 24 | 25 | @GetMapping("/user") 26 | fun user(@AuthenticationPrincipal user: OidcUser?): OidcUser? { 27 | return user; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notes-api/src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:file:./build/h2db/notes;DB_CLOSE_DELAY=-1 2 | spring.security.oauth2.client.provider.okta.user-name-attribute=preferred_username 3 | -------------------------------------------------------------------------------- /notes-api/src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect 2 | spring.jpa.hibernate.ddl-auto=update 3 | spring.datasource.url=jdbc:postgresql://localhost:5432/notes 4 | spring.datasource.username=notes 5 | spring.datasource.password= 6 | spring.security.oauth2.client.provider.okta.user-name-attribute=preferred_username 7 | server.port=${PORT:8080} 8 | -------------------------------------------------------------------------------- /notes-api/src/test/kotlin/com/okta/developer/notes/DemoApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package com.okta.developer.notes 2 | 3 | import org.junit.jupiter.api.Test 4 | import org.springframework.boot.test.context.SpringBootTest 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | fun contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /notes/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | -------------------------------------------------------------------------------- /notes/.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /notes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.1-alpine AS builder 2 | 3 | WORKDIR /opt/web 4 | COPY package.json package-lock.json ./ 5 | RUN npm install 6 | 7 | ENV PATH="./node_modules/.bin:$PATH" 8 | 9 | COPY . ./ 10 | RUN ng build --prod 11 | 12 | FROM nginx:1.17-alpine 13 | RUN apk --no-cache add curl 14 | RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m` -o envsubst && \ 15 | chmod +x envsubst && \ 16 | mv envsubst /usr/local/bin 17 | COPY ./nginx.config /etc/nginx/nginx.template 18 | CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/nginx.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] 19 | COPY --from=builder /opt/web/dist/notes /usr/share/nginx/html 20 | -------------------------------------------------------------------------------- /notes/README.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /notes/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "notes": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/notes", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.scss" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true, 47 | "budgets": [ 48 | { 49 | "type": "initial", 50 | "maximumWarning": "2mb", 51 | "maximumError": "5mb" 52 | }, 53 | { 54 | "type": "anyComponentStyle", 55 | "maximumWarning": "6kb", 56 | "maximumError": "10kb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "notes:build", 66 | "proxyConfig": "src/proxy.conf.js" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "notes:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "notes:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "tsconfig.spec.json", 86 | "karmaConfig": "karma.conf.js", 87 | "assets": [ 88 | "src/favicon.ico", 89 | "src/assets" 90 | ], 91 | "styles": [ 92 | "src/styles.scss" 93 | ], 94 | "scripts": [] 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-devkit/build-angular:tslint", 99 | "options": { 100 | "tsConfig": [ 101 | "tsconfig.app.json", 102 | "tsconfig.spec.json", 103 | "e2e/tsconfig.json" 104 | ], 105 | "exclude": [ 106 | "**/node_modules/**" 107 | ] 108 | } 109 | }, 110 | "e2e": { 111 | "builder": "@angular-devkit/build-angular:protractor", 112 | "options": { 113 | "protractorConfig": "e2e/protractor.conf.js", 114 | "devServerTarget": "notes:serve" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "notes:serve:production" 119 | } 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "defaultProject": "notes", 126 | "schematics": { 127 | "@schematics/angular:component": { 128 | "styleext": "scss" 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /notes/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'. -------------------------------------------------------------------------------- /notes/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: 'raw' } })); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /notes/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display title', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Notes'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /notes/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .navbar-brand')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /notes/e2e/src/login.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element, ExpectedConditions as ec } from 'protractor'; 2 | 3 | export class LoginPage { 4 | username = element(by.name('username')); 5 | password = element(by.name('password')); 6 | // button on IdP sign-in form 7 | loginButton = element(by.css('input[type=submit]')); 8 | signInButton = element(by.id('login')); 9 | logoutButton = element(by.id('logout')); 10 | 11 | async login() { 12 | await browser.get('/'); 13 | await browser.wait(ec.visibilityOf(this.signInButton)); 14 | await this.signInButton.click(); 15 | // You must set E2E_USERNAME and E2E_PASSWORD as environment variables 16 | await this.loginToIdP(process.env.E2E_USERNAME, process.env.E2E_PASSWORD); 17 | await browser.wait(ec.visibilityOf(this.logoutButton)); 18 | } 19 | 20 | async loginToIdP(username: string, password: string) { 21 | // Entering non angular site, tell webdriver to switch to synchronous mode. 22 | await browser.waitForAngularEnabled(false); 23 | await browser.wait(ec.visibilityOf(this.username)); 24 | 25 | if (await this.username.isPresent()) { 26 | await this.username.sendKeys(username); 27 | await this.password.sendKeys(password); 28 | await this.loginButton.click(); 29 | if (!(await this.username.isPresent())) { 30 | await browser.waitForAngularEnabled(true); 31 | } 32 | } else { 33 | // redirected back because already logged in 34 | await browser.waitForAngularEnabled(true); 35 | } 36 | } 37 | 38 | async logout() { 39 | await browser.get('/'); 40 | await browser.wait(ec.visibilityOf(this.logoutButton)); 41 | await this.logoutButton.click(); 42 | await browser.sleep(1000); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /notes/e2e/src/notes.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | import { LoginPage } from './login.po'; 3 | 4 | describe('Notes', () => { 5 | let loginPage: LoginPage; 6 | 7 | beforeAll(async () => { 8 | loginPage = new LoginPage(); 9 | await loginPage.login(); 10 | }); 11 | 12 | afterAll(async () => { 13 | await loginPage.logout(); 14 | }); 15 | 16 | beforeEach(async () => { 17 | await browser.get('/notes'); 18 | }); 19 | 20 | it('should have an input and search button', () => { 21 | expect(element(by.css('app-root app-note form input')).isPresent()).toEqual(true); 22 | expect(element(by.css('app-root app-note form button')).isPresent()).toEqual(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /notes/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /notes/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/notes'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /notes/nginx.config: -------------------------------------------------------------------------------- 1 | server { 2 | listen ${PORT:-80}; 3 | server_name _; 4 | 5 | root /usr/share/nginx/html; 6 | index index.html; 7 | 8 | location / { 9 | try_files $$uri /index.html; 10 | } 11 | 12 | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-ancestors 'none'; connect-src 'self' https://*.okta.com https://*.herokuapp.com"; 13 | add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin"; 14 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; 15 | add_header X-Content-Type-Options nosniff; 16 | add_header X-Frame-Options DENY; 17 | add_header X-XSS-Protection "1; mode=block"; 18 | add_header Feature-Policy "accelerometer 'none'; camera 'none'; microphone 'none'"; 19 | } 20 | -------------------------------------------------------------------------------- /notes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notes", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "docker": "docker build -t ng-notes .", 12 | "ng-notes": "docker run -p 4200:80 ng-notes" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~9.1.11", 17 | "@angular/common": "~9.1.11", 18 | "@angular/compiler": "~9.1.11", 19 | "@angular/core": "~9.1.11", 20 | "@angular/forms": "~9.1.11", 21 | "@angular/localize": "^9.1.11", 22 | "@angular/platform-browser": "~9.1.11", 23 | "@angular/platform-browser-dynamic": "~9.1.11", 24 | "@angular/router": "~9.1.11", 25 | "@ng-bootstrap/ng-bootstrap": "^6.1.0", 26 | "bootstrap": "^4.5.0", 27 | "rxjs": "~6.5.5", 28 | "tslib": "^2.0.0", 29 | "zone.js": "~0.10.3" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.901.9", 33 | "@angular/cli": "~9.1.9", 34 | "@angular/compiler-cli": "~9.1.11", 35 | "@angular/language-service": "~9.1.11", 36 | "@types/node": "^14.0.13", 37 | "@types/jasmine": "~3.5.10", 38 | "@types/jasminewd2": "~2.0.8", 39 | "angular-crud": "^1.0.0", 40 | "codelyzer": "^5.2.2", 41 | "jasmine-core": "~3.5.0", 42 | "jasmine-spec-reporter": "~5.0.2", 43 | "karma": "~6.3.16", 44 | "karma-chrome-launcher": "~3.1.0", 45 | "karma-coverage-istanbul-reporter": "~3.0.3", 46 | "karma-jasmine": "~3.3.1", 47 | "karma-jasmine-html-reporter": "^1.5.4", 48 | "protractor": "~7.0.0", 49 | "ts-node": "~8.10.2", 50 | "tslint": "~6.1.2", 51 | "typescript": "~3.8.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /notes/src/_variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | $primary: orange; 3 | $secondary: blue; 4 | $light: lighten($primary, 20%); 5 | $dark: darken($secondary, 10%); 6 | */ 7 | -------------------------------------------------------------------------------- /notes/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomeComponent } from './home/home.component'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 7 | { 8 | path: 'home', 9 | component: HomeComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forRoot(routes)], 15 | exports: [RouterModule] 16 | }) 17 | export class AppRoutingModule { } 18 | -------------------------------------------------------------------------------- /notes/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 30 | 31 | 35 | -------------------------------------------------------------------------------- /notes/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/okta-angular-spring-boot-docker-example/c6405578e4248a58ebc363148bd008f9424e3304/notes/src/app/app.component.scss -------------------------------------------------------------------------------- /notes/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | RouterTestingModule, 11 | HttpClientTestingModule 12 | ], 13 | declarations: [ 14 | AppComponent 15 | ], 16 | }).compileComponents(); 17 | })); 18 | 19 | it('should create the app', () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app).toBeTruthy(); 23 | }); 24 | 25 | it(`should have as title 'Notes'`, () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | const app = fixture.debugElement.componentInstance; 28 | expect(app.title).toEqual('Notes'); 29 | }); 30 | 31 | it('should render title', () => { 32 | const fixture = TestBed.createComponent(AppComponent); 33 | fixture.detectChanges(); 34 | const compiled = fixture.debugElement.nativeElement; 35 | expect(compiled.querySelector('.navbar-brand').textContent).toContain('Notes'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /notes/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from './shared/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | title = 'Notes'; 11 | isAuthenticated: boolean; 12 | isCollapsed = true; 13 | 14 | constructor(public auth: AuthService) { 15 | } 16 | 17 | async ngOnInit() { 18 | this.isAuthenticated = await this.auth.isAuthenticated(); 19 | this.auth.$authenticationState.subscribe( 20 | (isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /notes/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { NoteModule } from './note/note.module'; 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | import { HomeComponent } from './home/home.component'; 9 | import { HttpClientModule } from '@angular/common/http'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent, 14 | HomeComponent 15 | ], 16 | imports: [ 17 | BrowserModule, 18 | AppRoutingModule, 19 | HttpClientModule, 20 | NoteModule, 21 | NgbModule 22 | ], 23 | providers: [], 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppModule { } 27 | -------------------------------------------------------------------------------- /notes/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

View Notes

2 | -------------------------------------------------------------------------------- /notes/src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/okta-angular-spring-boot-docker-example/c6405578e4248a58ebc363148bd008f9424e3304/notes/src/app/home/home.component.scss -------------------------------------------------------------------------------- /notes/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 6 | 7 | describe('HomeComponent', () => { 8 | let component: HomeComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ HomeComponent ], 14 | imports: [ 15 | RouterTestingModule, 16 | HttpClientTestingModule 17 | ] 18 | }) 19 | .compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(HomeComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /notes/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../shared/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | templateUrl: './home.component.html', 7 | styleUrls: ['./home.component.scss'] 8 | }) 9 | export class HomeComponent implements OnInit { 10 | isAuthenticated: boolean; 11 | 12 | constructor(public auth: AuthService) { 13 | } 14 | 15 | async ngOnInit() { 16 | this.isAuthenticated = await this.auth.isAuthenticated(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /notes/src/app/note/model.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Notes", 3 | "entity": "note", 4 | "api": { 5 | "url": "http://localhost:8080/api/notes" 6 | }, 7 | "filter": [ 8 | "title" 9 | ], 10 | "fields": [ 11 | { 12 | "name": "id", 13 | "label": "Id", 14 | "isId": true, 15 | "readonly": true, 16 | "type": "number" 17 | }, 18 | { 19 | "name": "title", 20 | "type": "string", 21 | "label": "Title" 22 | }, 23 | { 24 | "name": "text", 25 | "type": "string", 26 | "label": "Text" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /notes/src/app/note/note-edit/note-edit.component.html: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 |

Notes Detail

8 |
9 |
{{ feedback.message }}
10 |
11 |
12 | 13 | {{note.id || 'n/a'}} 14 |
15 | 16 |
17 | 18 | 20 |
21 | Title is required 22 |
23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /notes/src/app/note/note-edit/note-edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { NoteEditComponent } from './note-edit.component'; 6 | import { NoteService } from '../note.service'; 7 | 8 | describe('NoteEditComponent', () => { 9 | let component: NoteEditComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [NoteEditComponent], 15 | imports: [FormsModule, HttpClientTestingModule, RouterTestingModule], 16 | providers: [NoteService] 17 | }) 18 | .compileComponents(); 19 | })); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(NoteEditComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /notes/src/app/note/note-edit/note-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { NoteService } from '../note.service'; 4 | import { Note } from '../note'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | import { of } from 'rxjs'; 7 | 8 | @Component({ 9 | selector: 'app-note-edit', 10 | templateUrl: './note-edit.component.html' 11 | }) 12 | export class NoteEditComponent implements OnInit { 13 | id: string; 14 | note: Note; 15 | feedback: any = {}; 16 | 17 | constructor( 18 | private route: ActivatedRoute, 19 | private router: Router, 20 | private noteService: NoteService) { 21 | } 22 | 23 | ngOnInit() { 24 | this 25 | .route 26 | .params 27 | .pipe( 28 | map(p => p.id), 29 | switchMap(id => { 30 | if (id === 'new') { return of(new Note()); } 31 | // this.id = id; 32 | return this.noteService.findById(id); 33 | }) 34 | ) 35 | .subscribe(note => { 36 | this.note = note; 37 | // this.note.id = +note.id; 38 | this.feedback = {}; 39 | }, 40 | err => { 41 | this.feedback = {type: 'warning', message: 'Error loading'}; 42 | } 43 | ); 44 | } 45 | 46 | save() { 47 | this.noteService.save(this.note).subscribe( 48 | note => { 49 | this.note = note; 50 | // this.note.id = +this.id; 51 | this.feedback = {type: 'success', message: 'Save was successful!'}; 52 | setTimeout(() => { 53 | this.router.navigate(['/notes']); 54 | }, 1000); 55 | }, 56 | err => { 57 | this.feedback = {type: 'warning', message: 'Error saving'}; 58 | } 59 | ); 60 | } 61 | 62 | cancel() { 63 | this.router.navigate(['/notes']); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /notes/src/app/note/note-filter.ts: -------------------------------------------------------------------------------- 1 | export class NoteFilter { 2 | title = ''; 3 | column: string; 4 | direction: string; 5 | page = 0; 6 | size = 20; 7 | } 8 | -------------------------------------------------------------------------------- /notes/src/app/note/note-list/note-list.component.html: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 |

Notes List

8 |
9 |
10 |
11 | 12 | 13 |
14 | 15 | 16 | New 17 |
18 |
19 |
{{ feedback.message }}
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 |
#TitleText
{{item.id}}{{item.title}}{{item.text}} 36 | Edit  37 | 38 |
42 | 43 |
44 | 47 | 48 | 49 | 55 |
56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /notes/src/app/note/note-list/note-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { NoteListComponent } from './note-list.component'; 6 | import { NoteService } from '../note.service'; 7 | 8 | describe('NoteListComponent', () => { 9 | let component: NoteListComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [NoteListComponent], 15 | imports: [FormsModule, HttpClientTestingModule, RouterTestingModule], 16 | providers: [NoteService] 17 | }) 18 | .compileComponents(); 19 | })); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(NoteListComponent); 23 | component = fixture.componentInstance; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /notes/src/app/note/note-list/note-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, QueryList, ViewChildren } from '@angular/core'; 2 | import { NoteFilter } from '../note-filter'; 3 | import { NoteService } from '../note.service'; 4 | import { Note } from '../note'; 5 | import { SortableHeaderDirective, SortEvent} from './sortable.directive'; 6 | import { Observable } from 'rxjs'; 7 | 8 | @Component({ 9 | selector: 'app-note', 10 | templateUrl: 'note-list.component.html' 11 | }) 12 | export class NoteListComponent implements OnInit { 13 | total$: Observable; 14 | @ViewChildren(SortableHeaderDirective) headers: QueryList; 15 | 16 | filter = new NoteFilter(); 17 | selectedNote: Note; 18 | feedback: any = {}; 19 | 20 | get noteList(): Note[] { 21 | return this.noteService.noteList; 22 | } 23 | 24 | constructor(private noteService: NoteService) { 25 | } 26 | 27 | ngOnInit() { 28 | this.search(); 29 | } 30 | 31 | search(): void { 32 | this.noteService.load(this.filter); 33 | this.total$ = this.noteService.size$; 34 | } 35 | 36 | onChange(pageSize: number) { 37 | this.filter.size = pageSize; 38 | this.filter.page = 0; 39 | this.search(); 40 | } 41 | 42 | onPageChange(page: number) { 43 | this.filter.page = page - 1; 44 | this.search(); 45 | this.filter.page = page; 46 | } 47 | 48 | onSort({column, direction}: SortEvent) { 49 | // reset other headers 50 | this.headers.forEach(header => { 51 | if (header.sortable !== column) { 52 | header.direction = ''; 53 | } 54 | }); 55 | 56 | this.filter.column = column; 57 | this.filter.direction = direction; 58 | this.filter.page = 0; 59 | this.search(); 60 | } 61 | 62 | select(selected: Note): void { 63 | this.selectedNote = selected; 64 | } 65 | 66 | delete(note: Note): void { 67 | if (confirm('Are you sure?')) { 68 | this.noteService.delete(note).subscribe(() => { 69 | this.feedback = {type: 'success', message: 'Delete was successful!'}; 70 | setTimeout(() => { 71 | this.search(); 72 | }, 1000); 73 | }, 74 | err => { 75 | this.feedback = {type: 'warning', message: 'Error deleting.'}; 76 | } 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /notes/src/app/note/note-list/sortable.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | export type SortDirection = 'asc' | 'desc' | ''; 4 | const rotate: { [key: string]: SortDirection } = {asc: 'desc', desc: '', '': 'asc'}; 5 | 6 | export interface SortEvent { 7 | column: string; 8 | direction: SortDirection; 9 | } 10 | 11 | @Directive({ 12 | selector: 'th[sortable]', 13 | host: { 14 | '[class.asc]': 'direction === "asc"', 15 | '[class.desc]': 'direction === "desc"', 16 | '(click)': 'rotate()' 17 | } 18 | }) 19 | export class SortableHeaderDirective { 20 | 21 | @Input() sortable: string; 22 | @Input() direction: SortDirection = ''; 23 | @Output() sort = new EventEmitter(); 24 | 25 | rotate() { 26 | this.direction = rotate[this.direction]; 27 | this.sort.emit({column: this.sortable, direction: this.direction}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notes/src/app/note/note.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NoteListComponent } from './note-list/note-list.component'; 6 | import { NoteEditComponent } from './note-edit/note-edit.component'; 7 | import { NoteService } from './note.service'; 8 | import { NOTE_ROUTES } from './note.routes'; 9 | import { SortableHeaderDirective } from './note-list/sortable.directive'; 10 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | RouterModule.forChild(NOTE_ROUTES), 17 | NgbModule 18 | ], 19 | declarations: [ 20 | NoteListComponent, 21 | NoteEditComponent, 22 | SortableHeaderDirective 23 | ], 24 | providers: [NoteService], 25 | exports: [] 26 | }) 27 | export class NoteModule { } 28 | -------------------------------------------------------------------------------- /notes/src/app/note/note.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { NoteListComponent } from './note-list/note-list.component'; 3 | import { NoteEditComponent } from './note-edit/note-edit.component'; 4 | 5 | export const NOTE_ROUTES: Routes = [ 6 | { 7 | path: 'notes', 8 | component: NoteListComponent 9 | }, 10 | { 11 | path: 'notes/:id', 12 | component: NoteEditComponent 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /notes/src/app/note/note.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NoteService } from './note.service'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | 5 | describe('NoteService', () => { 6 | let service: NoteService; 7 | let httpMock: HttpTestingController; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | providers: [NoteService] 13 | }); 14 | 15 | service = TestBed.get(NoteService); 16 | httpMock = TestBed.get(HttpTestingController); 17 | }); 18 | 19 | it('should be created', () => { 20 | expect(service).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /notes/src/app/note/note.service.ts: -------------------------------------------------------------------------------- 1 | import { Note } from './note'; 2 | import { NoteFilter } from './note-filter'; 3 | import { Injectable } from '@angular/core'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 6 | import { map } from 'rxjs/operators'; 7 | import { environment } from '../../environments/environment'; 8 | 9 | const headers = new HttpHeaders().set('Accept', 'application/json'); 10 | 11 | @Injectable() 12 | export class NoteService { 13 | noteList: Note[] = []; 14 | api = `${environment.apiUrl}/api/notes`; 15 | size$ = new BehaviorSubject(0); 16 | 17 | constructor(private http: HttpClient) { 18 | } 19 | 20 | findById(id: string): Observable { 21 | const url = `${this.api}/${id}`; 22 | const params = { id }; 23 | return this.http.get(url, {params, headers}); 24 | } 25 | 26 | load(filter: NoteFilter): void { 27 | this.find(filter).subscribe(result => { 28 | this.noteList = result; 29 | }, 30 | err => { 31 | console.error('error loading', err); 32 | } 33 | ); 34 | } 35 | 36 | find(filter: NoteFilter): Observable { 37 | const params: any = { 38 | title: filter.title, 39 | sort: `${filter.column},${filter.direction}`, 40 | size: filter.size, 41 | page: filter.page 42 | }; 43 | if (!filter.direction) { delete params.sort; } 44 | 45 | const userNotes = `${environment.apiUrl}/user/notes`; 46 | return this.http.get(userNotes, {params, headers}).pipe( 47 | map((response: any) => { 48 | this.size$.next(response.totalElements); 49 | return response.content; 50 | }) 51 | ); 52 | } 53 | 54 | save(entity: Note): Observable { 55 | let params = new HttpParams(); 56 | let url = ''; 57 | if (entity.id) { 58 | url = `${this.api}/${entity.id.toString()}`; 59 | params = new HttpParams().set('ID', entity.id.toString()); 60 | return this.http.put(url, entity, {headers, params}); 61 | } else { 62 | url = `${this.api}`; 63 | return this.http.post(url, entity, {headers, params}); 64 | } 65 | } 66 | 67 | delete(entity: Note): Observable { 68 | let params = new HttpParams(); 69 | let url = ''; 70 | if (entity.id) { 71 | url = `${this.api}/${entity.id.toString()}`; 72 | params = new HttpParams().set('ID', entity.id.toString()); 73 | return this.http.delete(url, {headers, params}); 74 | } 75 | return null; 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /notes/src/app/note/note.ts: -------------------------------------------------------------------------------- 1 | export class Note { 2 | id: number; 3 | title: string; 4 | text: string; 5 | } 6 | -------------------------------------------------------------------------------- /notes/src/app/shared/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | import { BehaviorSubject, Observable } from 'rxjs'; 4 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 5 | import { environment } from '../../environments/environment'; 6 | import { User } from './user'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | const headers = new HttpHeaders().set('Accept', 'application/json'); 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class AuthService { 15 | $authenticationState = new BehaviorSubject(false); 16 | 17 | constructor(private http: HttpClient, private location: Location) { 18 | } 19 | 20 | getUser(): Observable { 21 | return this.http.get(`${environment.apiUrl}/user`, {headers}).pipe( 22 | map((response: User) => { 23 | if (response !== null) { 24 | this.$authenticationState.next(true); 25 | return response; 26 | } 27 | }) 28 | ); 29 | } 30 | 31 | isAuthenticated(): Promise { 32 | return this.getUser().toPromise().then((user: User) => { 33 | return user !== undefined; 34 | }).catch(() => { 35 | return false; 36 | }) 37 | } 38 | 39 | login(): void { 40 | location.href = `${location.origin}${this.location.prepareExternalUrl('oauth2/authorization/okta')}`; 41 | } 42 | 43 | logout(): void { 44 | const redirectUri = `${location.origin}${this.location.prepareExternalUrl('/')}`; 45 | 46 | this.http.post(`${environment.apiUrl}/api/logout`, {}).subscribe((response: any) => { 47 | location.href = response.logoutUrl + '?id_token_hint=' + response.idToken 48 | + '&post_logout_redirect_uri=' + redirectUri; 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /notes/src/app/shared/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | sub: number; 3 | fullName: string; 4 | } 5 | -------------------------------------------------------------------------------- /notes/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/okta-angular-spring-boot-docker-example/c6405578e4248a58ebc363148bd008f9424e3304/notes/src/assets/.gitkeep -------------------------------------------------------------------------------- /notes/src/assets/images/angular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /notes/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: '' 4 | }; 5 | -------------------------------------------------------------------------------- /notes/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 | production: false, 7 | apiUrl: '' 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 | -------------------------------------------------------------------------------- /notes/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oktadev/okta-angular-spring-boot-docker-example/c6405578e4248a58ebc363148bd008f9424e3304/notes/src/favicon.ico -------------------------------------------------------------------------------- /notes/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Notes 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /notes/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /notes/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | 28 | /** 29 | * Web Animations `@angular/platform-browser/animations` 30 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 31 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 32 | */ 33 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 34 | 35 | /** 36 | * By default, zone.js will patch all possible macroTask and DomEvents 37 | * user can disable parts of macroTask/DomEvents patch by setting following flags 38 | * because those flags need to be set before `zone.js` being loaded, and webpack 39 | * will put import in the top of bundle, so user need to create a separate file 40 | * in this directory (for example: zone-flags.ts), and put the following flags 41 | * into that file, and then add the following code before importing zone.js. 42 | * import './zone-flags.ts'; 43 | * 44 | * The flags allowed in zone-flags.ts are listed here. 45 | * 46 | * The following flags will work for all browsers. 47 | * 48 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 49 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 50 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 51 | * 52 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 53 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 54 | * 55 | * (window as any).__Zone_enable_cross_context_check = true; 56 | * 57 | */ 58 | 59 | /*************************************************************************************************** 60 | * Zone JS is required by default for Angular itself. 61 | */ 62 | import 'zone.js/dist/zone'; // Included with Angular CLI. 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | -------------------------------------------------------------------------------- /notes/src/proxy.conf.js: -------------------------------------------------------------------------------- 1 | const PROXY_CONFIG = [ 2 | { 3 | context: ['/user', '/api', '/oauth2', '/login'], 4 | target: 'http://localhost:8080', 5 | secure: false, 6 | logLevel: 'debug' 7 | } 8 | ] 9 | 10 | module.exports = PROXY_CONFIG; 11 | -------------------------------------------------------------------------------- /notes/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "~bootstrap/scss/bootstrap.scss"; 3 | 4 | th[sortable] { 5 | cursor: pointer; 6 | user-select: none; 7 | -webkit-user-select: none; 8 | } 9 | 10 | th[sortable].desc:before, th[sortable].asc:before { 11 | content: ''; 12 | display: block; 13 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC') no-repeat; 14 | background-size: 22px; 15 | width: 22px; 16 | height: 22px; 17 | float: left; 18 | margin-left: -22px; 19 | } 20 | 21 | th[sortable].desc:before { 22 | transform: rotate(180deg); 23 | -ms-transform: rotate(180deg); 24 | } 25 | -------------------------------------------------------------------------------- /notes/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /notes/static.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": { 3 | "/**": { 4 | "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-ancestors 'none'; connect-src 'self' https://*.okta.com https://*.herokuapp.com", 5 | "Referrer-Policy": "no-referrer, strict-origin-when-cross-origin", 6 | "Strict-Transport-Security": "max-age=63072000; includeSubDomains", 7 | "X-Content-Type-Options": "nosniff", 8 | "X-Frame-Options": "DENY", 9 | "X-XSS-Protection": "1; mode=block", 10 | "Feature-Policy": "accelerometer 'none'; camera 'none'; microphone 'none'" 11 | } 12 | }, 13 | "https_only": true, 14 | "root": "dist/notes/", 15 | "routes": { 16 | "/**": "index.html" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /notes/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /notes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /notes/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /notes/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } --------------------------------------------------------------------------------