Skip to content

Commit

Permalink
admin flag in database
Browse files Browse the repository at this point in the history
  • Loading branch information
paolini committed Dec 16, 2024
1 parent b06fde8 commit 1d24f59
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 80 deletions.
53 changes: 44 additions & 9 deletions app/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,93 @@
"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 <Provider>
<Users />
</Provider>
}

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 <Loading />
if (error) return <Error error={error}/>
return <>
{edit
? <a className="ml-auto" href="#" onClick={() => setEdit(false)}>
termina modifiche
</a>
:<a className="ml-auto" href="#" onClick={() => setEdit(true)}>
modifica
</a>
}
<Table>
<Thead>
<tr>
<Th className="text-left">email</Th>
<Th className="text-right">#</Th>
<Th className="text-right"></Th>
<Th className="text-left">data</Th>
<Th className="text-left">admin</Th>
</tr>
</Thead>
<tbody>
{data.users.map((user: any, i: number) =>
{data.userTransactions.map((user: any, i: number) =>
<Tr key={i}>
<Td>{user.email}</Td>
<Td className="text-right">{user.count||""}</Td>
<Td className="text-right"><Amount cents={user.creditCents}/></Td>
<Td className="text-left">{myDate(user.timestamp)}</Td>
<Td className="text-left">
{!edit && user.admin?"✅":""}
{edit && user._id && <UpdateAdminButton user={user} />}
</Td>
</Tr>
)}
</tbody>
</Table>
</>
}
}

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
? <Button onClick={() => updateUser({variables: {_id: user._id, data: {admin: false}}})}>
remove admin
</Button>
:<Button onClick={() => updateUser({variables: {_id: user._id, data: {admin: true}}})}>
make admin
</Button>
}
9 changes: 9 additions & 0 deletions app/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,14 @@ async function get_db(): Promise<Database> {
}

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 } }
)
}
}


52 changes: 52 additions & 0 deletions app/graphql/permissions.ts
Original file line number Diff line number Diff line change
@@ -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")
}



117 changes: 46 additions & 71 deletions app/graphql/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -104,6 +41,8 @@ const typeDefs = gql`
creditCents: Int
count: Int
timestamp: Timestamp
admin: Boolean
_id: String
}
type Balance {
Expand All @@ -119,6 +58,10 @@ const typeDefs = gql`
email: String
}
input UpdateUserInput {
admin: Boolean
}
type Query {
profile: Profile
Expand Down Expand Up @@ -158,9 +101,9 @@ const typeDefs = gql`
transactionYears: [Int]
"""
users and their credit
transactions aggregated by users
"""
users: [User]
userTransactions: [User]
"""
notices
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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([
Expand All @@ -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
},

Expand Down Expand Up @@ -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
}
},

Expand Down Expand Up @@ -517,13 +489,16 @@ const handler = startServerAndCreateNextHandler<NextRequest,Context>(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: {
email: token.email,
name: token.name || '',
picture: token.picture || '',
id: token.sub || '',
admin: user?.admin || false
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions app/graphql/types.ts
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 1d24f59

Please sign in to comment.