diff --git a/.env.example b/.env.example index c072ad2..3123d7b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ REACT_APP_GOOGLE_MAPS_API_KEY=--your-google-maps-api-key-- -VITE_API_SERVER_URL=http://localhost:3000 \ No newline at end of file +VITE_API_SERVER_URL=http://localhost:3000/api \ No newline at end of file diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..14cd48c --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,63 @@ +name: Deploy UI to AWS +on: + pull_request: + branches: + - master + types: + - closed + +jobs: + deploy-ui: + if: github.event.pull_request.merged + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }} + CODE_BUCKET: ${{vars.CODE_BUCKET}} + AWS_REGION: ${{ vars.AWS_REGION }} + VITE_API_SERVER_URL: ${{ vars.VITE_API_SERVER_URL }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache ui files + id: cache-ui + uses: actions/cache@v4 + env: + cache-name: cache-ui-files + with: + path: . + key: ${{ hashFiles('package*.json') }}-${{ hashFiles('src/**/*') }} + + - if: ${{ steps.cache-ui.outputs.cache-hit == 'true' }} + name: Check ui changes + continue-on-error: true + run: echo 'No ui changes found. Skip ui build and deployment.' + + - if: ${{ steps.cache-ui.outputs.cache-hit != 'true' }} + name: Build + run: | + npm ci + npm run build + + - if: ${{ steps.cache-ui.outputs.cache-hit != 'true' }} + name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{vars.AWS_REGION}} + + - if: ${{ steps.cache-ui.outputs.cache-hit != 'true' }} + name: Sync with S3 + run: | + aws s3 sync ./dist s3://$CODE_BUCKET --delete + + - if: ${{ steps.cache-ui.outputs.cache-hit != 'true' }} + name: Invalidate cloudfront + run: | + # Send SSM command to invalidate cloudfront + aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f75cd7 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# deployment + +## before deployment, a relevant infrastructure must be created (s3 bucket, cloudfront distribution, route53 record) + +See ../api/README.md for details how to create it (it is created with api's infrastructure setup cdk script) + +## ci/cd variables + +before setup ci/cd, you need to know and set up the following variables + +- secrets.VITE_GOOGLE_MAPS_API_KEY +- secrets.AWS_ACCESS_KEY_ID +- secrets.AWS_SECRET_ACCESS_KEY +- vars.CODE_BUCKET +- vars.AWS_REGION +- vars.VITE_API_SERVER_URL +- vars.CLOUDFRONT_DISTRIBUTION_ID diff --git a/capacitor.config.ts b/capacitor.config.ts index 1014045..71c1d4e 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -6,8 +6,18 @@ const config: CapacitorConfig = { webDir: 'dist', android: { minWebViewVersion: 55, - backgroundColor: "#00000000", - }, + backgroundColor: '#00000000', + allowMixedContent: true, // remove when using https + }, + server: { + androidScheme: 'http', // remove when using https + cleartext: true, // remove when using https + }, + plugins: { + CapacitorHttp: { + enabled: true, + }, + }, }; export default config; diff --git a/src/api/constants.ts b/src/api/constants.ts index f79bfbd..856337d 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -5,10 +5,10 @@ export const BASE_URL = import.meta.env.VITE_API_SERVER_URL; export const ENDPOINTS: TEndpoints = { auth: { - signin: { url: 'auth/signin', method: 'POST', options: { auth: false } }, - refresh: { url: 'auth/refresh', method: 'POST', options: { auth: false } }, + signin: { url: () => 'auth/signin', method: 'POST', options: { auth: false } }, + refresh: { url: () => 'auth/refresh', method: 'POST', options: { auth: false } }, }, users: { - getAll: { url: 'users', method: 'GET', options: { auth: true } }, + getAll: { url: () => 'users', method: 'GET', options: { auth: true } }, }, }; diff --git a/src/api/services/auth-service.ts b/src/api/services/auth-service.ts index a698c5c..fd42ba2 100644 --- a/src/api/services/auth-service.ts +++ b/src/api/services/auth-service.ts @@ -5,7 +5,7 @@ import { ENDPOINTS } from "../constants"; export class AuthService { async login(credentials: AuthDto, setTokens: (tokens: ResponseTokenDto) => void) { - const tokens = await transport.post(ENDPOINTS.auth.signin.url, credentials); + const tokens = await transport.post(ENDPOINTS.auth.signin.url(), credentials); setTokens(tokens); } } diff --git a/src/api/transport/base-transoport.ts b/src/api/transport/base-transoport.ts index b088f88..a07b200 100644 --- a/src/api/transport/base-transoport.ts +++ b/src/api/transport/base-transoport.ts @@ -10,28 +10,28 @@ export abstract class BaseTransport { abstract patch(endpoint: string, data: object, options: TransportOptions): Promise; abstract delete(endpoint: string, options: TransportOptions): Promise; - public async useEndpoint(endpoint: TEndpoint, data?: object | null): Promise { + public async useEndpoint(endpoint: TEndpoint, data?: object | null, params?: Record): Promise { switch (endpoint.method) { case 'GET': - return this.get(endpoint.url, endpoint.options); + return this.get(endpoint.url(params), endpoint.options); case 'POST': - return this.post(endpoint.url, data ?? null, endpoint.options); + return this.post(endpoint.url(params), data ?? null, endpoint.options); case 'PUT': { if (!data) { console.error('Data is required for PUT request, default to {}'); data = {}; } - return this.put(endpoint.url, data, endpoint.options); + return this.put(endpoint.url(params), data, endpoint.options); } case 'PATCH': { if (!data) { console.error('Data is required for PATCH request, default to {}'); data = {}; } - return this.patch(endpoint.url, data, endpoint.options); + return this.patch(endpoint.url(params), data, endpoint.options); } case 'DELETE': - return this.delete(endpoint.url, endpoint.options); + return this.delete(endpoint.url(params), endpoint.options); } } @@ -56,7 +56,7 @@ export abstract class BaseTransport { protected async refreshTokens(): Promise { const tokens = JSON.parse(localStorage.getItem('auth') || '{}') as ResponseTokenDto; - const newTokens = await this.post(ENDPOINTS.auth.refresh.url, null, { + const newTokens = await this.post(ENDPOINTS.auth.refresh.url(), null, { customHeaders: { Authorization: `Bearer ${tokens.refreshToken}` }, }); localStorage.setItem('auth', JSON.stringify(newTokens)); diff --git a/src/api/types.ts b/src/api/types.ts index 6410a7f..cdd01b2 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -4,7 +4,7 @@ export type TransportOptions = { }; export type TEndpoint = { - url: string; + url: (params?: Record) => string; options: TransportOptions; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; }; @@ -17,5 +17,5 @@ export interface ITransport { put: (endpoint: string, data: object, options: TransportOptions) => Promise; patch: (endpoint: string, data: object, options: TransportOptions) => Promise; delete: (endpoint: string, options: TransportOptions) => Promise; - useEndpoint: (endpoint: TEndpoint, data: object | null) => Promise; + useEndpoint: (endpoint: TEndpoint, data: object | null, params?: Record) => Promise; } \ No newline at end of file