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 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

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.

+
+ + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ad41d9d --- /dev/null +++ b/public/index.html @@ -0,0 +1,89 @@ + + + + + + Welcome to Firebase Hosting + + + + + + + + + + + + + + + + + + + +
+

Welcome

+

Firebase Hosting Setup Complete

+

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; + } + } +}