Merge branch 'develop'

This commit is contained in:
Tiago Ribeiro
2024-03-12 17:56:57 +00:00
18 changed files with 231 additions and 155 deletions

View File

@@ -2,6 +2,7 @@ import {Module} from ".";
import {InstructorGender} from "./exam"; import {InstructorGender} from "./exam";
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser; export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
export type UserStatus = "active" | "disabled" | "paymentDue";
export interface BasicUser { export interface BasicUser {
email: string; email: string;
@@ -17,7 +18,7 @@ export interface BasicUser {
isVerified: boolean; isVerified: boolean;
subscriptionExpirationDate?: null | Date; subscriptionExpirationDate?: null | Date;
registrationDate?: Date; registrationDate?: Date;
status: "active" | "disabled" | "paymentDue"; status: UserStatus;
} }
export interface StudentUser extends BasicUser { export interface StudentUser extends BasicUser {

View File

@@ -23,11 +23,11 @@ export function getServerSideProps({
res: any; res: any;
}) { }) {
if (!query || !query.oobCode || !query.mode) { if (!query || !query.oobCode || !query.mode) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: {}, redirect: {
destination: "/login",
permanent: false,
}
}; };
} }

View File

@@ -60,6 +60,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
payment_source: { payment_source: {
paypal: { paypal: {
email_address: req.session.user.email || "", email_address: req.session.user.email || "",
address: {
address_line_1: "",
address_line_2: "",
admin_area_1: "",
admin_area_2: "",
// added default values as requsted by the client, using the default values recommended
// the paypal engineer, otherwise we would have to create something that would detect the location
// of the user and generate a valid postal code for that location...
country_code: "US",
postal_code: "94107",
},
experience_context: { experience_context: {
payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED", payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED",
locale: "en-US", locale: "en-US",

View File

@@ -12,6 +12,8 @@ import moment from "moment";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import {Payment} from "@/interfaces/paypal"; import {Payment} from "@/interfaces/paypal";
import {toFixedNumber} from "@/utils/number"; import {toFixedNumber} from "@/utils/number";
import { propagateStatusChange } from '@/utils/propagate.user.changes';
const db = getFirestore(app); const db = getFirestore(app);
const auth = getAuth(app); const auth = getAuth(app);
@@ -74,12 +76,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return; return;
} }
const userRef = doc(db, "users", req.query.id ? (req.query.id as string) : req.session.user.id); const queryId = req.query.id as string;
const userRef = doc(db, "users", queryId ? (queryId as string) : req.session.user.id);
const updatedUser = req.body as User & {password?: string; newPassword?: string}; const updatedUser = req.body as User & {password?: string; newPassword?: string};
if (!!req.query.id) { if (!!queryId) {
const user = await setDoc(userRef, updatedUser, {merge: true}); const user = await setDoc(userRef, updatedUser, {merge: true});
await managePaymentRecords(updatedUser, updatedUser.id); await managePaymentRecords(updatedUser, updatedUser.id);
if(updatedUser.status) {
// there's no await as this does not affect the user
propagateStatusChange(queryId, updatedUser.status);
}
res.status(200).json({ok: true}); res.status(200).json({ok: true});
return; return;
} }
@@ -132,6 +142,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
} }
if(updatedUser.status) {
// there's no await as this does not affect the user
propagateStatusChange(req.session.user.id, updatedUser.status);
}
delete updatedUser.password; delete updatedUser.password;
delete updatedUser.newPassword; delete updatedUser.newPassword;
@@ -140,12 +155,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const docUser = await getDoc(doc(db, "users", req.session.user.id)); const docUser = await getDoc(doc(db, "users", req.session.user.id));
const user = docUser.data() as User; const user = docUser.data() as User;
if (!req.query.id) { if (!queryId) {
req.session.user = {...user, id: req.session.user.id}; req.session.user = {...user, id: req.session.user.id};
await req.session.save(); await req.session.save();
} }
await managePaymentRecords(user, req.query.id); await managePaymentRecords(user, queryId);
res.status(200).json({user}); res.status(200).json({user});
} }

View File

