From 1d24f59a660241afc06f716bdbaefc9e074a8d59 Mon Sep 17 00:00:00 2001 From: Emanuele Paolini Date: Mon, 16 Dec 2024 07:05:50 +0100 Subject: [PATCH] admin flag in database --- app/admin/users/page.tsx | 53 ++++++++++++++--- app/db.ts | 9 +++ app/graphql/permissions.ts | 52 +++++++++++++++++ app/graphql/route.ts | 117 +++++++++++++++---------------------- app/graphql/types.ts | 16 +++++ 5 files changed, 167 insertions(+), 80 deletions(-) create mode 100644 app/graphql/permissions.ts create mode 100644 app/graphql/types.ts diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 374c2e1..e2fdb36 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -1,17 +1,18 @@ "use client" -import { useQuery, gql, useMutation } from '@apollo/client' -import { useState } from 'react' +import {useState} from 'react' +import { useQuery, useMutation, gql } from '@apollo/client' import Provider from '../../components/Provider' import Loading from '../../components/Loading' import Error from '../../components/Error' import Amount from '../../components/Amount' -import {myDate, myTime} from '../../utils' +import {myDate} from '../../utils' import Table from '../../components/Table' import Thead from '../../components/Thead' import Th from '../../components/Th' import Tr from '../../components/Tr' import Td from '../../components/Td' +import Button from '../../components/Button' export default function UsersPage({}) { return @@ -19,21 +20,32 @@ export default function UsersPage({}) { } -const GET_USERS = gql` -query GetUsers { - users { +const GET_USER_TRANSACTIONS = gql` +query GetUserTransactions { + userTransactions { email creditCents timestamp count + _id + admin } }` function Users() { - const {loading, error, data} = useQuery(GET_USERS) + const {loading, error, data} = useQuery(GET_USER_TRANSACTIONS) + const [edit, setEdit] = useState(false) if (loading) return if (error) return return <> + {edit + ? setEdit(false)}> + termina modifiche + + : setEdit(true)}> + modifica + + } @@ -41,18 +53,41 @@ function Users() { + - {data.users.map((user: any, i: number) => + {data.userTransactions.map((user: any, i: number) => + )}
# dataadmin
{user.email} {user.count||""} {myDate(user.timestamp)} + {!edit && user.admin?"✅":""} + {edit && user._id && } +
-} \ No newline at end of file +} + +const UPDATE_USER = gql` +mutation UpdateUser($_id: String!, $data: UpdateUserInput) { + updateUser(_id: $_id, data: $data) +}` + +function UpdateAdminButton({user}:{user: {_id: string, admin: Boolean}}) { + const [updateUser, {data, loading, error}] = useMutation(UPDATE_USER, { + refetchQueries: ["GetUserTransactions"] + }) + return user.admin + ? + : +} diff --git a/app/db.ts b/app/db.ts index e244026..bf7b08e 100644 --- a/app/db.ts +++ b/app/db.ts @@ -55,5 +55,14 @@ async function get_db(): Promise { } async function initialize(db: Db) { + if (config.ADMINS) { + const emails = config.ADMINS.split(',') + console.log('making admins:', emails) + await db.collection('users').updateMany( + { email: { $in: emails } }, + { $set: { admin: true } } + ) + } } + diff --git a/app/graphql/permissions.ts b/app/graphql/permissions.ts new file mode 100644 index 0000000..ed0264e --- /dev/null +++ b/app/graphql/permissions.ts @@ -0,0 +1,52 @@ +import { User, Context } from './types' +import config from '../config' +import { isPermittedEmail } from '../utils' + +/** + * @param context + * @returns user object if authenticated + * @throws error if not authenticated + */ +export function requireAuthenticatedUser(context: Context) { + const user = context?.user + if (!user) throw new Error("not logged in") + return user + } + + /** + * @param context + * @returns user object if authenticated and email is permitted by configuration + * @throws error if not authenticated or email is not permitted + */ + export function requirePermittedUser(context: Context) { + const user = requireAuthenticatedUser(context) + if (!isPermittedEmail(user?.email)) throw new Error("email not permitted") + return user + } + + /** + * @param context + * @returns user object if authenticated and email is in the list of admins + * @throws error if not authenticated or email is not in the list of admins + */ + export function requireAdminUser(context: Context): User { + const authorization = context.req.headers.get('authorization') + if (authorization && !Array.isArray(authorization) && config.ADMIN_SECRET_TOKENS.split(',').includes(authorization)) { + return { email: 'admin', name: 'request with authorization token', picture: '', id: 'unknown_admin', admin: true } + } + + const user = requireAuthenticatedUser(context) + if (!user.admin) throw new Error("not admin") + return user + } + + export function requireCardAuthentication(context: Context): User { + const authorization = context.req.headers.get('authorization') + if (authorization && !Array.isArray(authorization) && config.CARD_SECRET_TOKENS.split(',').includes(authorization)) { + return { email: 'card', name: 'request with authorization token', picture: '', id: 'unknown_card', admin: false } + } + throw new Error("not card user") + } + + + \ No newline at end of file diff --git a/app/graphql/route.ts b/app/graphql/route.ts index 2a17a71..a23ce8f 100644 --- a/app/graphql/route.ts +++ b/app/graphql/route.ts @@ -8,71 +8,8 @@ import { ObjectId } from 'mongodb' import config from '../config' import { isPermittedEmail } from '../utils' - -type User = { - email: string - name: string - picture: string - id: string -} - -type UserWithAdminField = User & { admin: boolean } - -type Context = { - req: NextRequest - res: Response|undefined - user?: User -} - -/** - * @param context - * @returns user object if authenticated - * @throws error if not authenticated - */ -function requireAuthenticatedUser(context: Context): UserWithAdminField { - const user = context?.user - if (!user) throw new Error("not logged in") - return { - ...user, - admin: config.ADMINS.split(',').includes(user.email) - } -} - -/** - * @param context - * @returns user object if authenticated and email is permitted by configuration - * @throws error if not authenticated or email is not permitted - */ -function requirePermittedUser(context: Context): UserWithAdminField { - const user = requireAuthenticatedUser(context) - if (!isPermittedEmail(user?.email)) throw new Error("email not permitted") - return user -} - -/** - * @param context - * @returns user object if authenticated and email is in the list of admins - * @throws error if not authenticated or email is not in the list of admins - */ -function requireAdminUser(context: Context): User { - const authorization = context.req.headers.get('authorization') - if (authorization && !Array.isArray(authorization) && config.ADMIN_SECRET_TOKENS.split(',').includes(authorization)) { - return { email: 'admin', name: 'request with authorization token', picture: '', id: 'unknown_admin' } - } - - const user = requireAuthenticatedUser(context) - if (!user.admin) throw new Error("not admin") - return user -} - -function requireCardAuthentication(context: Context): User { - const authorization = context.req.headers.get('authorization') - if (authorization && !Array.isArray(authorization) && config.CARD_SECRET_TOKENS.split(',').includes(authorization)) { - return { email: 'card', name: 'request with authorization token', picture: '', id: 'unknown_card' } - } - throw new Error("not card user") -} - +import { Context } from './types' +import { requireAdminUser, requireAuthenticatedUser, requirePermittedUser, requireCardAuthentication } from './permissions' const typeDefs = gql` scalar Timestamp @@ -104,6 +41,8 @@ const typeDefs = gql` creditCents: Int count: Int timestamp: Timestamp + admin: Boolean + _id: String } type Balance { @@ -119,6 +58,10 @@ const typeDefs = gql` email: String } + input UpdateUserInput { + admin: Boolean + } + type Query { profile: Profile @@ -158,9 +101,9 @@ const typeDefs = gql` transactionYears: [Int] """ - users and their credit + transactions aggregated by users """ - users: [User] + userTransactions: [User] """ notices @@ -214,6 +157,11 @@ const typeDefs = gql` risolvi una segnalazione """ solveNotice(_id: String!): Boolean + + """ + modifica il flag admin di un utente + """ + updateUser(_id: String!, data: UpdateUserInput): Boolean }` const resolvers = { @@ -318,8 +266,8 @@ const resolvers = { return result }, - users: async(_: any, __: {}, context: Context) => { - const user = requireAdminUser(context) + userTransactions: async(_: any, __: {}, context: Context) => { + requireAdminUser(context) const db = (await databasePromise).db const result = await db.collection("account").aggregate([ @@ -330,9 +278,24 @@ const resolvers = { timestamp: { $max: "$timestamp" }, }}, { $project: { _id: 0, email: "$_id", creditCents: 1, count: 1, timestamp: 1 } }, - { $sort: { email: 1 } } + { $sort: { email: 1 } }, + { $lookup: { + from: "users", + localField: "email", + foreignField: "email", + as: "user" + } + }, + { $unwind: { path: "$user", preserveNullAndEmptyArrays: true} }, + { $project: { + _id: "$user._id", + email: 1, + creditCents: 1, + count: 1, + timestamp: 1, + admin: "$user.admin" } } ]).toArray() - // console.log("users:", result) + console.log(JSON.stringify(result)) return result }, @@ -488,6 +451,15 @@ const resolvers = { const result = await notices.updateOne({ _id: new ObjectId(_id) }, { $set: { solved: true } }) return true + }, + + updateUser: async(_:any, { _id, data }: { _id: string, data: { admin: boolean } }, context: Context) => { + requireAdminUser(context) + const db = (await databasePromise).db + const users = db.collection("users") + const result = await users.updateOne({ _id: new ObjectId(_id) }, + { $set: data }) + return true } }, @@ -517,6 +489,8 @@ const handler = startServerAndCreateNextHandler(server, { const ctx: Context = { req, res } const token = await getToken({ req }) if (!token || !token.email) return ctx // not logged in + const db = (await databasePromise).db + const user = await db.collection("users").findOne({ email: token.email }) return { ...ctx, user: { @@ -524,6 +498,7 @@ const handler = startServerAndCreateNextHandler(server, { name: token.name || '', picture: token.picture || '', id: token.sub || '', + admin: user?.admin || false } } } diff --git a/app/graphql/types.ts b/app/graphql/types.ts new file mode 100644 index 0000000..96f4937 --- /dev/null +++ b/app/graphql/types.ts @@ -0,0 +1,16 @@ +import type { NextRequest } from "next/server" + +export type User = { + email: string + name: string + picture: string + id: string + admin: boolean + } + +export type Context = { + req: NextRequest + res: Response|undefined + user?: User + } +