Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added firebase auth #2213

Merged
merged 41 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f8d84c5
feat: add upvote system
Vidip-Ghosh Oct 21, 2023
b8037c7
resolves_conflicts
Vidip-Ghosh Oct 26, 2023
8e9c5b2
Merge branch 'feat_upvote_1853' of https://github.com/Vidip-Ghosh/Lin…
Vidip-Ghosh Oct 26, 2023
1ea4504
resolves_conflicts
Vidip-Ghosh Oct 26, 2023
a82f78d
gitMerge branch 'main' of https://github.com/Vidip-Ghosh/LinksHub int…
Vidip-Ghosh Oct 29, 2023
abb1679
Merge branch 'rupali-codes:main' into feat_upvote_1853
Vidip-Ghosh Oct 29, 2023
0acb06e
Merge branch 'feat_upvote_1853' of https://github.com/Vidip-Ghosh/Lin…
Vidip-Ghosh Oct 29, 2023
30d599a
pnpm file commited
Vidip-Ghosh Oct 29, 2023
32d6987
Merge branch 'rupali-codes:main' into feat_upvote_1853
Vidip-Ghosh Oct 31, 2023
8ea420f
Merge branch 'rupali-codes:main' into feat_upvote_1853
Vidip-Ghosh Nov 3, 2023
b0002fc
adds: upvote icon
Vidip-Ghosh Nov 3, 2023
44d7edb
Merge branch 'rupali-codes:main' into feat_upvote_1853
Vidip-Ghosh Nov 4, 2023
7618086
changes: upvote logic
Vidip-Ghosh Nov 6, 2023
ee63b8d
fix: upvote_icon
Vidip-Ghosh Nov 8, 2023
179b90e
Merge branch 'rupali-codes:main' into feat_upvote_1853
Vidip-Ghosh Nov 8, 2023
c2f3a83
fix: decrease count for few resources
Vidip-Ghosh Nov 8, 2023
2f1270e
fix: upvote function
Vidip-Ghosh Nov 11, 2023
535009e
feat: add_firebase_github_auth
Vidip-Ghosh Nov 23, 2023
e1b03ab
Merge branch 'rupali-codes:main' into feat_auth_1852
Vidip-Ghosh Nov 23, 2023
0d0cab6
feat: Add firebase github auth
Vidip-Ghosh Nov 26, 2023
47eb2c7
Merge branch 'feat_auth_1852' of https://github.com/Vidip-Ghosh/Links…
Vidip-Ghosh Nov 26, 2023
b8fe6b2
fix: text color light mode
Vidip-Ghosh Nov 26, 2023
58a7846
fix: Sensitive token in .env
Vidip-Ghosh Nov 27, 2023
d6c9c7f
fix: resource visible before signin but can't upvote functionality
Vidip-Ghosh Nov 29, 2023
ad2964a
fix: dependencies
Vidip-Ghosh Nov 29, 2023
79403c2
feat: add toast alerts
Vidip-Ghosh Dec 2, 2023
252f706
fix: toast multiple occurence
Vidip-Ghosh Dec 2, 2023
473b36f
fix: toast multiple occurence
Vidip-Ghosh Dec 2, 2023
c891513
fix: toast double occurence
Vidip-Ghosh Dec 3, 2023
6dec0bc
Merge branch 'rupali-codes:main' into feat_auth_1852
Vidip-Ghosh Dec 7, 2023
6488736
fixes: lint error warnings
Vidip-Ghosh Dec 19, 2023
1d879c3
removes: save-remove code
Vidip-Ghosh Dec 19, 2023
60a733a
fixes: error in card.tsx
Vidip-Ghosh Dec 19, 2023
4d64586
fix: eslint warnings
Vidip-Ghosh Dec 19, 2023
5234b59
removes: firebase-tool dependency
Vidip-Ghosh Dec 20, 2023
6e8a32f
removes: getUser.ts file
Vidip-Ghosh Dec 20, 2023
13ed467
chore: temp commit
rupali-codes Dec 22, 2023
64a49eb
chore: test commit
rupali-codes Dec 22, 2023
aceca6d
chore: test commit 2
rupali-codes Dec 22, 2023
1d93c45
Merge branch 'rupali-codes:main' into feat_auth_1852
Vidip-Ghosh Dec 22, 2023
a267c5e
chore: removes console log
Vidip-Ghosh Dec 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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