@@ -10,24 +10,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -10,24 +10,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -26,24 +26,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user) || user.type !== "developer") { if (shouldRedirectHome(user) || user.type !== "developer") {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -45,14 +45,11 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}); });
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
envVariables, permanent: false,
}, }
}; };
} }
@@ -61,7 +58,12 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}; };
}, sessionOptions); }, sessionOptions);
export default function Home({envVariables}: {envVariables: {[key: string]: string}}) { interface Props {
user: any;
envVariables: {[key: string]: string};
}
export default function Home(props: Props) {
const { envVariables} = props;
const [showDiagnostics, setShowDiagnostics] = useState(false); const [showDiagnostics, setShowDiagnostics] = useState(false);
const [showDemographicInput, setShowDemographicInput] = useState(false); const [showDemographicInput, setShowDemographicInput] = useState(false);
const [selectedScreen, setSelectedScreen] = useState<Type>("admin"); const [selectedScreen, setSelectedScreen] = useState<Type>("admin");

View File

@@ -22,14 +22,11 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
}); });
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
envVariables, permanent: false,
}, }
}; };
} }

View File

@@ -31,15 +31,12 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}); });
if (user && user.isVerified) { if (user && user.isVerified) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
envVariables, permanent: false,
}, }
}; };
} }
return { return {

View File

@@ -42,25 +42,21 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}; }
};
} }
if (shouldRedirectHome(user) || !["admin", "developer"].includes(user.type)) { if (shouldRedirectHome(user) || !["admin", "developer"].includes(user.type)) {
res.setHeader("location", "/"); return {
res.statusCode = 302; redirect: {
res.end(); destination: "/",
return { permanent: false,
props: { }
user: null, };
},
};
} }
return { return {

View File

@@ -17,15 +17,12 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}); });
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
envVariables, permanent: false,
}, }
}; };
} }
return { return {

View File

@@ -37,24 +37,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -30,24 +30,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -18,24 +18,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user) || user.type !== "developer") { if (shouldRedirectHome(user) || user.type !== "developer") {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -35,24 +35,20 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/login",
}, permanent: false,
}
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return { return {
props: { redirect: {
user: null, destination: "/",
}, permanent: false,
}
}; };
} }

View File

@@ -35,28 +35,24 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
res.setHeader("location", "/login"); return {
res.statusCode = 302; redirect: {
res.end(); destination: "/login",
return { permanent: false,
props: { }
user: null, };
},
};
} }
if ( if (
shouldRedirectHome(user) || shouldRedirectHome(user) ||
["admin", "developer", "agent"].includes(user.type) ["admin", "developer", "agent"].includes(user.type)
) { ) {
res.setHeader("location", "/"); return {
res.statusCode = 302; redirect: {
res.end(); destination: "/",
return { permanent: false,
props: { }
user: null, };
},
};
} }
return { return {

View File

@@ -0,0 +1,92 @@
// updating specific user changes other users
// for example, updating the status of a corporate user should update the status of all users in the same corporate group
import { UserStatus, User } from "../interfaces/user";
import {
getFirestore,
collection,
getDocs,
getDoc,
doc,
setDoc,
query,
where,
} from "firebase/firestore";
import { app } from "@/firebase";
const db = getFirestore(app);
export const propagateStatusChange = (userId: string, status: UserStatus) =>
new Promise((resolve, reject) => {
getDoc(doc(db, "users", userId))
.then((docUser) => {
const user = docUser.data() as User;
// only update the status of the user's groups if the user is a corporate user
if (user.type === "corporate") {
getDocs(
query(collection(db, "groups"), where("admin", "==", userId))
).then(async (userGroupsRef) => {
const userGroups = userGroupsRef.docs.map((x) => x.data());
const targetUsers = [
...new Set(
userGroups.flatMap((g) => g.participants).filter((u) => u)
),
];
Promise.all(
targetUsers.map(async (targetUserId) => {
const ref = await getDoc(doc(db, "users", targetUserId));
if (!ref.exists()) return null;
const data = ref.data() as User;
return { ...data, id: targetUserId };
})
)
.then((data) => {
const filtered = data.filter((x) => {
if (x === null) return false;
if (x.status === status) return false;
if (x.type !== "student") return false;
return true;
}) as User[];
if (filtered.length === 0) {
return;
}
Promise.all(
filtered.map((user: User) =>
setDoc(
doc(db, "users", user.id),
{ status },
{ merge: true }
)
)
)
.then(() => {
resolve(true);
})
.catch((err) => {
console.error(err);
reject(err);
});
})
.catch((err) => {
console.error(err);
reject(err);
});
});
return;
}
const error = new Error("User is not a corporate user");
console.error(error);
reject(error);
})
.catch((err) => {
console.error(err);
reject(err);
});
});