Skip to content

Commit

Permalink
feat: added firebase auth (#2213)
Browse files Browse the repository at this point in the history
* feat: add upvote system

* resolves_conflicts

* resolves_conflicts

* pnpm file commited

* adds: upvote icon

* changes: upvote logic

* fix: upvote_icon

* fix: decrease count for few resources

* fix: upvote function

* feat: add_firebase_github_auth

* feat: Add firebase github auth

* fix: text color light mode

* fix: Sensitive token in .env

* fix: resource visible before signin but can't upvote functionality

* fix: dependencies

* feat: add toast alerts

* fix: toast multiple occurence

* fix: toast multiple occurence

* fix: toast double occurence

* fixes: lint error warnings

* removes: save-remove code

* fixes: error in card.tsx

* fix: eslint warnings

* removes: firebase-tool dependency

* removes: getUser.ts file

* chore: temp commit

* chore: test commit

* chore: test commit 2

* chore: removes console log

---------

Co-authored-by: Rupali Haldiya <[email protected]>
  • Loading branch information
Vidip-Ghosh and rupali-codes authored Dec 22, 2023
1 parent ffa4d1c commit 42a718b
Show file tree
Hide file tree
Showing 19 changed files with 2,335 additions and 39 deletions.
97 changes: 97 additions & 0 deletions app/SignInWithGithub/SignInWithGithub.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client"
import { signInWithPopup,GithubAuthProvider,signOut } from 'firebase/auth'
import React, { useEffect, useState } from 'react'
import {auth,provider} from '../../lib/firebase-config'
import { useRouter } from 'next/router'
import Image from 'next/image'
import { toast,ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css';

const SignInWithGithub=()=>{
const router = useRouter();
const [imageURL, setImageURL] = useState<string | null>(null);
const [authenticated,setAuthenticated] = useState(false);
const signIn = async () => {
try {
const result = await signInWithPopup(auth, provider);
const credential = GithubAuthProvider.credentialFromResult(result);
console.log("Credential",credential);
console.log("Result: ",result);
const token: string = await result.user.getIdToken();
const username = result.user.displayName;
localStorage.setItem('accessToken', token);
const currDate = new Date().getTime;
document.cookie = `accessToken=${token};path=/; expires=${currDate}`;
const imgURL = result.user.photoURL;
localStorage.setItem('imageURL',imgURL as string);
setImageURL(imgURL);
setAuthenticated(true);

fetch('/api/auth',{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
}
}).then((response)=>{
if(response.status===200)
{
router.push('/');
toast.success(`${username} is authenticated successfully`);
}
})
}
catch (error) {
console.error('Error signing in:', error);
}
}

const handleSignOut=async()=>{
try {
await signOut(auth);
router.push("/");
toast.success("You are successfully logged out!!");
const currDate = new Date().getTime;
document.cookie = `accessToken=; expires=${currDate}; path=/;`;
console.log(document.cookie);
localStorage.removeItem('accessToken');
localStorage.removeItem('imageURL');
setAuthenticated(false);
setImageURL(null);
} catch (error) {
console.log(error);
}
}

useEffect(()=>{
const storedToken = localStorage.getItem('accessToken');
const storedImageURL = localStorage.getItem('imageURL');
if (storedToken && storedImageURL) {
setImageURL(storedImageURL);
setAuthenticated(true);
}
},[])
return (
<>
<div>
{authenticated && (
<div>
{imageURL && <Image height={100} width={100} className='rounded-lg' src={imageURL} alt='User Profile' />}
</div>
)}
<ToastContainer />
</div>
{authenticated ? (
<button style={{ background: '#4d0080',color:'white', padding: 10 }} onClick={handleSignOut}>
Sign Out
</button>
) : (
<button style={{ background: '#4d0080',color:'white', padding: 10 }} onClick={signIn}>
Sign In With Github
</button>
)}
</>
)

}

export default SignInWithGithub;
68 changes: 68 additions & 0 deletions app/api/auth/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { auth } from "firebase-admin";
import { cookies } from "next/headers"
import { NextRequest,NextResponse } from "next/server";
import { customInitApp } from "lib/firebase-admin-config";

customInitApp();

export async function POST(request: NextRequest){
const authorization = request.headers.get("Authorization");
if(authorization && authorization.startsWith('Bearer '))
{
const idToken = authorization.split("Bearer ")[1];

const decodedToken = await auth().verifyIdToken(idToken);
if(decodedToken)
{
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const sessionCookie = await auth().createSessionCookie(idToken,{
expiresIn
});

const options = {
name: "Session",
value: sessionCookie,
maxAge: expiresIn,
httpOnly: true,
secure: true,
};
cookies().set(options);
}
}
return NextResponse.json({}, { status: 200 });
}

export async function GET()
{
const session = cookies().get("accessToken")?.value || "";
if(!session)
{
return NextResponse.json({isLogged: false},{status: 401});
}
const decodedClaims = await auth().verifySessionCookie(session,true);
if(!decodedClaims)
{
return NextResponse.json({isLogged: false},{status: 401});
}
return NextResponse.json({ isLogged: true }, { status: 200});
}

export async function DELETE(request: NextRequest, response: NextResponse) {
const session = request.cookies.get("accessToken");

if (!session) {
return NextResponse.json({ isLogged: false }, { status: 401 });
}

try {
response.cookies.set('accessToken', '', {
httpOnly: true,
maxAge: 0,
})

return NextResponse.json({}, { status: 200 });
} catch (error) {
console.error('Error in handleLogout:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
16 changes: 16 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
124 changes: 121 additions & 3 deletions components/Cards/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,135 @@
import { FC, useState, useRef, useEffect } from 'react'
import React, { FC, useState, useRef, useEffect } from 'react'
import {BsYoutube , BsPen} from 'react-icons/bs'
import {AiOutlineRead} from 'react-icons/ai'
import{MdArticle} from 'react-icons/md'
import { CopyToClipboard } from 'components/CopyToClipboard/CopyToClipboard'
import Share from 'components/Share/Share'
import type { IData } from 'types'
import { collection, doc,where,query,getDocs, setDoc,getDoc } from 'firebase/firestore'
import { onAuthStateChanged } from 'firebase/auth'
import {db,auth} from '../../lib/firebase-config'
import { Timestamp } from 'firebase/firestore'
import Image from 'next/image'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css';

interface CardProps {
data: IData
data: IData,
}

export const Card: FC<CardProps> = ({ data }) => {
const { name, description, url,subcategory } = data
const descriptionRef = useRef<HTMLParagraphElement>(null)
const [isOverflow, setIsOverflow] = useState(false)
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/
const id = data.url.replace(/[^\w\s]/gi, '');

const [upvoteCount,setUpvoteCount] = useState(0)
const [isUpvoted,setIsUpvoted] = useState(false);
const timestamp = Timestamp.fromDate(new Date())
const date = timestamp.toDate()
const [user,setUser] = useState<string | null>(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user.displayName);
} else {
setUser(null);
}
console.log('Authentication state changed:', user);
});

return () => unsubscribe();
}, []);
const docRef = doc(db, 'resources', id)
const save = async()=>{
await setDoc(docRef, {
name: name,
description: description,
url: url,
upvotedBy: [user],
upvotes: upvoteCount,
created: date,
},
{ merge: true }
)
}

const addUserToAssetBookmark = async () => {
try {
const subcollectionRef = collection(db, 'resources')
const assetQuery = query(subcollectionRef, where('name', '==', data.name))
const assetQuerySnapshot = await getDocs(assetQuery)

if (assetQuerySnapshot.empty) {
console.log('Asset not found');
return;
}

const assetDocSnapshot = assetQuerySnapshot.docs[0];
const assetDocRef = doc(db, 'resources', data.name);
const assetData = assetDocSnapshot.data();
const upvotes = assetData.upvotes || {};
const userUid = auth.currentUser? auth.currentUser.uid : null;

if (userUid && upvotes[userUid]) {
// User has already upvoted, so remove their upvote
delete upvotes[userUid];
} else {
// User has not upvoted, so add their upvote
if(userUid)
{
upvotes[userUid] = true;
}
}
await setDoc(assetDocRef, {
...assetData, // Keep existing data
upvotes: upvotes,
});

await getDoc(assetDocRef);

const updatedAssetDoc = await getDoc(assetDocRef);
if (!updatedAssetDoc.exists()) {
console.log('Asset document not found');
return;
}
const updatedUpvotes = updatedAssetDoc.data().upvotes || {};
const upvoteCount = Object.keys(updatedUpvotes).length;
setUpvoteCount(upvoteCount);
} catch (error) {
console.error('Error adding user to asset upvotes:', error);
}
};

