diff --git a/.env.sample b/.env.sample index 90b845e..9525097 100644 --- a/.env.sample +++ b/.env.sample @@ -32,3 +32,12 @@ FACEBOOK_CLIENT_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +FIREBASE_API_KEY= +FIREBASE_AUTH_DOMAIN= +FIREBASE_DATABASE_URL= +FIREBASE_PROJECT_ID= +FIREBASE_STORAGE_BUCKET= +FIREBASE_MESSAGING_SENDER_ID= +FIREBASE_APP_ID= +FIREBASE_MEASUREMENT_ID= + diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..b87b610 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "noteups-web-original-backend" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..490bcdf --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +'on': + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_NOTEUPS_WEB_ORIGINAL_BACKEND }}' + channelId: live + projectId: noteups-web-original-backend diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..a56b2e9 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,17 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +'on': pull_request +jobs: + build_and_preview: + if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_NOTEUPS_WEB_ORIGINAL_BACKEND }}' + projectId: noteups-web-original-backend diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 0000000..f54493d --- /dev/null +++ b/database.rules.json @@ -0,0 +1,7 @@ +{ + /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ + "rules": { + ".read": false, + ".write": false + } +} \ No newline at end of file diff --git a/firebase.config.js b/firebase.config.js new file mode 100644 index 0000000..0d69673 --- /dev/null +++ b/firebase.config.js @@ -0,0 +1,34 @@ +// Import the functions you need from the SDKs you need +import { getAnalytics } from "firebase/analytics"; +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getDatabase } from "firebase/database"; +import { getFirestore } from "firebase/firestore"; +import { getFunctions } from "firebase/functions"; +import { getStorage } from "firebase/storage"; + +// TODO: Add SDKs for Firebase products that you want to use +// https://firebase.google.com/docs/web/setup#available-libraries + +// Your web app's Firebase configuration +// For Firebase JS SDK v7.20.0 and later, measurementId is optional +const firebaseConfig = { + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + databaseURL: process.env.FIREBASE_DATABASE_URL, + projectId: process.env.FIREBASE_PROJECT_ID, + storageBucket: process.env.FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID, + measurementId: process.env.FIREBASE_MEASUREMENT_ID, +}; + +// Initialize Firebase +export const firebaseApp = initializeApp(firebaseConfig); +export const firebaseAnalytics = getAnalytics(app); +export const firebaseAuth = getAuth(app); +export const firebaseDb = getFirestore(app); +export const firebaseRealtimeDb = getDatabase(app); +export const firebaseStorage = getStorage(app); +export const firebaseFunctions = getFunctions(app); + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..f066668 --- /dev/null +++ b/firebase.json @@ -0,0 +1,41 @@ +{ + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint" + ] + } + ], + "database": { + "rules": "database.rules.json" + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "hosting": { + "public": "public", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + "storage": { + "rules": "storage.rules" + } +} diff --git a/firebase/FirebaseAuth.js b/firebase/FirebaseAuth.js new file mode 100644 index 0000000..8c532c4 --- /dev/null +++ b/firebase/FirebaseAuth.js @@ -0,0 +1,182 @@ +const firebase = require("firebase/app"); +const { firebaseAuth, firebaseDb } = require("../firebase.config"); + +const auth = firebaseAuth; +const db = firebaseDb; + +// Google Sign In +const googleProvider = new firebase.auth.GoogleAuthProvider(); +googleProvider.setCustomParameters({ prompt: "select_account" }); + +const signInWithGoogle = async () => { + try { + const result = await auth.signInWithPopup(googleProvider); + const user = result.user; + const { displayName, email, photoURL } = user; + + // Check if user already exists in Firestore + const userRef = db.collection("users").doc(user.uid); + const doc = await userRef.get(); + if (!doc.exists) { + // Create new user in Firestore + await userRef.set({ + name: displayName, + email, + photoURL, + }); + } + } catch (error) { + console.error(error); + } +}; + +// Facebook Sign In +const facebookProvider = new firebase.auth.FacebookAuthProvider(); + +const signInWithFacebook = async () => { + try { + const result = await auth.signInWithPopup(facebookProvider); + const user = result.user; + const { displayName, email, photoURL } = user; + + // Check if user already exists in Firestore + const userRef = db.collection("users").doc(user.uid); + const doc = await userRef.get(); + if (!doc.exists) { + // Create new user in Firestore + await userRef.set({ + name: displayName, + email, + photoURL, + }); + } + } catch (error) { + console.error(error); + } +}; + +// GitHub Sign In +const githubProvider = new firebase.auth.GithubAuthProvider(); + +const signInWithGithub = async () => { + try { + const result = await auth.signInWithPopup(githubProvider); + const user = result.user; + const { displayName, email, photoURL } = user; + + // Check if user already exists in Firestore + const userRef = db.collection("users").doc(user.uid); + const doc = await userRef.get(); + if (!doc.exists) { + // Create new user in Firestore + await userRef.set({ + name: displayName, + email, + photoURL, + }); + } + } catch (error) { + console.error(error); + } +}; + +// Email and Password Sign In +const signInWithEmailAndPassword = async (email, password) => { + try { + const result = await auth.signInWithEmailAndPassword(email, password); + const user = result.user; + const { displayName, email, photoURL } = user; + + // Check if user already exists in Firestore + const userRef = db.collection("users").doc(user.uid); + const doc = await userRef.get(); + if (!doc.exists) { + // Create new user in Firestore + await userRef.set({ + name: displayName, + email, + photoURL, + }); + } + } catch (error) { + console.error(error); + } +}; + +// Link Google Account +const linkWithGoogle = async () => { + try { + const result = await auth.signInWithPopup(googleProvider); + const credential = result.credential; + await auth.currentUser.linkWithCredential(credential); + + // Update Firestore with additional data from Google account + const { displayName, email, photoURL } = result.user; + await db + .collection("users") + .doc(auth.currentUser.uid) + .update({ + name: displayName || "", + email: email || "", + photoURL: photoURL || "", + }); + } catch (error) { + if (error.code === "auth/credential-already-in-use") { + // The Google account is already linked to another user + // You can handle this case by merging the accounts + console.error(error.message); + } + } +}; + +// Link Facebook Account +const linkWithFacebook = async () => { + try { + const result = await auth.signInWithPopup(facebookProvider); + const credential = result.credential; + await auth.currentUser.linkWithCredential(credential); + + // Update Firestore with additional data from Facebook account + const { displayName, email, photoURL } = result.user; + await db + .collection("users") + .doc(auth.currentUser.uid) + .update({ + name: displayName || "", + email: email || "", + photoURL: photoURL || "", + }); + } catch (error) { + if (error.code === "auth/credential-already-in-use") { + // The Facebook account is already linked to another user + // You can handle this case by merging the accounts + console.error(error.message); + } + } +}; + +// Link GitHub Account +const linkWithGithub = async () => { + try { + const result = await auth.signInWithPopup(githubProvider); + const credential = result.credential; + await auth.currentUser.linkWithCredential(credential); + + // Update Firestore with additional data from GitHub account + const { displayName, email, photoURL } = result.user; + await db + .collection("users") + .doc(auth.currentUser.uid) + .update({ + name: displayName || "", + email: email || "", + photoURL: photoURL || "", + }); + } catch (error) { + if (error.code === "auth/credential-already-in-use") { + // The GitHub account is already linked to another user + // You can handle this case by merging the accounts + console.error(error.message); + } + } +}; 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..c38e3ae --- /dev/null +++ b/firestore.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} \ No newline at end of file diff --git a/functions/.eslintrc.js b/functions/.eslintrc.js new file mode 100644 index 0000000..f4cb76c --- /dev/null +++ b/functions/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + env: { + es6: true, + node: true, + }, + parserOptions: { + "ecmaVersion": 2018, + }, + extends: [ + "eslint:recommended", + "google", + ], + rules: { + "no-restricted-globals": ["error", "name", "length"], + "prefer-arrow-callback": "error", + "quotes": ["error", "double", {"allowTemplateLiterals": true}], + }, + overrides: [ + { + files: ["**/*.spec.*"], + env: { + mocha: true, + }, + rules: {}, + }, + ], + globals: {}, +}; diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 0000000..e81477f --- /dev/null +++ b/functions/index.js @@ -0,0 +1,19 @@ +/** + * Import function triggers from their respective submodules: + * + * const {onCall} = require("firebase-functions/v2/https"); + * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ + +const {onRequest} = require("firebase-functions/v2/https"); +const logger = require("firebase-functions/logger"); + +// Create and deploy your first functions +// https://firebase.google.com/docs/functions/get-started + +// exports.helloWorld = onRequest((request, response) => { +// logger.info("Hello logs!", {structuredData: true}); +// response.send("Hello from Firebase!"); +// }); diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 0000000..a88bc5e --- /dev/null +++ b/functions/package.json @@ -0,0 +1,26 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "eslint .", + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^11.8.0", + "firebase-functions": "^4.3.1" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/public/404.html @@ -0,0 +1,33 @@ + + +
+ + +The specified file was not found on this website. Please check the URL for mistakes and try again.
+This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html
file in your project's configured public
directory.
You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!
+ Open Hosting Documentation +Firebase SDK Loading…
+ + + + diff --git a/storage.rules b/storage.rules new file mode 100644 index 0000000..f08744f --- /dev/null +++ b/storage.rules @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +}