Skip to content

Commit

Permalink
feat: add cross device syncing with firebase (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJustChew authored Aug 5, 2024
2 parents 0346e2a + f3460f1 commit c96dd3a
Show file tree
Hide file tree
Showing 15 changed files with 2,023 additions and 170 deletions.
1,623 changes: 1,621 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
"concurrently": "^8.2.2",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.0.1",
"firebase": "^10.12.4",
"firebase-admin": "^12.3.0",
"fuse.js": "^6.6.2",
"html-entities": "^2.5.2",
"html-to-image": "^1.11.11",
Expand All @@ -112,6 +114,7 @@
"react-cookie": "^7.1.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-firebase-hooks": "^5.1.1",
"react-hook-form": "^7.51.4",
"react-instantsearch": "^7.8.1",
"react-instantsearch-nextjs": "^0.2.4",
Expand Down
53 changes: 33 additions & 20 deletions src/app/[lang]/(mods-pages)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use client';
'use client';;
import useDictionary from "@/dictionaries/useDictionary";
import { useSettings } from "@/hooks/contexts/settings";
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";
import LoginDialog from "@/components/Forms/LoginDialog";
import { TimetableThemeList } from "./TimetableThemeList";
import TimetablePreview from "./TimetablePreview";
Expand All @@ -12,21 +11,16 @@ import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Language } from "@/types/settings";
import Footer from '@/components/Footer';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import TimetablePreferences from "./TimetablePreferences";
import useUserTimetable from "@/hooks/contexts/useUserTimetable";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {GraduationCap, Hash} from 'lucide-react';
import { GraduationCap, Hash } from 'lucide-react';
import ChangePasswordDialog from "@/components/Forms/ChangePasswordDialog";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth } from "@/config/firebase";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";

