Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin/users table with update-claims #242 #325

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/api/src/modules/accounts/accounts.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ExternalId,
Passphrase,
Username,
UsersQuerystring,
} from '@algomart/schemas'
import { UpdateUserAccount } from '@algomart/schemas'
import { FastifyReply, FastifyRequest } from 'fastify'
Expand Down Expand Up @@ -63,6 +64,17 @@ export async function getByUsername(
reply.send(account)
}

export async function getUsers(
request: FastifyRequest<{ Querystring: UsersQuerystring }>,
reply: FastifyReply
) {
const accountService = request
.getContainer()
.get<AccountsService>(AccountsService.name)
const users = await accountService.getUsers(request.query)
reply.send(users)
}

export async function verifyPassphrase(
request: FastifyRequest<{ Params: ExternalId; Body: Passphrase }>,
reply: FastifyReply
Expand Down
48 changes: 48 additions & 0 deletions apps/api/src/modules/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {
CreateUserAccountRequest,
ExternalId,
PublicAccount,
SortDirection,
UserAccounts,
UserSortField,
UsersQuerystring,
} from '@algomart/schemas'
import { UpdateUserAccount } from '@algomart/schemas'
import { Username } from '@algomart/schemas'
Expand Down Expand Up @@ -165,6 +169,50 @@ export default class AccountsService {
return this.mapPublicAccount(userAccount)
}

async getUsers({
page = 1,
pageSize = 10,
search = '',
sortBy = UserSortField.CreatedAt,
sortDirection = SortDirection.Ascending,
}: UsersQuerystring): Promise<UserAccounts> {
userInvariant(page > 0, 'page must be greater than 0')
userInvariant(
pageSize > 0 || pageSize === -1,
'pageSize must be greater than 0'
)
userInvariant(
[
UserSortField.Username,
UserSortField.CreatedAt,
UserSortField.Email,
].includes(sortBy),
'sortBy must be one of username, email, or createdAt'
)
userInvariant(
[SortDirection.Ascending, SortDirection.Descending].includes(
sortDirection
),
'sortDirection must be one of asc or desc'
)

const query = UserAccountModel.query()

// Find payer
if (search?.length > 0) {
const ilikeSearch = `%${search}%`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do any sanitizing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed any value in a non-raw is escaped, so even though it's interpolated here it's passed to .where(.., ilikeSearch) and .orWhere. Some poking around https://github.com/knex/documentation/issues/73#issuecomment-572482153 , I'm not completely sure now...

query
.where('email', 'ilike', ilikeSearch)
.orWhere('username', 'ilike', ilikeSearch)
}

const { results: users, total } = await query
.orderBy(sortBy, sortDirection)
.page(page >= 1 ? page - 1 : page, pageSize)

return { users, total }
}

async verifyPassphraseFor(externalId: string, passphrase: string) {
const userAccount = await UserAccountModel.query()
.findOne({ externalId })
Expand Down
17 changes: 17 additions & 0 deletions apps/api/src/modules/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
PassphraseSchema,
PublicUserAccountSchema,
UpdateUserAccountSchema,
UserAccountsSchema,
UsernameSchema,
UsersQuerystringSchema,
} from '@algomart/schemas'
import { Type } from '@sinclair/typebox'
import { FastifyInstance } from 'fastify'
Expand All @@ -14,6 +16,7 @@ import {
createAccount,
getByExternalId,
getByUsername,
getUsers,
removeUser,
updateAccount,
verifyPassphrase,
Expand Down Expand Up @@ -69,6 +72,20 @@ export async function accountsRoutes(app: FastifyInstance) {
},
getByUsername
)
.get(
'/all',
{
schema: {
tags,
security,
querystring: UsersQuerystringSchema,
response: {
200: UserAccountsSchema,
},
},
},
getUsers
)
.get(
'/:externalId',
{
Expand Down
13 changes: 13 additions & 0 deletions apps/web/locales/en-US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@
"resetPaymentDesc": "Resetting the transaction puts it back into the pending state which allows the buyer to attept to make the payment again using the original wire instructions.",
"revokePack": "Revoke pack",
"revokePackDesc": "If someone fails to pay you can revoke the pack. Note: Re-selling/auctioning revoked packs is not yet supported."
},
"users": {
"roles": {
"admin": "Admin"
},
"addRole": "Add {{username}} to {{role}}?",
"removeRole": "Remove {{username}} from {{role}}?",
"table": {
"Username": "Username",
"Email": "Email",
"Created": "Created",
"Role": "Role"
}
}
}
1 change: 1 addition & 0 deletions apps/web/locales/en-US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"Sign Up": "Sign Up",
"Transaction": "Transaction",
"Transactions": "Transactions",
"Users": "Users",
"User Profile": "@{{name}} Profile"
},
"nav": {
Expand Down
8 changes: 1 addition & 7 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,7 @@ module.exports = withNx(
process.env.NEXT_PUBLIC_CRYPTO_PAYMENT_ENABLED,
NODE_ENV: process.env.NODE_ENV,
},
redirects: async () => [
{
source: '/admin',
destination: '/admin/transactions',
permanent: false,
},
],
redirects: async () => [],
})
)

Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/clients/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ import {
UpdatePayment,
UpdatePaymentCard,
UpdateUserAccount,
UserAccounts,
Username,
UsersQuerystring,
WirePayment,
} from '@algomart/schemas'
import axios from 'axios'
Expand All @@ -72,6 +74,7 @@ import {
getPacksByOwnerFilterQuery,
getPaymentsFilterQuery,
getPublishedPacksFilterQuery,
getUsersFilterQuery,
} from '@/utils/filters'
import { HttpTransport, validateStatus } from '@/utils/http-transport'
import { invariant } from '@/utils/invariant'
Expand Down Expand Up @@ -143,6 +146,13 @@ export class ApiClient {
})
}

async getUsers(query: UsersQuerystring): Promise<UserAccounts> {
const searchQuery = getUsersFilterQuery(query)
return await this.http
.get<UserAccounts>(`accounts/all?${searchQuery}`)
.then((response) => response.data)
}

async verifyPassphrase(externalId: string, passphrase: string) {
return await this.http
.post<{ isValid: boolean }>(`accounts/${externalId}/verify-passphrase`, {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SortDirection } from '@algomart/schemas'
import { ArrowUpIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import get from 'lodash.get'
import get from 'lodash/get'

import {
Table as TableElement,
Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/pages/admin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { GetServerSideProps } from 'next'
import useTranslation from 'next-translate/useTranslation'

import Breadcrumbs from '@/components/breadcrumbs'
import Tabs from '@/components/tabs/tabs'
import AdminLayout from '@/layouts/admin-layout'
import { isAuthenticatedUserAdmin } from '@/services/api/auth-service'
import { urls } from '@/utils/urls'

export default function AdminPage() {
const { t, lang } = useTranslation('admin')

return (
<AdminLayout pageTitle={t('common:pageTitles.Admin')}>
<Breadcrumbs breadcrumbs={[{ label: 'Admin' }]} />
<Tabs
activeTab={-1}
tabs={[
{
href: urls.admin.transactions,
label: t('common:pageTitles.Transactions'),
},
{ href: urls.admin.users, label: t('common:pageTitles.Users') },
]}
/>
</AdminLayout>
)
}

export const getServerSideProps: GetServerSideProps = async (context) => {
// Check if the user is admin (check again on render, to prevent caching of claims)
const user = await isAuthenticatedUserAdmin(context)
if (!user) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}

return {
props: {},
}
}