From e43eec38f89cedb796cea8b00016968b792d86f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kan=C4=9Bra?= Date: Wed, 11 Nov 2020 00:15:32 +0100 Subject: [PATCH 01/26] feat: rework --- components/admin-project/admin-project.sass | 0 components/admin-project/admin-project.vue | 38 +++++++++ components/admin-stats/admin-stats.vue | 8 +- components/modal/modal.vue | 5 ++ components/proposal-form/proposal-form.vue | 16 ++-- .../student-proposal/student-proposal.vue | 4 +- .../teacher-project/teacher-project.vue | 4 +- firebase/functions/functions.rules | 4 +- keywords.js | 59 +++++++++++++ middleware/student.ts | 2 +- pages/index.vue | 85 +++++++++++++++++-- pages/students.vue | 24 +++--- server/api/index.ts | 1 + server/api/proposals/accept.ts | 8 +- server/api/user/create-user.ts | 5 +- server/api/user/update-year.ts | 29 +++++++ store/index.ts | 5 +- 17 files changed, 252 insertions(+), 45 deletions(-) create mode 100644 components/admin-project/admin-project.sass create mode 100644 components/admin-project/admin-project.vue create mode 100644 keywords.js create mode 100644 server/api/user/update-year.ts diff --git a/components/admin-project/admin-project.sass b/components/admin-project/admin-project.sass new file mode 100644 index 00000000..e69de29b diff --git a/components/admin-project/admin-project.vue b/components/admin-project/admin-project.vue new file mode 100644 index 00000000..261cfba0 --- /dev/null +++ b/components/admin-project/admin-project.vue @@ -0,0 +1,38 @@ + + + diff --git a/components/admin-stats/admin-stats.vue b/components/admin-stats/admin-stats.vue index 52063402..045d51c0 100644 --- a/components/admin-stats/admin-stats.vue +++ b/components/admin-stats/admin-stats.vue @@ -1,7 +1,9 @@ @@ -19,6 +21,10 @@ export default defineComponent({ type: Number, default: 0, }, + loading: { + type: Boolean, + default: true, + }, }, }); diff --git a/components/modal/modal.vue b/components/modal/modal.vue index 6ccad796..296d7db0 100644 --- a/components/modal/modal.vue +++ b/components/modal/modal.vue @@ -14,6 +14,10 @@ export default defineComponent({ type: Boolean, default: false, }, + disabled: { + type: Boolean, + default: false, + }, }, setup(props, { emit }) { const display = ref(false); @@ -32,6 +36,7 @@ export default defineComponent({ }); const closeModal = () => { + if (props.disabled) return; display.value = false; emit('input', display.value); }; diff --git a/components/proposal-form/proposal-form.vue b/components/proposal-form/proposal-form.vue index 64d233c6..704e7765 100644 --- a/components/proposal-form/proposal-form.vue +++ b/components/proposal-form/proposal-form.vue @@ -6,7 +6,7 @@ ps-select.w-full.mb-5(v-model='selectedTeacherId', placeholder='Učitel', :options='teachers', :loading='teachersLoading') span.self-start.mb-1.text-ps-green.text-sm Projekty ps-select.w-full.mb-10(v-model='selectedProjectId', placeholder='Projekt', :options='projects', :loading='projectsLoading') - ps-text-field.w-full.mb-8(v-model='studentsProjectName', v-if='displayTextField', type='text', label='Tvé vlastní téma', name='theme') + ps-text-field.w-full.mb-8(v-model='studentsProjectTitle', v-if='displayTextField', type='text', label='Tvé vlastní téma', name='theme') ps-btn(:disabled='submitted || loadingBtn || disabledBtn', :loading='loadingBtn', @click='submitProposal') Odeslat ps-snackbar(:display='displaySnack', :delay='9000') Zadání odesláno, počkej na schválení učitelem @@ -78,7 +78,7 @@ export default defineComponent({ .onSnapshot((snapshot) => { projects.value = snapshot.docs.map((projectDoc) => { return { - placeholder: projectDoc.data().name, + placeholder: projectDoc.data().title, value: projectDoc.id, }; }); @@ -93,7 +93,7 @@ export default defineComponent({ const disabledBtn = ref(true); const displaySnack = ref(false); - const studentsProjectName = ref(''); + const studentsProjectTitle = ref(''); const displayTextField = ref(false); watch(selectedProjectId, (selectedProjectId) => { @@ -101,9 +101,9 @@ export default defineComponent({ disabledBtn.value = selectedProjectId === '' || displayTextField.value; }); - watch(studentsProjectName, (_) => { - if (selectedProjectId.value === 'studentTheme') disabledBtn.value = !(studentsProjectName.value.length > 0); - else studentsProjectName.value = ''; + watch(studentsProjectTitle, (_) => { + if (selectedProjectId.value === 'studentTheme') disabledBtn.value = !(studentsProjectTitle.value.length > 0); + else studentsProjectTitle.value = ''; }); const submitProposal = async () => { @@ -117,7 +117,7 @@ export default defineComponent({ console.log(proposal); - if (studentsProjectName.value !== '') proposal = { ...proposal, ...{ name: studentsProjectName.value } }; + if (studentsProjectTitle.value !== '') proposal = { ...proposal, ...{ title: studentsProjectTitle.value } }; const collectionRef = firebase.firestore().collection('proposals'); const docRef = selectedProjectId.value === 'studentTheme' ? collectionRef.doc() : collectionRef.doc(selectedProjectId.value); @@ -150,7 +150,7 @@ export default defineComponent({ submitProposal, disabledBtn, displaySnack, - studentsProjectName, + studentsProjectTitle, displayTextField, }; }, diff --git a/components/student-proposal/student-proposal.vue b/components/student-proposal/student-proposal.vue index 9e946a94..240cb4e5 100644 --- a/components/student-proposal/student-proposal.vue +++ b/components/student-proposal/student-proposal.vue @@ -4,7 +4,7 @@ img.border-2.border-solid.border-ps-green.rounded-full(:src='profilePicture', width='48') .ml-2 span.text-ps-green.font-bold.block {{ displayName }} - span.text-ps-white {{ projectName }} + span.text-ps-white {{ ptojectTitle }} .flex.justify-between.mt-5 ps-btn.text-ps-white.text-sm(error, @click='declineProposal', :loading='declineLoading', :disabled='declineLoading || acceptLoading') Zamítnout template(#icon-left) @@ -41,7 +41,7 @@ export default defineComponent({ default: '', required: true, }, - projectName: { + ptojectTitle: { type: String, default: '', required: true, diff --git a/components/teacher-project/teacher-project.vue b/components/teacher-project/teacher-project.vue index 2cbdebf9..373a4370 100644 --- a/components/teacher-project/teacher-project.vue +++ b/components/teacher-project/teacher-project.vue @@ -4,7 +4,7 @@ img.border-2.border-solid.border-ps-green.rounded-full(:src='profilePicture', width='48') .ml-2 span.text-ps-green.font-bold.block {{ displayName }} - span.text-ps-white {{ projectName }} + span.text-ps-white {{ ptojectTitle }} ps-btn.text-ps-white(text) odevzdat posudek template(#icon-right) arrow-right.text-ps-white(:size='32') @@ -27,7 +27,7 @@ export default defineComponent({ default: '', required: true, }, - projectName: { + ptojectTitle: { type: String, default: '', required: true, diff --git a/firebase/functions/functions.rules b/firebase/functions/functions.rules index df11e271..505e1df9 100644 --- a/firebase/functions/functions.rules +++ b/firebase/functions/functions.rules @@ -15,12 +15,12 @@ function isAdmin(request) { function validProposalMod(request) { return request.resource.data.studentId != null && - request.resource.data.name is string && + request.resource.data.title is string && request.resource.data.teacherId != null; } function validProjectMod(request) { return request.resource.data.studentId != null && - request.resource.data.name is string && + request.resource.data.title is string && request.resource.data.teacherId != null; } \ No newline at end of file diff --git a/keywords.js b/keywords.js new file mode 100644 index 00000000..d18ce4d9 --- /dev/null +++ b/keywords.js @@ -0,0 +1,59 @@ +const projectName = 'Úložiště maturitních projektů'; +const displayName = 'Martin Kaněra'; + +const fs = require('fs'); + +const charLimit = 10; + +const generateKeywords = (string) => { + const resultArr = []; + let currentString = ''; + + string.split('').forEach((letter) => { + currentString += letter; + resultArr.push(currentString); + }); + + // for (let i = 0; i < charLimit; i++) { + // currentString += string[i]; + // resultArr.push(currentString); + // } + + return resultArr; +}; + +function permute(permutation) { + const length = permutation.length; + const result = [permutation.slice()]; + const c = new Array(length).fill(0); + let i = 1; + let k; + let p; + + while (i < length) { + if (c[i] < i) { + k = i % 2 && c[i]; + p = permutation[i]; + permutation[i] = permutation[k]; + permutation[k] = p; + ++c[i]; + i = 1; + result.push(permutation.slice()); + } else { + c[i] = 0; + ++i; + } + } + return result; +} + +let finalProduct = []; +const names = `${displayName} ${projectName}`.split(' '); + +const cases = permute(names); +cases.forEach((nameCase) => { + finalProduct = [...finalProduct, ...generateKeywords(nameCase.join(' '))]; +}); + +// fs.writeFileSync('keywords.text', finalProduct); +console.log(finalProduct); diff --git a/middleware/student.ts b/middleware/student.ts index da3a730c..0f184415 100644 --- a/middleware/student.ts +++ b/middleware/student.ts @@ -4,7 +4,7 @@ import { useMainStore } from '@/store'; const studentMiddleware: Middleware = (ctx) => { const mainStore = useMainStore(); - if (!mainStore.isLoggedIn.value || !mainStore.isStudent.value) { + if (!mainStore.isLoggedIn.value || !mainStore.isStudent.value || !mainStore.state.user.currentYear) { ctx.redirect('/'); } }; diff --git a/pages/index.vue b/pages/index.vue index 9eb0e665..b9fecf5f 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,18 +1,85 @@ diff --git a/pages/students.vue b/pages/students.vue index 3e68dc29..4c93ce8f 100644 --- a/pages/students.vue +++ b/pages/students.vue @@ -8,7 +8,7 @@ :key='proposal.studentId', :studentId='proposal.studentId', :displayName='proposal.displayName', - :projectName='proposal.projectName', + :ptojectTitle='proposal.ptojectTitle', :profilePicture='proposal.profilePicture', :proposalRef='proposal.proposalRef' ) @@ -19,14 +19,14 @@ plus-icon/ ps-modal(v-model='projectModalDisplay') span.text-2xl.text-ps-white.font-medium Přidat zadání projektu - ps-text-field.my-8(name='project-name', label='Název projektu', v-model='projectName') + ps-text-field.my-8(name='project-name', label='Název projektu', v-model='ptojectTitle') ps-btn.ml-auto(@click='addProject', :disabled='submitting || disabledBtn', :loading='submitting') Přidat projekt .flex.flex-col.mt-4.flex-wrap.justify-between(class='lg:flex-row') ps-teacher-project( v-for='project in projects', :key='project.projectId', :projectId='project.projectId', - :projectName='project.projectName', + :ptojectTitle='project.ptojectTitle', :displayName='project.displayName', :profilePicture='project.profilePicture' ) @@ -44,14 +44,14 @@ import plusIcon from 'vue-material-design-icons/Plus.vue'; type StudentProposal = { studentId: String; displayName: String; - projectName: String; + ptojectTitle: String; profilePicture: String; proposalRef: firebase.firestore.DocumentReference; }; type Project = { projectId: String; - projectName: String; + ptojectTitle: String; displayName: String; profilePicture: String; }; @@ -95,7 +95,7 @@ export default defineComponent({ return { studentId: studentDoc.id, displayName: studentDoc.data().displayName, - projectName: proposalsSnap.docs.find((proposalDoc) => proposalDoc.data().studentId === studentDoc.id)?.data().name, + ptojectTitle: proposalsSnap.docs.find((proposalDoc) => proposalDoc.data().studentId === studentDoc.id)?.data().title, profilePicture: studentDoc.data().profilePicture, proposalRef: proposalsRefs.find((proposalRef) => proposalRef.studentId === studentDoc.id)!.ref, }; @@ -107,8 +107,8 @@ export default defineComponent({ firebase .firestore() .collection('projects') + .where('currentYear', '>=', new firebase.firestore.Timestamp(new Date().getSeconds(), 0)) .where('teacherId', '==', mainStore.state.user.id) - .where('year', '==', new Date().getFullYear()) .onSnapshot(async (projectSnap) => { const studentIds = projectSnap.docs.map((projectDoc) => projectDoc.data().studentId); @@ -124,7 +124,7 @@ export default defineComponent({ return { projectId: projectDoc.id, - projectName: projectDoc.data().name, + ptojectTitle: projectDoc.data().title, displayName: currentStudent?.data().displayName, profilePicture: currentStudent?.data().profilePicture, }; @@ -165,13 +165,13 @@ export default defineComponent({ projectModalDisplay.value = !projectModalDisplay.value; }; - const projectName = ref(''); + const ptojectTitle = ref(''); const submitting = ref(false); const disabledBtn = ref(true); watchEffect(() => { - disabledBtn.value = projectName.value === ''; + disabledBtn.value = ptojectTitle.value === ''; }); const addProject = async () => { @@ -181,7 +181,7 @@ export default defineComponent({ try { await docRef.set({ premade: true, - name: projectName.value, + title: ptojectTitle.value, teacherId: mainStore.state.user.id, studentId: null, }); @@ -198,7 +198,7 @@ export default defineComponent({ projects, projectModal, projectModalDisplay, - projectName, + ptojectTitle, addProject, submitting, disabledBtn, diff --git a/server/api/index.ts b/server/api/index.ts index aadbba8d..e89f9beb 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -16,5 +16,6 @@ if (!admin.apps.length) app.post('/user/create', async (req, res) => (await import('./user/create-user')).default(req, res)); app.get('/teachers/list', async (req, res) => (await import('./teachers/list')).default(req, res)); app.put('/proposal/accept', async (req, res) => (await import('./proposals/accept')).default(req, res)); +app.put('/user/year', async (req, res) => (await import('./user/update-year')).default(req, res)); export default app; diff --git a/server/api/proposals/accept.ts b/server/api/proposals/accept.ts index 2ea4640b..20724268 100644 --- a/server/api/proposals/accept.ts +++ b/server/api/proposals/accept.ts @@ -27,11 +27,15 @@ export default async (req: Request, res: Response) => { const projectRef = admin.firestore().collection('projects').doc(); + // TODO probbably add files and links array + get default deadline from system collection (set by admin) + transaction.set(projectRef, { - name: sfDoc.data()?.name, + title: sfDoc.data()?.title, + description: '', studentId: sfDoc.data()?.studentId, teacherId: sfDoc.data()?.teacherId, - year: new Date().getFullYear(), + opponentId: '', + currentYear: sfDoc.data()?.currentYear, }); transaction.delete(proposalRef); diff --git a/server/api/user/create-user.ts b/server/api/user/create-user.ts index dd3f0c87..cc863520 100644 --- a/server/api/user/create-user.ts +++ b/server/api/user/create-user.ts @@ -9,7 +9,6 @@ export default async (req: Request, res: Response) => { const idToken = req.headers.authorization?.split(' ')[1] ?? ''; // TODO implement req.body premade teachor account - // TODO project doc const usersCollection = admin.firestore().collection('users'); @@ -39,7 +38,6 @@ export default async (req: Request, res: Response) => { return `https://storage.googleapis.com/${bucketName}/${fileName}`; } catch (_) { - // TODO take 'anonymous' profile picture return 'https://storage.googleapis.com/ps-profile-pictures/empty.png'; } }; @@ -56,8 +54,7 @@ export default async (req: Request, res: Response) => { admin: false, student: userData.email?.includes('delta-studenti'), teacher: userData.email?.includes('delta-skola'), - verified: false, - year: 0, + currentYear: null, }; await usersCollection.doc(userData.uid).set(newUserDoc); diff --git a/server/api/user/update-year.ts b/server/api/user/update-year.ts new file mode 100644 index 00000000..36b9710d --- /dev/null +++ b/server/api/user/update-year.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express'; +import admin from 'firebase-admin'; + +export default async (req: Request, res: Response) => { + const idToken = req.headers.authorization?.split(' ')[1] ?? ''; + + try { + await admin.auth().getUser(idToken); + } catch (_) { + return res.status(401).send('Unauthorized'); + } + + const yearTolerance = 4; + const year = req.body?.year; + + if (!year && year <= year + yearTolerance) return res.status(400).send('Missing parameters'); + + const currentYearTimestamp = admin.firestore.Timestamp.fromDate(new Date(year, 4, 25)); + + try { + await admin.firestore().collection('users').doc(idToken).update({ + currentYear: currentYearTimestamp, + }); + + return res.status(200).send(currentYearTimestamp); + } catch (_) {} + + return res.status(500).send(); +}; diff --git a/store/index.ts b/store/index.ts index 81ef210b..443c899a 100644 --- a/store/index.ts +++ b/store/index.ts @@ -1,4 +1,5 @@ import { createStore } from 'pinia'; +import firebase from 'firebase/app'; type State = { user: { @@ -9,7 +10,7 @@ type State = { student: Boolean; teacher: Boolean; admin: Boolean; - year: Number; + currentYear: firebase.firestore.Timestamp | null; }; project: { id: String; @@ -27,7 +28,7 @@ export const useMainStore = createStore({ student: false, teacher: false, admin: false, - year: 0, + currentYear: null, }, project: { id: '', From 8e34013010e2547996ed232a253a8dc2cf32cfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kan=C4=9Bra?= Date: Wed, 11 Nov 2020 00:47:17 +0100 Subject: [PATCH 02/26] feat(api): increased security --- middleware/proposal.ts | 10 ++++++++++ pages/proposal.vue | 2 +- server/api/proposals/accept.ts | 4 ++-- server/api/teachers/list.ts | 17 +++++++++++++---- 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 middleware/proposal.ts diff --git a/middleware/proposal.ts b/middleware/proposal.ts new file mode 100644 index 00000000..af8616fd --- /dev/null +++ b/middleware/proposal.ts @@ -0,0 +1,10 @@ +import { Middleware } from '@nuxt/types'; +import { useMainStore } from '@/store'; + +const proposalMiddleware: Middleware = ({ redirect }) => { + const mainStore = useMainStore(); + + if (mainStore.projectId.value) return redirect('/myproject'); +}; + +export default proposalMiddleware; diff --git a/pages/proposal.vue b/pages/proposal.vue index 2e31d9a6..2c591ffa 100644 --- a/pages/proposal.vue +++ b/pages/proposal.vue @@ -7,6 +7,6 @@ import { defineComponent } from 'nuxt-composition-api'; export default defineComponent({ - middleware: 'student', + middleware: ['student', 'proposal'], }); diff --git a/server/api/proposals/accept.ts b/server/api/proposals/accept.ts index 20724268..8db0d322 100644 --- a/server/api/proposals/accept.ts +++ b/server/api/proposals/accept.ts @@ -35,13 +35,13 @@ export default async (req: Request, res: Response) => { studentId: sfDoc.data()?.studentId, teacherId: sfDoc.data()?.teacherId, opponentId: '', - currentYear: sfDoc.data()?.currentYear, + currentYear: userData?.currentYear, }); transaction.delete(proposalRef); return transaction; - } catch (e) { + } catch (_) { return res.status(500).send(); } }); diff --git a/server/api/teachers/list.ts b/server/api/teachers/list.ts index 4205c1b3..07fdc826 100644 --- a/server/api/teachers/list.ts +++ b/server/api/teachers/list.ts @@ -8,14 +8,23 @@ export default async (req: Request, res: Response) => { try { await admin.auth().getUser(userId); - - if ((await admin.firestore().collection('proposals').where('studentId', '==', userId).get()).docs.length === 1) { - return res.status(202).send({ message: 'Proposal already submitted', status: 202 }); - } } catch (e) { return res.status(401).send(); } + if ((await admin.firestore().collection('proposals').where('studentId', '==', userId).get()).docs[0].exists) + return res.status(202).send({ message: 'Proposal already submitted', status: 202 }); + + if ((await admin.firestore().collection('projects').where('studentId', '==', userId).get()).docs[0].exists) return res.status(202).send({ message: 'You already have project', status: 202 }); + + const userData = (await admin.firestore().collection('users').doc(userId).get()).data(); + + // User doesnt have current year set + if (!userData?.currentYear) return res.status(400).send(); + + // Teacher cant submit proposal + if (userData?.teacher) return res.status(403).send('Teacher cannot submit proposal'); + const teachersData = (await admin.firestore().collection('users').where('teacher', '==', true).get()).docs; const teachersList = teachersData.map((teacherDoc) => { From eaa806174c88d0ddc9db2279edeb6a205f2262ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kan=C4=9Bra?= Date: Wed, 11 Nov 2020 01:02:06 +0100 Subject: [PATCH 03/26] feat(admin-stats): user-select --- components/admin-stats/admin-stats.sass | 5 +++++ components/admin-stats/admin-stats.vue | 2 +- pages/admin.vue | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/components/admin-stats/admin-stats.sass b/components/admin-stats/admin-stats.sass index 60107b2b..e3ebc665 100644 --- a/components/admin-stats/admin-stats.sass +++ b/components/admin-stats/admin-stats.sass @@ -5,5 +5,10 @@ @screen lg flex: 0 1 32% + .spinner + -webkit-user-select: none + -ms-user-select: none + user-select: none + .max @apply text-ps-green text-2xl \ No newline at end of file diff --git a/components/admin-stats/admin-stats.vue b/components/admin-stats/admin-stats.vue index 045d51c0..192563dd 100644 --- a/components/admin-stats/admin-stats.vue +++ b/components/admin-stats/admin-stats.vue @@ -1,7 +1,7 @@