Admin can manage users

main
Ashelyn Dawn 4 years ago
parent f10d245b20
commit 26addb2e40

@ -1,6 +1,7 @@
const router = require('express-promise-router')()
const parseJSON = require('body-parser').json()
const db = require('../db')
const ensureAdmin = require('./middleware/ensureAdmin')
const sendgrid = require('@sendgrid/mail')
sendgrid.setApiKey(process.env.SENDGRID_KEY)
@ -52,4 +53,28 @@ router.post('/', parseJSON, registerValidation, async (req, res) => {
res.json(user)
})
router.get('/', ensureAdmin, async (req, res) => {
const users = await db.user.findAll()
res.json(users)
})
router.get('/:uuid', ensureAdmin, async (req, res) => {
const user = await db.user.findById(req.params.uuid)
res.json(user)
})
router.get('/:uuid/orders', ensureAdmin, async (req, res) => {
return res.json(await db.order.findAllForUser(req.params.uuid))
})
router.put('/:uuid/admin', ensureAdmin, async (req, res) => {
const user = await db.user.makeAdmin(req.params.uuid)
res.json(user)
})
router.delete('/:uuid/admin', ensureAdmin, async (req, res) => {
const user = await db.user.removeAdmin(req.params.uuid)
res.json(user)
})
module.exports = router;

@ -8,6 +8,8 @@ module.exports = [
'email_confirmed',
'time_registered',
'time_email_confirmed',
'last_active',
'num_orders',
'is_admin'
]
},{

@ -144,3 +144,31 @@ user.markEmailVerified = user_uuid =>
returnType: 'user',
single: true
})
user.findAll = () =>
dbUtil.executeQuery({
query: 'select * from sos.v_user',
returnType: 'user'
})
user.makeAdmin = user_uuid =>
dbUtil.executeFunction({
name: 'set_user_admin',
params: [
user_uuid,
true
],
returnType: 'user',
single: true
})
user.removeAdmin = user_uuid =>
dbUtil.executeFunction({
name: 'set_user_admin',
params: [
user_uuid,
false
],
returnType: 'user',
single: true
})

@ -163,3 +163,25 @@ create or replace view sos.v_shipment as
select * from sos."shipment"
left join sos.v_stockchange on v_stockchange.stockchange_shipment_uuid = shipment_uuid
left join sos.v_item on v_stockchange.stockchange_item_uuid = v_item.item_uuid;
create or replace view sos.v_user as
select
"user".*,
user_num_orders,
user_last_active
from sos.user
left join (
select
order_user_uuid,
count(distinct order_uuid)::int4 user_num_orders
from sos."v_order"
where transaction_payment_state = 'completed'
group by order_user_uuid
) "order" on "user".user_uuid = "order".order_user_uuid
left join (
select
session_user_uuid,
max(session_time_last_active) user_last_active
from sos."session"
group by session_user_uuid
) "last_session" on "user".user_uuid = "last_session".session_user_uuid;

@ -1194,3 +1194,15 @@ begin
return query select * from sos."user" where user_uuid = _user_uuid;
end; $function$;
create or replace function sos.set_user_admin(_user_uuid uuid, _is_admin boolean)
returns setof sos.v_user
language plpgsql
as $function$
begin
update sos."user" set
user_is_admin = _is_admin
where user_uuid = _user_uuid;
return query select * from sos.v_user where user_uuid = _user_uuid;
end; $function$;

@ -3,4 +3,13 @@ import React, {useContext} from 'react'
const UserContext = React.createContext(null);
export const UserContextProvider = UserContext.Provider;
export default () => useContext(UserContext)
export default function useUser(){
const [user, setUser] = useContext(UserContext)
return user
}
export function useSetUser() {
const [user, setUser] = useContext(UserContext)
return setUser
}