const toggleUpvote = () => {
setIsUpvoted(p => !p);
};
const [errorToastShown, setErrorToastShown] = useState(false);
const handleClick = async(e: React.MouseEvent<HTMLButtonElement >)=>{

const currentUser = auth.currentUser;
if (!currentUser && !errorToastShown) {
console.log('User is not authenticated');
toast.error('Please Sign In to upvote!!');
setErrorToastShown(true);
setTimeout(()=>{
window.location.href = '/';
},2000)
toggleUpvote();
}
e.stopPropagation();
e.preventDefault();
toggleUpvote();
save();
await addUserToAssetBookmark();
}
/* eslint-disable @typescript-eslint/no-explicit-any */
function Img({ url }:any) {
return (
<Image src={`${url}`} alt={'altimage'} width={40} height={40} />
);
}

useEffect(() => {
if (descriptionRef.current) {
Expand All @@ -24,7 +139,6 @@ export const Card: FC<CardProps> = ({ data }) => {
)
}
}, [])

return (
<article className="z-10 h-full w-full rounded-3xl border border-dashed border-theme-secondary dark:border-theme-primary bg-[rgba(255,255,255,0.3)] shadow-md dark:bg-dark dark:text-text-primary dark:shadow-sm">
<div className="card-body">
Expand Down Expand Up @@ -53,6 +167,10 @@ export const Card: FC<CardProps> = ({ data }) => {
</p>
)}
</div>
<div className='flex'>
<p className='text-3xl'>{upvoteCount}</p>
<button onClick={handleClick}><Img url={isUpvoted ? '/upvoteFilled.png' : '/upvote.png'} toggleUpvote={toggleUpvote}/></button>
</div>
<footer className="card-actions justify-end">
<a
onClick={(e) => e.stopPropagation()}
Expand Down
2 changes: 2 additions & 0 deletions components/Cards/CardsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BackToTopButton } from '../BackToTop/BackToTopButton'
import { PopupInfo } from 'components/popup/popupInfo'
import CardsListItem from './CardsListItem'
import type { IData } from 'types'
import { ToastContainer } from 'react-toastify'

const CardsList: FC<{ cards: IData[] }> = ({ cards }) => {
const [currentCard, setCurrentCard] = useState<IData | null>(null)
Expand All @@ -29,6 +30,7 @@ const CardsList: FC<{ cards: IData[] }> = ({ cards }) => {
))}
</ul>
<BackToTopButton />
<ToastContainer/>
<PopupInfo currentCard={currentCard} onClose={removeCurrentCard} />
</>
)
Expand Down
2 changes: 0 additions & 2 deletions components/Searchbar/SearchbarReducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useRouter } from "next/router"

export interface SearchbarState {
searchQuery: string
categoryQuery: string
Expand Down
1 change: 0 additions & 1 deletion components/SideNavbar/SideNavbarCategory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { SideNavbarElement } from './SideNavbarElement'
import type { ISidebar } from '../../types'
import Link from 'next/link'
import useOnClickOutside from 'hooks/useOnClickOutside'
import { useSearchReducer } from 'hooks/useSearchReducer'
import { useRouter } from 'next/router'

export const SideNavbarCategory: FC<{
Expand Down
13 changes: 13 additions & 0 deletions lib/firebase-admin-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import admin from "firebase-admin"
import { initializeApp,cert,getApps } from "firebase-admin/app"
import {firebaseConfig} from "../service-account.ts";

const firebaseAdminConfig = {
credential: cert(firebaseConfig as admin.ServiceAccount)
}

export function customInitApp() {
if(!getApps().length) {
initializeApp(firebaseAdminConfig)
}
}
Loading

0 comments on commit 42a718b

Please sign in to comment.