Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
ukataria committed Aug 28, 2024
2 parents aa3a2cf + da67117 commit 81db067
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 57 deletions.
30 changes: 28 additions & 2 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,34 @@ rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write, delete: if true;
function isAdmin () {
return request.auth.token.role == "ADMIN"
}

match /users/{userId} {
allow read: if isAdmin() || request.auth.uid == userId
allow create: if true
allow update: if isAdmin() || request.auth.uid == userId
allow delete: if false
}
match /surveys/{surveyId} {
allow read: if isAdmin() || request.auth.uid in resource.data.assignedUsers
allow create: if isAdmin()
allow update: if isAdmin() || request.auth.uid in resource.data.assignedUsers
allow delete: if isAdmin()
}
match /responses/{responseId} {
allow read: if isAdmin() || request.auth.uid == resource.data.userId
allow create: if isAdmin() || request.auth.uid == resource.data.userId
allow update: if isAdmin() || request.auth.uid == resource.data.userId
allow delete: if false
}
match /metadata/adminRefreshToken {
allow read: if isAdmin()
allow write: if false
}
match /metadata/nextUserId {
allow read, write: if true
}
}
}
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions src/app/api/auth/claims/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { adminAuth } from "@/lib/firebase/firebaseAdminConfig";

// sets the role user claim to "STUDENT" for new account with the given uid
export async function POST(req: NextRequest): Promise<NextResponse> {
const body = await req.json();
try {
await adminAuth.setCustomUserClaims(body.uid, { role: "ADMIN" });
return NextResponse.json({ success: true }, { status: 200 });
} catch (err) {
console.error(err);
return NextResponse.json({ error: err }, { status: 500 });
}
}
5 changes: 5 additions & 0 deletions src/app/api/auth/consent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { authorizeWithGoogle } from "@/lib/googleAuthorization";

export function GET() {
authorizeWithGoogle();
}
17 changes: 17 additions & 0 deletions src/app/api/auth/handler/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
import { redirect } from "next/navigation";
import { setCredentialsWithAuthCode } from "@/lib/googleAuthorization";

export async function GET(req: NextRequest) {
const query = req.nextUrl.searchParams;
if (query.get("code")) {
console.log(query.get("code"));
const credsSet = await setCredentialsWithAuthCode(query.get("code") as string);
if (credsSet) {
return NextResponse.redirect("http://localhost:3000");
}
redirect("/");
}
console.error(query.get("error"));
redirect("/");
}
13 changes: 13 additions & 0 deletions src/app/api/auth/refreshToken/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { updateRefreshToken } from "@/lib/googleAuthorization";