@ -49,8 +49,9 @@ Layout.getInitialProps = async ({Component, ctx}) => {
return {pageProps, user, cart}
}
function Layout({ Component, pageProps, user, cart: _cart }){
const cartState = useState(_cart)
function Layout({ Component, pageProps, user, cart }){
const cartState = useState(cart)
const userState = useState(user)
return (
<>
@ -58,7 +59,7 @@ function Layout({ Component, pageProps, user, cart: _cart }){
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
</Head>
<CartContextProvider value={cartState}>
<UserContextProvider value={user}>
<UserContextProvider value={userState}>
<Header/>
<ErrorBoundary>
<AdminNav>

@ -0,0 +1,101 @@
import router from 'next/router'
import {DateTime} from 'luxon'
import AdminToolbar from '~/components/admin/actionBar'
import Table from '~/components/table'
UserDetails.getInitialProps = async ({ctx: {axios, query: {uuid}}}) => {
const {data: user} = await axios.get(`/api/users/${uuid}`)
const {data: orders} = await axios.get(`/api/users/${uuid}/orders`)
return {user, orders: orders.sort(sortOrders)}
}
export default function UserDetails({user, orders}) {
return (
<>
<AdminToolbar title={`User Details: ${user.email}`}/>
<h3>User details</h3>
<div style={{maxWidth: 700, margin: '0 auto'}}>
<p><strong>Registered:</strong> {DateTime.fromISO(user.time_registered).toLocal().toFormat('LLLL dd, h:mm a')}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Password:</strong> {!user.password_hash ? 'Unset' : <>Set.</>}</p>
</div>
<h3>Their Orders</h3>
<Table
columns={[
{name: 'Purchased', extractor: getPurchaseTime},
{name: 'Items', extractor: getNumberItems},
{name: 'Item Price', extractor: getItemPrice},
{name: 'Shipping', extractor: getShippingEstimate},
{name: 'Total', extractor: getAmountPaid},
{name: '', extractor: order =>
<button className="buttonLink" onClick={() => router.push(`/admin/orders/${order.uuid}`)}>Details</button>
}
]}
rows={orders}
/>
</>
)
}
function getPurchaseTime(order){
const mostRecentTransaction = order.transactions.sort(sortTransactions)[0]
const time = parsePaymentTime(mostRecentTransaction)
return time.setZone('local').toFormat('LLLL dd, h:mm a')
}
function getNumberItems(order){
return order.transactions.map(transaction =>
transaction.cart.items.map(item => item.count)
).reduce((a,b)=>(a+b))
}
function getItemPrice(order){
return formatMoney(order.transactions.map(transaction => transaction.item_total_price)
.reduce((a,b)=>(a+b)))
}
function getShippingEstimate(order){
return formatMoney(order.transactions.map(transaction => transaction.shipping_price)
.reduce((a,b)=>(a+b)))
}
function getAmountPaid(order){
return formatMoney(order.transactions.map(({payments}) => payments.map(payment => payment.value_cents))
.flat()
.reduce((a,b)=>(a+b)))
}
function parsePaymentTime({payments}){
for(const payment of payments) {
if(typeof payment.time === 'string')
payment.time = DateTime.fromISO(payment.time)
}
payments.sort((a,b) => b.time.diff(a.time))
return payments[0].time
}
function sortTransactions(a,b){
const timeA = parsePaymentTime(a)
const timeB = parsePaymentTime(b)
return timeB.diff(timeA).as('seconds')
}
function sortOrders(a,b){
const timePaidA = parsePaymentTime(a.transactions.sort(sortTransactions)[0])
const timePaidB = parsePaymentTime(b.transactions.sort(sortTransactions)[0])
return timePaidB.diff(timePaidA).as('seconds')
}
const formatMoney = money => {
if (money === undefined || money === null) return null;
return '$' + (money / 100).toFixed(2)
}

@ -0,0 +1,75 @@
import React, {useState} from 'react'
import router from 'next/router'
import Link from 'next/link'
import {DateTime} from 'luxon'
import {Icon} from '@rmwc/icon'
import axios from 'axios'
import useUser, {useSetUser} from '~/hooks/useUser'
import AdminToolbar from '~/components/admin/actionBar'
import Table from '~/components/table'
UsersTable.getInitialProps = async ({ctx: {axios}}) => {
const {data: users} = await axios.get(`/api/users`)
return {users}
}
export default function UsersTable({users: _users}) {
const currentUser = useUser()
const setCurrentUser = useSetUser()
const [users, setUsers] = useState(_users)
const promoteAdmin = uuid => async ev => {
if(ev) ev.preventDefault()
if(!window.confirm(`Are you sure you want to make this user an admin?`))
return
const {data: user} = await axios.put(`/api/users/${uuid}/admin`)
if(user.uuid === currentUser.uuid)
setCurrentUser(user)
setUsers(users.map(u => u.uuid === uuid ? user : u))
}
const demoteAdmin = uuid => async ev => {
if(ev) ev.preventDefault()
if(!window.confirm(`Are you sure you want to demote this user?`))
return
const {data: user} = await axios.delete(`/api/users/${uuid}/admin`)
if(user.uuid === currentUser.uuid){
setCurrentUser(user)
router.push('/')
}
setUsers(users.map(u => u.uuid === uuid ? user : u))
}
return (
<>
<AdminToolbar title="Users"/>
<Table
columns={[
{name: 'Email', extractor: user => user.email},
{name: 'Orders', extractor: user => user.num_orders},
{name: 'Registered', extractor: user => DateTime.fromISO(user.time_registered).setZone('local').toFormat('LLL dd')},
{name: 'Email Confirmed', extractor: user => user.email_confirmed ? <Icon icon="check"/> : <Icon icon="cancel"/>},
{name: 'Admin', extractor: user => user.is_admin ? <Icon icon="check"/> : <Icon icon="cancel"/>},
{name: 'Actions', extractor: user => (
<span>
<Link href={`/admin/users/${user.uuid}`}><a>Details</a></Link>
{user.is_admin
? <button onClick={demoteAdmin(user.uuid)} type="button" className="buttonLink">Remove Admin</button>
: <button onClick={promoteAdmin(user.uuid)} type="button" className="buttonLink">Make Admin</button>
}
</span>
)}
]}
rows={users}
/>
</>
)
}
Loading…
Cancel
Save