Skip to content

Commit

Permalink
Merge pull request #150 from r00tat/feature/groups
Browse files Browse the repository at this point in the history
Add Feature groups
  • Loading branch information
r00tat authored Sep 28, 2024
2 parents 885f338 + 500f91d commit 0cdd06b
Show file tree
Hide file tree
Showing 50 changed files with 1,194 additions and 182 deletions.
4 changes: 2 additions & 2 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
{
"database": "ffndev",
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
"rules": "firestore.dev.rules",
"indexes": "firestore.indexes.dev.json"
}
]
}
63 changes: 63 additions & 0 deletions firestore.dev.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {

function authorizedUser() {
return request.auth != null && (
request.auth.token.email.matches(".*@ff-neusiedlamsee.at") ||
get(/databases/$(database)/documents/user/$(request.auth.uid)).data.authorized in [true, 'on', 'yes', 'y']
);
}

function adminUser() {
return request.auth != null &&
get(/databases/$(database)/documents/user/$(request.auth.uid)).data.isAdmin == true;
}

match /hydrant/{doc=*} {
allow read: if authorizedUser()
}
match /saugstelle/{doc=*} {
allow read: if authorizedUser()
}
match /loeschteich/{doc=*} {
allow read: if authorizedUser()
}
match /risikoobjekt/{doc=*} {
allow read: if authorizedUser()
}

match /gefahrobjekt/{doc=*} {
allow read: if authorizedUser()
}

match /call/{doc=**} {
allow read, write: if authorizedUser()
&& resource.data.group in get(/databases/$(database)/documents/user/$(request.auth.uid)).data.groups
}

match /clusters/{doc=**} {
allow read: if authorizedUser();
}

match /clusters6/{doc=**} {
allow read: if authorizedUser();
allow write: if adminUser();
}

match /tokens/{doc=**} {
allow read: if authorizedUser() && request.auth.uid == resource.data.owner;
allow delete: if authorizedUser() && request.auth.uid == resource.data.owner;
allow write: if authorizedUser() && request.auth.uid == request.resource.data.owner;
}

match /user/{doc=*} {
allow read: if request.auth != null && request.auth.uid == doc
}

match /{document=**} {
allow read, write: if adminUser();
}

}
}
51 changes: 51 additions & 0 deletions firestore.indexes.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"indexes": [
{
"collectionGroup": "call",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "deleted",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "call",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "group",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "call",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "deleted",
"order": "ASCENDING"
},
{
"fieldPath": "group",
"order": "ASCENDING"
},
{
"fieldPath": "date",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": []
}
36 changes: 20 additions & 16 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,44 @@ service cloud.firestore {
match /databases/{database}/documents {

function authorizedUser() {
return request.auth != null && (

request.auth.token.email.matches(".*@ff-neusiedlamsee.at") ||
get(/databases/$(database)/documents/user/$(request.auth.uid)).data.authorized in [true, 'on', 'yes', 'y']
return request.auth != null && (
request.auth.token.email.matches(".*@ff-neusiedlamsee.at") ||
get(/databases/$(database)/documents/user/$(request.auth.uid)).data.authorized in [true, 'on', 'yes', 'y']
);
}

function adminUser() {
return request.auth != null && request.auth.token.email == "paul.woelfel@ff-neusiedlamsee.at";
return request.auth != null &&
get(/databases/$(database)/documents/user/$(request.auth.uid)).data.isAdmin == true;
}

match /hydrant/{doc=*} {
allow read
match /hydrant/{doc=*} {
allow read: if authorizedUser()
}
match /saugstelle/{doc=*} {
allow read
allow read: if authorizedUser()
}
match /loeschteich/{doc=*} {
allow read
allow read: if authorizedUser()
}
match /risikoobjekt/{doc=*} {
allow read: if authorizedUser()
allow read: if authorizedUser()
}

match /gefahrobjekt/{doc=*} {
allow read: if authorizedUser()
allow read: if authorizedUser()
}

match /call/{doc=**} {
allow read, write: if authorizedUser()
allow read, write: if authorizedUser()
}

match /clusters/{doc=**} {
allow read: if authorizedUser();
allow read: if authorizedUser();
}

match /clusters6/{doc=**} {
allow read: if authorizedUser();
allow read: if authorizedUser();
}

match /tokens/{doc=**} {
Expand All @@ -50,8 +50,12 @@ service cloud.firestore {
}


match /user/{doc=*} {
allow read: if request.auth != null && request.auth.uid == doc
match /user/{doc=*} {
allow read: if request.auth != null && request.auth.uid == doc
}
match /clusters6/{doc=**} {
allow read: if authorizedUser();
allow write: if adminUser();
}

match /{document=**} {
Expand Down
54 changes: 54 additions & 0 deletions src/app/admin/adminActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use server';

import { UserRecordExtended } from '../../common/users';
import {
Firecall,
FIRECALL_COLLECTION_ID,
USER_COLLECTION_ID,
} from '../../components/firebase/firestore';
import { firestore } from '../../server/firebase/admin';
import { actionAdminRequired } from '../auth';

export async function setAuthorizedToBool(): Promise<UserRecordExtended[]> {
await actionAdminRequired();
const badUsers = await firestore
.collection(USER_COLLECTION_ID)
.where('authorized', '==', 'on')
.get();
await Promise.all(
badUsers.docs.map(async (user) =>
firestore
.collection(USER_COLLECTION_ID)
.doc(user.id)
.update({ authorized: true })
)
);
return badUsers.docs.map(
(user) =>
({ ...user.data(), uid: user.id } as unknown as UserRecordExtended)
);
}

export async function setEmptyFirecallGroup() {
await actionAdminRequired();
const calls = (
await firestore.collection(FIRECALL_COLLECTION_ID).get()
).docs.filter((call) => call.data().group === undefined);

await Promise.all(
calls.map((call) =>
firestore
.collection(FIRECALL_COLLECTION_ID)
.doc(call.id)
.update({ group: 'ffnd' })
)
);

return calls.map(
(call) =>
({
...call.data(),
id: call.id,
} as unknown as Firecall)
);
}
46 changes: 46 additions & 0 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { useCallback, useState } from 'react';
import { setAuthorizedToBool, setEmptyFirecallGroup } from './adminActions';
import Box from '@mui/material/Box';

export default function AdminPage() {
const [status, setStatus] = useState('');

const updateAuthorized = useCallback(async () => {
const fixedUsers = await setAuthorizedToBool();
setStatus(
`${fixedUsers.length} users corrected. (${fixedUsers
.map((user) => user.email)
.join(', ')})`
);
}, []);
const setFirecallGroup = useCallback(async () => {
const calls = await setEmptyFirecallGroup();
setStatus(
`${calls.length} calls corrected. (${calls
.map((call) => call.name)
.join(', ')})`
);
}, []);

return (
<Box margin={2}>
<Typography variant="h3">Admin Actions</Typography>
<Typography>{status}</Typography>
<Typography>
Set authorized from on to true{' '}
<Button onClick={updateAuthorized} variant="contained">
Fix users authorized
</Button>
</Typography>
<Typography>
Set ffnd as default group on firecalls{' '}
<Button onClick={setFirecallGroup} variant="contained">
Fix empty firecall group
</Button>
</Typography>
</Box>
);
}
3 changes: 2 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChatMessage } from '../../../common/chat';
import userRequired from '../../../server/auth/userRequired';
import firebaseAdmin, { firestore } from '../../../server/firebase/admin';
import { isDynamicServerError } from 'next/dist/client/components/hooks-server-context';
import { FIRECALL_COLLECTION_ID } from '../../../components/firebase/firestore';

export interface UsersResponse {
// user: UserRecordExtended;
Expand All @@ -30,7 +31,7 @@ async function newChatMessage(
};

const chatCollection = firestore
.collection('call')
.collection(FIRECALL_COLLECTION_ID)
.doc(firecallId)
.collection('chat');

Expand Down
5 changes: 3 additions & 2 deletions src/app/api/users/[uid]/messaging/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userRequired from '../../../../../server/auth/userRequired';
import { firestore } from '../../../../../server/firebase/admin';
import { NextRequest, NextResponse } from 'next/server';
import { ApiException } from '../../../errors';
import { USER_COLLECTION_ID } from '../../../../../components/firebase/firestore';

export interface UsersResponse {
// user: UserRecordExtended;
Expand All @@ -14,7 +15,7 @@ export interface RegisterBody {
}

async function handleRegister(uid: string, token: string) {
const doc = firestore.collection('user').doc(`${uid}`);
const doc = firestore.collection(USER_COLLECTION_ID).doc(`${uid}`);

const oldData = (await doc.get()).data();
const tokens = Array.from(
Expand Down Expand Up @@ -67,7 +68,7 @@ export async function POST(
}

async function handleUnRegister(uid: string, token: string) {
const doc = firestore.collection('user').doc(`${uid}`);
const doc = firestore.collection(USER_COLLECTION_ID).doc(`${uid}`);

const oldData = (await doc.get()).data();
const tokens = Array.from(
Expand Down
8 changes: 6 additions & 2 deletions src/app/api/users/[uid]/updateUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use server';
import { isTruthy } from '../../../../common/boolish';
import { feuerwehren } from '../../../../common/feuerwehren';
import { UserRecordExtended } from '../../../../common/users';
import { USER_COLLECTION_ID } from '../../../../components/firebase/firestore';
import { firestore } from '../../../../server/firebase/admin';

export interface UsersResponse {
Expand All @@ -10,15 +13,16 @@ export async function updateUser(uid: string, user: UserRecordExtended) {
const newData = {
displayName: user.displayName,
email: user.email,
authorized: user.authorized,
authorized: isTruthy(user.authorized),
feuerwehr: user.feuerwehr || 'neusiedl',
abschnitt: feuerwehren[user.feuerwehr || 'neusiedl'].abschnitt || 0,
groups: user.groups || [],
};

console.info(`updating ${uid}: ${JSON.stringify(newData)}`);

await firestore
.collection('user')
.collection(USER_COLLECTION_ID)
.doc(`${uid}`)
.set(
Object.fromEntries(
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/users/listUsers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { DocumentData } from 'firebase/firestore';
import { UserRecordExtended } from '../../../common/users';
import firebaseAdmin, { firestore } from '../../../server/firebase/admin';
import { USER_COLLECTION_ID } from '../../../components/firebase/firestore';

export const listUsers = async (): Promise<UserRecordExtended[]> => {
const users: UserRecordExtended[] = (
await firebaseAdmin.auth().listUsers(1000)
).users.map((u) => u.toJSON() as UserRecordExtended);
const userDocs = (await firestore.collection('user').get()).docs;
const userDocs = (await firestore.collection(USER_COLLECTION_ID).get()).docs;
const userDocsMap: { [uid: string]: DocumentData } = {};
userDocs.forEach((doc) => (userDocsMap[doc.id] = doc.data()));
users.forEach((u) => {
Expand Down
Loading

0 comments on commit 0cdd06b

Please sign in to comment.