diff --git a/README.md b/README.md
index a16e022..0839a34 100644
--- a/README.md
+++ b/README.md
@@ -10,30 +10,26 @@ It uses Vue 3, Firebase (for auth/storage), and Bootstrap.
The goal was to make a leaderboard app that complements the local-first nature of ActivityWatch with basic social features like public leaderboards or sharing specific screentime data privately with a group.
+Previous experiments/attempts were made in:
+ - [aw-leaderboard-rust](https://github.com/activitywatch/aw-leaderboard-rust).
+ - [aw-supabase](https://github.com/ActivityWatch/aw-supabase)
+
---
-The below was automatically generated on repo initialization.
+
+Click to expand the initial README
## Recommended IDE Setup
-[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
-
-## Type Support for `.vue` Imports in TS
-
-TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
-
-If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
-
-1. Disable the built-in TypeScript Extension
- 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
- 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
-2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
+[VSCode](https://code.visualstudio.com/) + [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
+
+
### Clone the Repo and its submodules
```sh
git clone --recurse-submodules https://github.com/ActivityWatch/aw-leaderboard-firebase
@@ -83,3 +79,5 @@ npm run test:e2e
```sh
npm run lint
```
+
+
diff --git a/functions/firestore.indexes.json b/functions/firestore.indexes.json
new file mode 100644
index 0000000..b0b8193
--- /dev/null
+++ b/functions/firestore.indexes.json
@@ -0,0 +1,19 @@
+{
+ "indexes": [
+ {
+ "collectionGroup": "O2TIwM6QoIhCBbcCCUggf0c7xMJ3",
+ "queryScope": "COLLECTION",
+ "fields": [
+ {
+ "fieldPath": "public",
+ "order": "ASCENDING"
+ },
+ {
+ "fieldPath": "date",
+ "order": "ASCENDING"
+ }
+ ]
+ }
+ ],
+ "fieldOverrides": []
+}
diff --git a/functions/firestore.rules b/functions/firestore.rules
new file mode 100644
index 0000000..d164847
--- /dev/null
+++ b/functions/firestore.rules
@@ -0,0 +1,16 @@
+rules_version = '2';
+
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /screentime/{userId}/{document=**} {
+ allow read, write: if request.auth.uid == userId;
+ }
+ match /leaderboard/{document=**} {
+ allow read: if true;
+ allow write: if false;
+ }
+ match /users/{userId}/{document=**} {
+ allow read, write: if request.auth.uid == userId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/functions/package-lock.json b/functions/package-lock.json
index e040f12..c92556d 100644
--- a/functions/package-lock.json
+++ b/functions/package-lock.json
@@ -7,7 +7,8 @@
"name": "functions",
"dependencies": {
"firebase-admin": "^11.8.0",
- "firebase-functions": "^4.3.1"
+ "firebase-functions": "^4.3.1",
+ "generate-api-key": "^1.0.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.12.0",
@@ -19,7 +20,7 @@
"typescript": "^4.9.0"
},
"engines": {
- "node": "18"
+ "node": "20"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2413,6 +2414,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
},
+ "node_modules/base-x": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
+ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2649,6 +2655,11 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chance": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.11.tgz",
+ "integrity": "sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA=="
+ },
"node_modules/char-regex": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -3999,6 +4010,28 @@
"node": ">=12"
}
},
+ "node_modules/generate-api-key": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/generate-api-key/-/generate-api-key-1.0.2.tgz",
+ "integrity": "sha512-4rPSpXyboIXfugOTN3/0Qaoqpzbk0sepzPS0XyxPh3UMuu+Trk+0JMyJ6mB/7FEgp7oZ1juqsRW+8wSYeKDbfA==",
+ "dependencies": {
+ "base-x": "^4.0.0",
+ "chance": "^1.1.8",
+ "rfc4648": "^1.5.2",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/generate-api-key/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6884,6 +6917,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rfc4648": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz",
+ "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ=="
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -9805,6 +9843,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
},
+ "base-x": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
+ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
+ },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -9964,6 +10007,11 @@
"supports-color": "^7.1.0"
}
},
+ "chance": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.11.tgz",
+ "integrity": "sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA=="
+ },
"char-regex": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -11020,6 +11068,24 @@
"json-bigint": "^1.0.0"
}
},
+ "generate-api-key": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/generate-api-key/-/generate-api-key-1.0.2.tgz",
+ "integrity": "sha512-4rPSpXyboIXfugOTN3/0Qaoqpzbk0sepzPS0XyxPh3UMuu+Trk+0JMyJ6mB/7FEgp7oZ1juqsRW+8wSYeKDbfA==",
+ "requires": {
+ "base-x": "^4.0.0",
+ "chance": "^1.1.8",
+ "rfc4648": "^1.5.2",
+ "uuid": "^8.3.2"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+ }
+ }
+ },
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -13177,6 +13243,11 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
+ "rfc4648": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz",
+ "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ=="
+ },
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
diff --git a/functions/package.json b/functions/package.json
index 87f9bee..41ba3ee 100644
--- a/functions/package.json
+++ b/functions/package.json
@@ -1,7 +1,7 @@
{
"name": "functions",
"scripts": {
- "lint": "eslint --ext .js,.ts .",
+ "lint": "eslint --fix --ext .js,.ts .",
"build": "tsc",
"build:watch": "tsc --watch",
"serve": "npm run build && firebase emulators:start --only functions",
@@ -16,7 +16,8 @@
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^11.8.0",
- "firebase-functions": "^4.3.1"
+ "firebase-functions": "^4.3.1",
+ "generate-api-key": "^1.0.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.12.0",
diff --git a/functions/src/index.ts b/functions/src/index.ts
index 9462d2c..a8199a3 100644
--- a/functions/src/index.ts
+++ b/functions/src/index.ts
@@ -1,19 +1,234 @@
-/**
- * Import function triggers from their respective submodules:
- *
- * import {onCall} from "firebase-functions/v2/https";
- * import {onDocumentWritten} from "firebase-functions/v2/firestore";
- *
- * See a full list of supported triggers at https://firebase.google.com/docs/functions
- */
-
import {onRequest} from "firebase-functions/v2/https";
-import * as logger from "firebase-functions/logger";
+import {onObjectFinalized} from "firebase-functions/v2/storage";
+import {onSchedule} from "firebase-functions/v2/scheduler";
+import * as admin from "firebase-admin";
+import * as functions from "firebase-functions";
+import * as genKey from "generate-api-key";
+import {info, error} from "firebase-functions/logger";
+import {RawEvent, defaultCategories, Event} from "./types";
+
+admin.initializeApp();
+
+exports.onUserCreated = functions.auth.user().onCreate((user) => {
+ info("User created: ", user.uid);
+ const db = admin.firestore();
+ const colpath = "users";
+ const docpath = user.uid;
+ const apiKey = genKey.generateApiKey();
+ const jsonObj = JSON.parse(`{"apiKey": "${apiKey}"}`);
+ return db.collection(colpath).doc(docpath).set(jsonObj);
+});
+
+exports.onUserDeleted = functions.auth.user().onDelete((user) => {
+ info("User deleted: ", user.uid);
+ const db = admin.firestore();
+ const colpath = "users";
+ const docpath = user.uid;
+ return db.collection(colpath).doc(docpath).delete();
+});
+
+export const UpdateLeaderboardData = onSchedule(
+ "every day 00:00", async (event) => {
+ // WIP
+ info("Updating leaderboard data");
+ const db = admin.firestore();
+ const destinationColpath = "leaderboard";
+ const screentimeColpath = "screentime/";
+ const screenTimeDocs = await db.collection(screentimeColpath)
+ .listDocuments();
+ const totals = new Map();
+ const promises = [];
+ for (const userDoc of screenTimeDocs) {
+ const sourceColpath = "screentime/" + userDoc.id + "/" + userDoc.id;
+ const screenTimeUserDocs = await db.collection(sourceColpath)
+ .listDocuments();
+ for (const screenTimeUserDoc of screenTimeUserDocs) {
+ const doc = await screenTimeUserDoc.get();
+ const events = doc.data()?.events as Event[];
+ // info("events: ", events);
+ totals.set("total", 0);
+ for (const event of events) {
+ const category = event.category.length > 0 ?
+ event.category[0] as string : "Other";
+ // info("category: ", category)";
+ totals.set(category, (totals.get(category) || 0) + event.duration);
+ totals.set("total", (totals.get("total") || 0) + event.duration);
+ }
+ // const total = totals.get("Total");
+ const userId = (await screenTimeUserDoc.get()).data()?.userId;
+ const date = (await screenTimeUserDoc.get()).data()?.date;
+ totals.delete("total");
+ for (const [category, total] of totals.entries()) {
+ const docpath = date;
+ const docref = db.collection(destinationColpath).doc(docpath);
+ const doc = await docref.get();
+ if (doc.exists) {
+ const categoryTotals = doc.data()
+ ?.CategoryTotals as { [key: string]: number };
+ const keys = Object.keys(categoryTotals);
+ if (keys.includes(category)) {
+ categoryTotals[category] += total;
+ } else {
+ categoryTotals[category] = total;
+ }
+ const promise = docref
+ .update({CategoryTotals: categoryTotals,
+ total: total + doc.data()?.total});
+ promises.push(promise);
+ } else {
+ const jsonObj = Object.fromEntries(totals);
+ const promise = docref
+ .set({CategoryTotals: jsonObj,
+ total: total, userId: userId, date: date});
+ promises.push(promise);
+ }
+ }
+ }
+ await Promise.all(promises);
+ }
+ }
+);
+export const getApiKey = functions.https.onCall(async (_, context) => {
+ /** A callable function only executed when the user is logged in */
+ info("Getting ApiKey");
+ info("Request data: ", context);
+ if (!context.auth) {
+ error("Not authenticated");
+ return {error: "Not authenticated"};
+ }
+ const db = admin.firestore();
+ const colpath = "users";
+ const docpath = context.auth?.uid;
+ const docref = db.collection(colpath).doc(docpath);
+ const doc = await docref.get();
+ if (doc.exists && doc.data() && doc.data()?.apiKey) {
+ info("ApiKey found and sent to client");
+ return {apiKey: doc.data()?.apiKey};
+ } else {
+ const apiKey = genKey.generateApiKey();
+ await docref.set({apiKey: apiKey});
+ info("ApiKey set and sent to client");
+ return {apiKey: apiKey};
+ }
+});
+export const rotateApiKey = functions.https.onCall(async (_, context) => {
+ /** A callable function only executed when the user is logged in */
+ info("Rotating ApiKey");
+ info("Request data: ", context);
+ if (!context.auth) {
+ error("Not authenticated");
+ return {error: "Not authenticated"};
+ }
+ const db = admin.firestore();
+ const colpath = "apiKeys";
+ const docpath = context.auth?.uid;
+ const docref = db.collection(colpath).doc(docpath);
+ const doc = await docref.get();
+ if (doc.exists && doc.data() && doc.data()?.apiKey) {
+ const apiKey = genKey.generateApiKey();
+ await docref.update({apiKey: apiKey});
+ info("ApiKey rotated and sent to client");
+ return {apiKey: apiKey};
+ } else {
+ await docref.set({apiKey: genKey.generateApiKey()});
+ info("ApiKey set and sent to client");
+ return {apiKey: doc.data()?.apiKey};
+ }
+});
+
+exports.uploadData = onRequest(async (request, response) => {
+ info("Storing data");
+ info("Request data: ", request.body);
+ if (!request.body.apiKey) {
+ error("No apiKey provided!");
+ response.status(400).send({error: "No apiKey provided!"});
+ return;
+ }
+ if (!request.body.data) {
+ error("No data provided to store!");
+ response.status(400).send({error: "No data provided!"});
+ return;
+ }
+
+ const apiKey = request.body.apiKey;
+ const db = admin.firestore();
+ const querySnapshot = await db.collection("users")
+ .where("apiKey", "==", apiKey)
+ .get();
+ info("QuerySnapshot: ", querySnapshot);
+ if (querySnapshot.empty) {
+ error("Invalid apiKey provided!");
+ response.status(403).send({error: "Invalid apiKey provided!"});
+ return;
+ }
+ const userId = querySnapshot.docs[0].id;
+ const bucket = admin.storage().bucket();
+ const dataToStore = request.body.data;
+
+ const filename = `${userId}/${Date.now()}.json`;
+ const file = bucket.file(filename);
+ await file.save(dataToStore);
+
+ response.send({message: "Data stored successfully!"});
+});
-// Start writing functions
-// https://firebase.google.com/docs/functions/typescript
+exports.onUploadData = onObjectFinalized(
+ {cpu: 4, memory: "2GiB"}, async (event) => {
+ info("Processing uploaded data");
+ const file = event.data;
+ const bucket = admin.storage().bucket();
+ const data = await bucket.file(file.name).download();
+ const userId = file.name.split("/")[0];
+ const dataString = data.toString();
+ const jsonData: [RawEvent] = JSON.parse(dataString);
+ const db = admin.firestore();
+ const batch = db.batch();
+ const colpath = `screentime/${userId}/${userId}`;
+ const promises = [];
+ const dateMap = new Map();
+ for (const rawEvent of jsonData) {
+ // reduce from type RawEvent to Event
+ let category = [] as string[];
+ for (const cat of defaultCategories) {
+ if (cat.rule.regex?.test(rawEvent.data?.app)) {
+ category = cat.name;
+ break;
+ }
+ }
+ const event:Event = {
+ timestamp: rawEvent.timestamp,
+ duration: rawEvent.duration,
+ data: rawEvent.data,
+ category: category,
+ };
-// export const helloWorld = onRequest((request, response) => {
-// logger.info("Hello logs!", {structuredData: true});
-// response.send("Hello from Firebase!");
-// });
+ let date = new Date(event.timestamp).toISOString().split("T")[0];
+ date = date.replace(/\//g, "-");
+ if (dateMap.has(date)) {
+ dateMap.get(date)?.push(event);
+ } else {
+ dateMap.set(date, [event]);
+ }
+ }
+ const dates = dateMap.entries();
+ for (const [date, events] of dates) {
+ const promise = db.collection(colpath).doc(date).get().then((doc) => {
+ if (doc.exists) {
+ const events = doc.data()?.events as Event[];
+ events.push(...events);
+ batch.update(doc.ref, {events: events});
+ } else {
+ batch.set(db.collection(colpath).doc(date), {
+ events: [...events],
+ userId: userId,
+ date: date,
+ public: true,
+ });
+ }
+ });
+ promises.push(promise);
+ }
+ await Promise.all(promises);
+ await batch.commit();
+ info("Data processed successfully");
+ });
diff --git a/functions/src/types.ts b/functions/src/types.ts
new file mode 100644
index 0000000..f4e3d68
--- /dev/null
+++ b/functions/src/types.ts
@@ -0,0 +1,121 @@
+export interface Event {
+ timestamp: string;
+ duration: number;
+ data: any;
+ category: string[];
+}
+export interface RawEvent {
+ id: number;
+ timestamp: string;
+ duration: number;
+ data: any;
+}
+// Screentime is stored in day-level objects that have
+// their `events` appended when new data is added.
+export interface ScreenTimeData {
+ userId: string;
+ public: boolean;
+ date: string;
+ events: Event[];
+}
+
+// Same as above, but with events aggregated
+export interface ScreenTimeSummary {
+ userId: string;
+ total: number;
+ date: string;
+ categoryTotals: { [key: string]: number };
+}
+
+export interface ChartDataset {
+ label: string;
+ data: number[];
+ backgroundColor: string;
+}
+
+export interface ChartData {
+ labels: string[];
+ datasets: ChartDataset[];
+}
+
+export interface Rule {
+ type: "regex" | "none";
+ regex?: RegExp
+ ignore_case?: boolean;
+}
+
+export interface Category {
+ id?: number;
+ name: string[];
+ name_pretty?: string;
+ subname?: string;
+ rule: Rule;
+ data?: Record;
+ depth?: number;
+ parent?: string[];
+ children?: Category[];
+}
+
+export const defaultCategories: Category[] = [
+ {
+ name: ["Work"],
+ rule: {type: "regex", regex: /Google Docs|libreoffice|ReText/gi},
+ },
+ {
+ name: ["Work", "Programming"],
+ rule: {
+ type: "regex",
+ // eslint-disable-next-line max-len
+ regex: /GitHub|Stack Overflow|BitBucket|Gitlab|vim|Spyder|kate|Ghidra|Scite/gi,
+ },
+ },
+ {
+ name: ["Work", "Programming", "ActivityWatch"],
+ rule: {type: "regex", regex: /ActivityWatch|aw-/gi, ignore_case: true},
+ },
+ {name: ["Work", "Image"], rule: {type: "regex", regex: /GIMP|Inkscape/gi}},
+ {name: ["Work", "Video"], rule: {type: "regex", regex: /Kdenlive/gi}},
+ {name: ["Work", "Audio"], rule: {type: "regex", regex: /Audacity/gi}},
+ {name: ["Work", "3D"], rule: {type: "regex", regex: /Blender/gi}},
+ {
+ name: ["Media", "Games"],
+ rule: {type: "regex", regex: /Minecraft|RimWorld/gi},
+ },
+ {
+ name: ["Media", "Video"],
+ rule: {type: "regex", regex: /YouTube|Plex|VLC/gi},
+ },
+ {
+ name: ["Media", "Social Media"],
+ rule: {
+ type: "regex",
+ regex: /reddit|Facebook|Twitter|Instagram|devRant/gi,
+ ignore_case: true,
+ },
+ },
+ {
+ name: ["Media", "Music"],
+ rule: {
+ type: "regex",
+ regex: /Spotify|Deezer/gi,
+ ignore_case: true,
+ },
+ },
+ {
+ name: ["Comms"],
+ rule: {
+ type: "regex",
+ regex: /Slack|Riot|Element|Discord|Nheko|NeoChat|Mattermost/gi,
+ },
+ },
+ {
+ name: ["Comms", "IM"],
+ rule: {
+ type: "regex",
+ // eslint-disable-next-line max-len
+ regex: /Messenger|Telegram|Signal|WhatsApp|Rambox|Slack|Riot|Element|Discord|Nheko|NeoChat|Mattermost/gi,
+ },
+ },
+ // eslint-disable-next-line max-len
+ {name: ["Comms", "Email"], rule: {type: "regex", regex: /Gmail|Thunderbird|mutt|alpine/gi}},
+];
diff --git a/functions/storage.rules b/functions/storage.rules
new file mode 100644
index 0000000..0fd93d4
--- /dev/null
+++ b/functions/storage.rules
@@ -0,0 +1,6 @@
+rules_version = '2';
+service cloud.storage {
+ match /{userId}/{allPaths=**} {
+ allow read, write: if request.auth.uid == userId;
+ }
+}
diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue
index 95217c2..a52d48d 100644
--- a/src/components/Dashboard.vue
+++ b/src/components/Dashboard.vue
@@ -23,7 +23,6 @@ fetchSummary()
const summaries = summary
if (!isAuthenticated) {
- logout()
router.push({ name: 'Login' })
}
diff --git a/src/components/Leaderboard.vue b/src/components/Leaderboard.vue
index 7a3ff12..f5007a0 100644
--- a/src/components/Leaderboard.vue
+++ b/src/components/Leaderboard.vue
@@ -24,12 +24,17 @@ import { ref, onMounted } from 'vue'
import { useLeaderboardStore } from '@/stores/leaderboard'
import { type ScreenTimeSummary } from '@/types'
import AWLHeader from '@/components/Header.vue'
+import { useAuthStore } from '@/stores/auth'
+import router from '@/router'
export default {
name: 'AWLLeaderboard',
setup() {
const entries = ref([] as ScreenTimeSummary[] | null)
-
+ const { isAuthenticated } = useAuthStore()
+ if (!isAuthenticated) {
+ router.push({ name: 'Login' })
+ }
onMounted(async () => {
try {
const { fetchLeaderboardData, leaderboardData} = useLeaderboardStore()
diff --git a/src/firebase/data.ts b/src/firebase/data.ts
index 4702d7c..0e52c78 100644
--- a/src/firebase/data.ts
+++ b/src/firebase/data.ts
@@ -50,10 +50,14 @@ export async function addScreenTimeData(userId: number, data: ScreenTimeData) {
}
}
-export async function getScreenTimeData(userId: string): Promise {
- const colPath = `screentime/${userId}/${userId}`
- const colRef = collection(db, colPath)
- const snapshot = await getDocs(colRef)
+export async function getScreenTimeData(userId: string, since: Date | null = null, _public: Boolean = true): Promise {
+ const q = query(
+ collection(db, 'screentime/' + userId + '/' + userId),
+ // where('date', '>=', since || new Date('1900-1-1')),
+ where('public', '==', _public)
+ )
+
+ const snapshot = await getDocs(q)
if (snapshot.empty) {
return null
}
diff --git a/src/firebase/firebase.rules b/src/firebase/firebase.rules
deleted file mode 100644
index e483f81..0000000
--- a/src/firebase/firebase.rules
+++ /dev/null
@@ -1,19 +0,0 @@
-rules_version = '2';
-
-service cloud.firestore {
- match /databases/{database}/documents {
-
- // This rule allows anyone with your Firestore database reference to view, edit,
- // and delete all data in your Firestore database. It is useful for getting
- // started, but it is configured to expire after 30 days because it
- // leaves your app open to attackers. At that time, all client
- // requests to your Firestore database will be denied.
- //
- // Make sure to write security rules for your app before that time, or else
- // all client requests to your Firestore database will be denied until you Update
- // your rules
- match /{document=**} {
- allow read, write: if request.time < timestamp.date(2023, 8, 4);
- }
- }
-}
\ No newline at end of file