diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9c9a0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +root = true + +[*] +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[{package,bower}.json] +indent_style = space +indent_size = 2 + +[{.eslintrc,.scss-lint.yml}] +indent_style = space +indent_size = 2 + +[*.{scss,sass}] +indent_style = space +indent_size = 2 diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..4a7f93f --- /dev/null +++ b/.firebaserc @@ -0,0 +1,40 @@ +{ + "projects": { + "production": "fir-template-40a59", + "staging": "fir-template-staging", + "development": "fir-template-development", + "default": "fir-template-40a59" + }, + "targets": { + "fir-template-development": { + "hosting": { + "website": [ + "fir-template-website-development" + ], + "api": [ + "fir-template-api-development" + ] + } + }, + "fir-template-staging": { + "hosting": { + "website": [ + "fir-template-website-staging" + ], + "api": [ + "fir-template-api-staging" + ] + } + }, + "fir-template-40a59": { + "hosting": { + "website": [ + "fir-template-website" + ], + "api": [ + "fir-template-api" + ] + } + } + } +} diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml new file mode 100644 index 0000000..07ca2d5 --- /dev/null +++ b/.github/workflows/deploy-development.yml @@ -0,0 +1,79 @@ +name: Deploy Database, Storage, Firestore +on: + push: + branches: + - dev +env: + NODE_VERSION: 20 + TOKEN_FOR_WORKFLOW: ${{ secrets.TOKEN_FOR_WORKFLOW }} + FIREBASE_PROJECT: "development" + GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/firebase/functions/accounts/development.json + BRANCH: "dev" + FIREBASE_CHILD_REPO: "dudko-dev/firebase-template-functions" + FIREBASE_CHILD_BRANCH: "dev" +jobs: + deploy: + name: Deploy Database, Storage, Firestore + runs-on: ubuntu-latest + env: + ENVKEY: ${{ secrets.ENVKEY }} + timeout-minutes: 10 + steps: + - name: Сheckout firebase repo (${{ github.repository }}) + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref }} + path: "firebase" + - name: Сheckout firebase functions repo (${{ env.FIREBASE_CHILD_REPO }}) + uses: actions/checkout@v4 + with: + repository: ${{ env.FIREBASE_CHILD_REPO }} + ref: refs/heads/${{ env.FIREBASE_CHILD_BRANCH }} + token: ${{ env.TOKEN_FOR_WORKFLOW }} + path: "firebase/functions" + - name: Change mode directory + run: chmod 0766 -R firebase + working-directory: ${{ github.workspace }} + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install deploy dependencies + run: sudo npm install firebase-tools -g + working-directory: ${{ github.workspace }}/firebase + - name: Export envkey to file + run: echo "$ENVKEY">./.envkey + working-directory: ${{ github.workspace }}/firebase/functions + - name: Decrypt env and accounts files + run: npm run decrypt + working-directory: ${{ github.workspace }}/firebase/functions + - name: Select firebase project + run: firebase use $FIREBASE_PROJECT + working-directory: ${{ github.workspace }}/firebase + - name: Deploy to firebase + run: firebase deploy -m "Autodeploy from GitHUB ($GITHUB_ACTOR)" --only database,storage,firestore + working-directory: ${{ github.workspace }}/firebase + - name: The job has failed - archive result + if: ${{ failure() }} + run: | + if [[ -d ${{ github.workspace }}/firebase ]]; then + tar -czf firebase.tar.gz firebase; + fi + working-directory: ${{ github.workspace }} + - name: The job has failed - archive npm logs + if: ${{ failure() }} + run: | + if [[ -d /home/runner/.npm/_logs ]]; then + tar -czf ${{ github.workspace }}/npm-logs.tar.gz /home/runner/.npm/_logs; + fi + working-directory: / + - name: The job has failed - upload artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.FIREBASE_PROJECT }}-firebase-debug + path: | + ${{ github.workspace }}/firebase.tar.gz + ${{ github.workspace }}/npm-logs.tar.gz + retention-days: 1 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..dd34a35 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,79 @@ +name: Deploy Database, Storage, Firestore +on: + push: + branches: + - main +env: + NODE_VERSION: 20 + TOKEN_FOR_WORKFLOW: ${{ secrets.TOKEN_FOR_WORKFLOW }} + FIREBASE_PROJECT: "production" + GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/firebase/functions/accounts/production.json + BRANCH: "main" + FIREBASE_CHILD_REPO: "dudko-dev/firebase-template-functions" + FIREBASE_CHILD_BRANCH: "main" +jobs: + deploy: + name: Deploy Database, Storage, Firestore + runs-on: ubuntu-latest + env: + ENVKEY: ${{ secrets.ENVKEY }} + timeout-minutes: 10 + steps: + - name: Сheckout firebase repo (${{ github.repository }}) + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref }} + path: "firebase" + - name: Сheckout firebase functions repo (${{ env.FIREBASE_CHILD_REPO }}) + uses: actions/checkout@v4 + with: + repository: ${{ env.FIREBASE_CHILD_REPO }} + ref: refs/heads/${{ env.FIREBASE_CHILD_BRANCH }} + token: ${{ env.TOKEN_FOR_WORKFLOW }} + path: "firebase/functions" + - name: Change mode directory + run: chmod 0766 -R firebase + working-directory: ${{ github.workspace }} + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install deploy dependencies + run: sudo npm install firebase-tools -g + working-directory: ${{ github.workspace }}/firebase + - name: Export envkey to file + run: echo "$ENVKEY">./.envkey + working-directory: ${{ github.workspace }}/firebase/functions + - name: Decrypt env and accounts files + run: npm run decrypt + working-directory: ${{ github.workspace }}/firebase/functions + - name: Select firebase project + run: firebase use $FIREBASE_PROJECT + working-directory: ${{ github.workspace }}/firebase + - name: Deploy to firebase + run: firebase deploy -m "Autodeploy from GitHUB ($GITHUB_ACTOR)" --only database,storage,firestore + working-directory: ${{ github.workspace }}/firebase + - name: The job has failed - archive result + if: ${{ failure() }} + run: | + if [[ -d ${{ github.workspace }}/firebase ]]; then + tar -czf firebase.tar.gz firebase; + fi + working-directory: ${{ github.workspace }} + - name: The job has failed - archive npm logs + if: ${{ failure() }} + run: | + if [[ -d /home/runner/.npm/_logs ]]; then + tar -czf ${{ github.workspace }}/npm-logs.tar.gz /home/runner/.npm/_logs; + fi + working-directory: / + - name: The job has failed - upload artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.FIREBASE_PROJECT }}-firebase-debug + path: | + ${{ github.workspace }}/firebase.tar.gz + ${{ github.workspace }}/npm-logs.tar.gz + retention-days: 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa78362 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.firebase +/functions/ +/hosting/website/ +/hosting/api/ +*.log diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b44b815 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,36 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.formatOnSave": true, + "[json]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "full", + "editor.folding": true, + "editor.renderWhitespace": "all" + }, + "[javascript]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "full", + "editor.folding": true, + "editor.renderWhitespace": "all" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + { + "language": "typescript", + "autoFix": true + }, + { + "language": "typescriptreact", + "autoFix": true + } + ], + "files.associations": { + "*.json": "jsonc" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fc7e75 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Firebase Template + +This is part of the [firebase-template project](!https://github.com/search?q=topic%3Afirebase-template+org%3Adudko-dev&type=Repositories), which consists of: + +- `firebase` - firebase settings, including hosting settings and basic access rights, repo: [dudko-dev/firebase-template](https://github.com/dudko-dev/firebase-template) +- `firebase/functions` - gRPC and http cloud functions, repo: [dudko-dev/firebase-template-functions](https://github.com/dudko-dev/firebase-template-functions) +- `firebase/hosting/api` - a web portal based on react, repo: [dudko-dev/firebase-template-website](https://github.com/dudko-dev/firebase-template-website) +- `firebase/hosting/website` - web portal on js, repo: [dudko-dev/firebase-template-api](https://github.com/dudko-dev/firebase-template-api) + +The project allows you to quickly deploy the basic configuration of firebase, configure the basic skeleton of cloud functions, deploy a portal on react with ready-made authorization/registration/email confirmation/password recovery methods. + +just run a script: + +```bash +#!/usr/bin/bash +FIREBASE_REPO="dudko-dev/firebase-template" +FIREBASE_REPO_DIR="." +FIREBASE_FUNCTIONS_REPO="dudko-dev/firebase-template-functions" +FIREBASE_FUNCTIONS_DIR="./functions" +FIREBASE_WEBSITE_REPO="dudko-dev/firebase-template-website" +FIREBASE_WEBSITE_DIR="./hosting/website" +FIREBASE_WEBAPI_REPO="dudko-dev/firebase-template-api" +FIREBASE_WEBAPI_DIR="./hosting/api" + +mkdir $FIREBASE_REPO_DIR +git clone $FIREBASE_REPO $FIREBASE_REPO_DIR +mkdir $FIREBASE_FUNCTIONS_DIR +git clone $FIREBASE_FUNCTIONS_REPO $FIREBASE_FUNCTIONS_DIR +mkdir $FIREBASE_WEBSITE_DIR +git clone $FIREBASE_WEBSITE_REPO $FIREBASE_WEBSITE_DIR +mkdir $FIREBASE_WEBAPI_DIR +git clone $FIREBASE_WEBAPI_REPO $FIREBASE_WEBAPI_DIR +``` diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 0000000..50b55d2 --- /dev/null +++ b/database.rules.json @@ -0,0 +1,12 @@ +{ + "rules": { + ".read": false, + ".write": false, + "users": { + "$user_id": { + ".read": "$user_id === auth.uid", + ".write": false + } + } + } +} diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..0db40b2 --- /dev/null +++ b/firebase.json @@ -0,0 +1,93 @@ +{ + "database": { + "rules": "database.rules.json" + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": { + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint", + "npm --prefix \"$RESOURCE_DIR\" run build" + ] + }, + "storage": { + "rules": "storage.rules" + }, + "hosting": [ + { + "target": "website", + "public": "hosting/website/build", + "appAssociation": "AUTO", + "headers": [ + { + "source": "/**", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache, no-store, must-revalidate" + } + ] + }, + { + "source": "/static/**", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=7200" + } + ] + } + ], + "redirects": [], + "rewrites": [ + { + "source": "!/**/*.*", + "destination": "/index.html" + } + ] + }, + { + "target": "api", + "public": "hosting/api", + "rewrites": [ + { + "source": "**", + "function": "api" + } + ] + } + ], + "emulators": { + "auth": { + "port": 9099 + }, + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "database": { + "port": 9000 + }, + "hosting": { + "port": 5000 + }, + "pubsub": { + "port": 8085 + }, + "ui": { + "enabled": true + }, + "storage": { + "port": 9199 + }, + "singleProjectMode": true + }, + "remoteconfig": { + "template": "remoteconfig.template.json" + }, + "extensions": {} +} diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..415027e --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..a82328b --- /dev/null +++ b/firestore.rules @@ -0,0 +1,67 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // MARK: - Disabling access for all refs + + match /{allPaths=**} { + allow read, write: if false; + } + + // MARK: - Authentication + + function isSignedIn() { + return ((request.auth != null) + && (request.auth.uid != null)); + } + + // MARK: - Model Helpers + + function getUserPath() { + return (/databases/$(database)/documents/users/$(request.auth.uid)); + } + + function userExists() { + return (isSignedIn() + && (request.auth.token != null) + && exists(getUserPath())); + } + + function getUserData() { + return get(getUserPath()).data; + } + + function getAdminPath() { + return (/databases/$(database)/documents/admins/$(request.auth.uid)); + } + + function isAdmin() { + return (isSignedIn() + && (request.auth.token != null) + && exists(getAdminPath())); + } + + function getAdminData() { + return get(getAdminPath()).data; + } + + // MARK: - User Access + + match /_internal_/config { + allow read: if true; + } + + match /users/{userId}{ + allow read: if (isSignedIn() + && (request.auth.uid == userId)); + // allow write: if (isSignedIn() + // && (request.auth.uid == userId)); + } + + // MARK: - Admin Access + + match /{allPaths=**} { + allow read, write: if (isSignedIn() + && isAdmin()); + } + } +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ee6e831 --- /dev/null +++ b/install.sh @@ -0,0 +1,18 @@ +#!/usr/bin/bash +FIREBASE_REPO="dudko-dev/firebase-template" +FIREBASE_REPO_DIR="." +FIREBASE_FUNCTIONS_REPO="dudko-dev/firebase-template-functions" +FIREBASE_FUNCTIONS_DIR="./functions" +FIREBASE_WEBSITE_REPO="dudko-dev/firebase-template-website" +FIREBASE_WEBSITE_DIR="./hosting/website" +FIREBASE_WEBAPI_REPO="dudko-dev/firebase-template-api" +FIREBASE_WEBAPI_DIR="./hosting/api" + +mkdir $FIREBASE_REPO_DIR +git clone $FIREBASE_REPO $FIREBASE_REPO_DIR +mkdir $FIREBASE_FUNCTIONS_DIR +git clone $FIREBASE_FUNCTIONS_REPO $FIREBASE_FUNCTIONS_DIR +mkdir $FIREBASE_WEBSITE_DIR +git clone $FIREBASE_WEBSITE_REPO $FIREBASE_WEBSITE_DIR +mkdir $FIREBASE_WEBAPI_DIR +git clone $FIREBASE_WEBAPI_REPO $FIREBASE_WEBAPI_DIR diff --git a/remoteconfig.template.json b/remoteconfig.template.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/remoteconfig.template.json @@ -0,0 +1 @@ +{} diff --git a/storage.rules b/storage.rules new file mode 100644 index 0000000..d4dca21 --- /dev/null +++ b/storage.rules @@ -0,0 +1,73 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + // MARK: - Disabling access for all refs + + match /{allPaths=**} { + allow read, write: if false; + } + + // MARK: - Authentication + + function isSignedIn() { + return ((request.auth != null) + && (request.auth.uid != null)); + } + + // MARK: - Model Helpers + + function getUserPath() { + return (/databases/(default)/documents/users/$(request.auth.uid)); + } + + function userExists() { + return (isSignedIn() + && (request.auth.token != null) + && firestore.exists(getUserPath())); + } + + function getUserData() { + return firestore.get(getUserPath()).data; + } + + function getAdminPath() { + return (/databases/(default)/documents/admins/$(request.auth.uid)); + } + + function isAdmin() { + return (isSignedIn() + && (request.auth.token != null) + && firestore.exists(getAdminPath())); + } + + function getAdminData() { + return firestore.get(getAdminPath()).data; + } + + // MARK: - Public Access + + match /public/{allPaths=**} { + allow read: if true; + } + + // MARK: - User Access + + match /users/{userId}/{allPaths=**} { + allow read: if (isSignedIn() + && (request.auth.uid == userId)); + // allow write: if ( + // isSignedIn() + // && (request.auth.uid == userId) + // && (request.resource.size < 5 * 1024 * 1024) + // && request.resource.contentType.matches('image/.*') + // ) + } + + // MARK: - Admin Access + + match /{allPaths=**} { + allow read, write: if (isSignedIn() + && isAdmin()); + } + } +}