// refreshs the admin account's refresh token if it does not exist or has expired
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
await updateRefreshToken();
return NextResponse.json({ success: true }, { status: 200 });
} catch (err) {
console.error(err);
return NextResponse.json({ error: err }, { status: 500 });
}
}
1 change: 1 addition & 0 deletions src/components/auth/RequireAdminAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function RequireAdminAuth({
} else if (!authContext.user) {
redirect("/");
} else if (authContext.token?.claims?.role != "ADMIN") {
console.log("Admin" + authContext.token)
redirect("/student-dashboard");
}

Expand Down
1 change: 1 addition & 0 deletions src/components/auth/RequireSignedOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function RequireSignedOut({
if (authContext.loading) {
return <p>Loading</p>;
} else if (authContext.user && authContext.token?.claims?.role) {
console.log("Signed out" + authContext.token)
redirect(authContext.token?.claims?.role == "ADMIN" ? '/admin-dashboard' : '/student-dashboard');
}

Expand Down
2 changes: 2 additions & 0 deletions src/components/auth/RequireStudentAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import AuthProvider, { useAuth } from './AuthProvider';

export default function RequireStudentAuth({ children }: { children: JSX.Element }): JSX.Element {
const authContext = useAuth();
console.log(authContext.token?.claims.role==="STUDENT")
console.log(authContext);
if (authContext.loading) {
return <p>Loading</p>
} else if (!authContext.user) {
redirect('/');
} else if (authContext.token?.claims?.role != "STUDENT") {
console.log("Student" + authContext.token)
redirect('/admin-dashboard');
}

Expand Down
41 changes: 22 additions & 19 deletions src/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ async function getGmailClient() {
return google.gmail({ version: "v1", auth: oauth2Client });
}

export async function sendEmail(body: { recipients: string[], subject: string, text: string }) {
const { recipients, subject, text } = body;
const gmailClient = await getGmailClient();
for (const recipient of recipients) {
const emailInfo = await gmailClient.users.messages.send({
userId: "me",
requestBody: {
raw: Buffer.from(`To: ${recipient}
Subject: ${subject}
${text}`).toString("base64"),
}
});

try {
console.log(emailInfo)
} catch (err) {
throw Error('Unable to send email');
}
export async function sendEmail(body: {
recipients: string[];
subject: string;
text: string;
}) {
const { recipients, subject, text } = body;
const gmailClient = await getGmailClient();
for (const recipient of recipients) {
try {
await gmailClient.users.messages.send({
userId: "me",
requestBody: {
raw: Buffer.from(
`Subject: ${subject}\r\nTo: ${recipient}\r\n
${text}`
).toString("base64"),
},
});
} catch (err) {
throw Error("unable to send email");
}
}
}
}
37 changes: 9 additions & 28 deletions src/lib/firebase/authentication/emailPasswordAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,18 @@ export const signUpUser = async (
);

const user = userCredential.user;
const userUid = user?.uid;

if (!userUid) {
throw new Error("Failed to sign up user: UID is null or undefined.");
}


await updateProfile(user, { displayName: "STUDENT" });

// Create the document in Firestore right after the user is created
const userDoc = {
isAdmin: false,
firstName: '', // Default values, update later
middleName: '', // Default values, update later
lastName: '', // Default values, update later
email: user.email,
phone: 0, // Default values, update later
gender: '', // Default values, update later
birthdate: null,// Default values, update later
guardian: [], // Default values, update later
id: userUid,
address: {},
school: '',
gradYear: 0,
yearsWithSwaliga: 0,
swaligaID: 0,
ethnicity: [],
assignedSurveys: [],
completedResponses: [],
};
await updateAccount(user.uid, { isAdmin: false });

await setDoc(doc(db, 'users', userUid), userDoc);
await fetch("/api/auth/user/claims", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ uid: user.uid })
});

return { success: true, userId: userUid };
} catch (error) {
Expand Down
21 changes: 19 additions & 2 deletions src/lib/firebase/authentication/googleAuthentication.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { updateRefreshToken } from '@/lib/googleAuthorization';
import { auth } from '../firebaseConfig';
import { FirebaseError } from 'firebase/app';
import { signOut, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
Expand All @@ -23,9 +24,8 @@ async function signInWithGoogle(): Promise<void> {
provider.addScope("https://www.googleapis.com/auth/spreadsheets");
provider.addScope("https://www.googleapis.com/auth/gmail.send")
try {
console.log(auth.currentUser);
const result = await signInWithPopup(auth, provider);
await fetch("/api/auth/user/claims", { method: "POST", body: JSON.stringify({ uid: result.user.uid }) });
await auth.currentUser?.getIdToken(true); // refresh ID token upon account creation to set role in user claims
const credential = GoogleAuthProvider.credentialFromResult(result);
const accessToken = credential?.accessToken;
const verified = await verifyGoogleToken(accessToken);
Expand All @@ -35,6 +35,23 @@ async function signInWithGoogle(): Promise<void> {
} else {
console.log("Please sign in again");
}

const idTokenResult = await auth.currentUser?.getIdTokenResult();
const role = idTokenResult?.claims.role;
switch (role) {
case undefined:
await fetch("/api/auth/claims", {
method: "POST",
body: JSON.stringify({ uid: result.user.uid }),
});
await auth.currentUser?.getIdTokenResult(true); // refresh ID token upon account creation to set role in user claims
break;
case "ADMIN":
await fetch("/api/auth/refreshToken", { method: "POST" });
break;
default:
break;
}
} catch (error: unknown) {
if ((error as FirebaseError).code === 'auth/account-exists-with-different-credential') {
console.log("Already existing email address");
Expand Down
59 changes: 56 additions & 3 deletions src/lib/googleAuthorization.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { google } from "googleapis";
import { doc, getDoc } from "firebase/firestore";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { auth, db } from "./firebase/firebaseConfig";
import { redirect } from "next/navigation";

export const oauth2Client = new google.auth.OAuth2(
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET,
"http://localhost:3000/api/auth/handler"
);

// ensures the credentials in oauth2Client are up to date
export async function setCredentials() {
if (!oauth2Client.credentials.refresh_token) {
const response = await getDoc(doc(db, "metadata", "adminRefreshToken"));
if (!response.exists()) {
throw new Error("no refresh token found");
throw new Error("invalid refresh token");
}
const { adminRefreshToken } = response.data();
oauth2Client.setCredentials({ refresh_token: adminRefreshToken });
Expand All @@ -31,3 +33,54 @@ export async function setCredentials() {
const response = await oauth2Client.refreshAccessToken();
oauth2Client.setCredentials(response.credentials);
}

export function authorizeWithGoogle() {
const scopes = [
"https://www.googleapis.com/auth/forms.body",
"https://www.googleapis.com/auth/forms.responses.readonly",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/gmail.send",
];

const authorizationUrl = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
include_granted_scopes: true,
});

redirect(authorizationUrl);
}

export async function setCredentialsWithAuthCode(authCode: string): Promise<boolean> {
try {
const { tokens } = await oauth2Client.getToken(authCode);
console.log('tokens', tokens);
oauth2Client.setCredentials(tokens);
await setDoc(doc(db, "metadata", "adminRefreshToken"), { adminRefreshToken: tokens.refresh_token })
return true;
} catch (err) {
console.error("getting tokens failed");
console.log(err);
return false;
}
}

export async function updateRefreshToken() {
if (await isRefreshTokenValid()) {
return;
}
redirect("/api/auth/consent");
}

export async function isRefreshTokenValid(): Promise<boolean> {
const response = await getDoc(doc(db, "metadata", "adminRefreshToken"));
if (!response.exists()) {
return false;
}
try {
await setCredentials();
return true;
} catch (err) {
return false;
}
}

0 comments on commit 81db067

Please sign in to comment.