const DisplaySettingsCard = () => {
const { darkMode, setDarkMode, language, setLanguage } = useSettings();
Expand Down Expand Up @@ -85,7 +79,7 @@ const TimetableSettingsCard = () => {
}

const AccountInfoSettingsCard = () => {
const { user, ais, setAISCredentials } = useHeadlessAIS();
const { user, ais, signOut } = useHeadlessAIS();
const dict = useDictionary();
const [openChangePassword, setOpenChangePassword] = useState(false);

Expand All @@ -98,22 +92,41 @@ const AccountInfoSettingsCard = () => {
{user && <div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 w-full">
<div className="flex flex-col gap-2">
<h2 className="text-xl font-semibold">{user.name_zh}</h2>
<h3 className="text-sm">{user.name_en}</h3>
<h2 className="text-xl font-semibold">{user.name_zh}</h2>
<h3 className="text-sm">{user.name_en}</h3>
</div>
<div className="flex flex-col gap-1">
<div className="text-gray-500 flex flex-row text-sm"><GraduationCap className="w-4 h-4 mr-2" /> {user.department}</div>
<div className="text-gray-500 flex flex-row text-sm"><Hash className="w-4 h-4 mr-2" /> {user.studentid}</div>
<div className="text-gray-500 flex flex-row text-sm"><GraduationCap className="w-4 h-4 mr-2" /> {user.department}</div>
<div className="text-gray-500 flex flex-row text-sm"><Hash className="w-4 h-4 mr-2" /> {user.studentid}</div>
</div>
</div>
<div className="flex flex-row justify-end items-center w-full gap-2">
<ChangePasswordDialog open={openChangePassword} setOpen={setOpenChangePassword}>
<Button variant={'ghost'}>更新密碼</Button>
</ChangePasswordDialog>
<Button variant="destructive" onClick={() => setAISCredentials()}>{dict.settings.account.signout}</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">{dict.settings.account.signout}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Logout?</DialogTitle>
<DialogDescription>
Are you sure you want to log out?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant='outline'>Cancel</Button>
</DialogClose>
<Button variant="destructive" onClick={() => signOut()}>{dict.settings.account.signout}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

</div>
</div>}
<div className={cn("flex flex-row gap-4 py-4", user ? 'hidden': '')} id="account">
<div className={cn("flex flex-row gap-4 py-4", user ? 'hidden' : '')} id="account">
<div className="flex flex-col flex-1 gap-1">
<h2 className="font-semibold text-base">{dict.settings.account.ccxp.title}</h2>
<p className="text-gray-600 dark:text-gray-400 text-sm">{dict.settings.account.ccxp.description}</p>
Expand Down
32 changes: 9 additions & 23 deletions src/components/Forms/ChangePasswordDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Expand Down Expand Up @@ -37,7 +38,7 @@ const formSchema = z.object({
})

const ChangePasswordDialog = ({ open, setOpen, children }: PropsWithChildren<{ open: boolean, setOpen: (s: boolean) => void }>) => {
const { setAISCredentials, user } = useHeadlessAIS();
const { signIn, signOut, user } = useHeadlessAIS();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
Expand All @@ -47,7 +48,7 @@ const ChangePasswordDialog = ({ open, setOpen, children }: PropsWithChildren<{ o

async function onSubmit(values: z.infer<typeof formSchema>) {
if (!user) return;
await setAISCredentials(user.studentid, values.newPassword)
await signIn(user.studentid, values.newPassword)
setOpen(false);
toast({
title: "密碼更新成功",
Expand Down Expand Up @@ -77,27 +78,12 @@ const ChangePasswordDialog = ({ open, setOpen, children }: PropsWithChildren<{ o
</FormItem>
)}
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant='destructive'>Logout</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>確定要登出嗎?</DialogTitle>
<DialogDescription>登出後將無法使用校務資訊系統相關功能,確定要登出嗎?</DialogDescription>
</DialogHeader>
<DialogClose asChild>
<Button >Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant='destructive' onClick={() => {
setAISCredentials("", "")
setOpen(false)
}}>Logout</Button>
</DialogClose>
</DialogContent>
</Dialog>
<Button type="submit">{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : "Submit"}</Button>
<DialogFooter>
<DialogClose>
<Button variant='outline'>Cancel</Button>
</DialogClose>
<Button type="submit" >{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : "Submit"}</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Forms/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export const LoginPage = ({ onClose }: { onClose: () => void; }) => {
const [agreeChecked, setAgreeChecked] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const { user, setAISCredentials } = useHeadlessAIS();
const { user, signIn } = useHeadlessAIS();
const dict = useDictionary();
const { language } = useSettings();

const onSubmit = async () => {
setLoading(true);
const result = await setAISCredentials(studentid, password);
const result = await signIn(studentid, password);
if (!result) {
setError(dict.ccxp.incorrect_credentials);
}
Expand Down
20 changes: 20 additions & 0 deletions src/config/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";


const firebaseConfig = {
apiKey: "AIzaSyAu3LZ0FZFTDukUmgsJlr6U_0KxBx34uBo",
authDomain: "nthumods-prod.firebaseapp.com",
projectId: "nthumods-prod",
storageBucket: "nthumods-prod.appspot.com",
messagingSenderId: "977252315806",
appId: "1:977252315806:web:e7fcf9992f1b916c2c018f",
measurementId: "G-D06ZGLBZR3"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export default app;
18 changes: 18 additions & 0 deletions src/config/firebase_admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { initializeApp, getApps, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { getFirestore } from "firebase-admin/firestore";

const serviceAccountBase64 = process.env.FIREBASE_SERVICE_ACCOUNT;
if(!serviceAccountBase64) throw new Error('FIREBASE_SERVICE_ACCOUNT is required');
const serviceAccount = JSON.parse(Buffer.from(serviceAccountBase64, 'base64').toString());

export const admin =
getApps().find((it) => it.name === "firebase-admin-app") ||
initializeApp(
{
credential: cert(serviceAccount),
},
"firebase-admin-app"
);
export const adminAuth = getAuth(admin);
export const adminFirestore = getFirestore(admin);
Loading

0 comments on commit c96dd3a

Please sign in to comment.