Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions adminapp/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import StaticStringCreatePage from "./pages/StaticStringCreatePage";
import StaticStringsNamespacePage from "./pages/StaticStringsNamespacePage";
import StaticStringsPage from "./pages/StaticStringsPage";
import VendorAccountDetailPage from "./pages/VendorAccountDetailPage";
import VendorAccountEditPage from "./pages/VendorAccountEditPage";
import VendorAccountListPage from "./pages/VendorAccountListPage";
import VendorConfigurationDetailPage from "./pages/VendorConfigurationDetailPage";
import VendorConfigurationEditPage from "./pages/VendorConfigurationEditPage";
Expand Down Expand Up @@ -577,6 +578,11 @@ function PageSwitch() {
VendorAccountDetailPage
)}
/>
<Route
exact
path="/vendor-account/:id/edit"
element={renderWithHocs(redirectIfUnauthed, withLayout(), VendorAccountEditPage)}
/>
<Route
exact
path="/vendor-configurations"
Expand Down
2 changes: 2 additions & 0 deletions adminapp/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ export default {
get("/adminapi/v1/anon_proxy_vendor_accounts", data, ...args),
getVendorAccount: ({ id, ...data }, ...args) =>
get(`/adminapi/v1/anon_proxy_vendor_accounts/${id}`, data, ...args),
updateVendorAccount: ({ id, ...data }, ...args) =>
post(`/adminapi/v1/anon_proxy_vendor_accounts/${id}`, data, ...args),
destroyVendorAccount: ({ id, ...data }, ...args) =>
post(`/adminapi/v1/anon_proxy_vendor_accounts/${id}/destroy`, data, ...args),

Expand Down
60 changes: 52 additions & 8 deletions adminapp/src/components/AdminActions.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,57 @@
import api from "../api";
import useErrorSnackbar from "../hooks/useErrorSnackbar";
import { Button, Card, CardContent, CircularProgress, Stack } from "@mui/material";
import {
Button,
Card,
CardContent,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
Stack,
} from "@mui/material";
import Typography from "@mui/material/Typography";
import size from "lodash/size";
import React from "react";

export default function AdminActions({ adminActions, updateModel }) {
const { enqueueErrorSnackbar } = useErrorSnackbar();
const [lastResponse, setLastResponse] = React.useState(null);
const [actionLoadings, setActionLoadings] = React.useState({});
const [confirmingAction, setConfirmingAction] = React.useState(null);

function handleClick(e, url, params) {
e.preventDefault();
setActionLoadings({ ...actionLoadings, [url]: true });
if (!size(adminActions)) {
return null;
}

function submitAction(e, action) {
setActionLoadings({ ...actionLoadings, [action.url]: true });
api
.post(url, params)
.post(action.url, action.params)
.then((r) => {
if (r.headers["admin-action-handler"] === "update") {
updateModel(r.data);
} else {
setLastResponse(r.data);
}
setConfirmingAction(null); // In case this came from the confirmation modal
})
.catch(enqueueErrorSnackbar)
.finally(() => setActionLoadings({ ...actionLoadings, [url]: false }));
.finally(() => setActionLoadings({ ...actionLoadings, [action.url]: false }));
}

/**
* @param {MouseEvent} e
* @param {AdminAction} action
*/
function handleClick(e, action) {
e.preventDefault();
if (action.confirmationPrompt) {
setConfirmingAction(action);
} else {
submitAction(e, action);
}
}

return (
Expand All @@ -32,11 +61,11 @@ export default function AdminActions({ adminActions, updateModel }) {
Actions
</Typography>
<Stack direction="row" gap={2}>
{adminActions.map(({ label, url, params }) => (
{adminActions.map(({ label, url, ...rest }) => (
<Button
key={url}
variant="outlined"
onClick={(e) => handleClick(e, url, params)}
onClick={(e) => handleClick(e, { url, ...rest })}
>
{actionLoadings[url] && (
<CircularProgress size="1rem" sx={{ marginRight: 1 }} />
Expand All @@ -51,6 +80,21 @@ export default function AdminActions({ adminActions, updateModel }) {
</pre>
)}
</CardContent>
<Dialog open={Boolean(confirmingAction)} onClose={() => setConfirmingAction(null)}>
<DialogContent>
<DialogContentText>{confirmingAction?.confirmationPrompt}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmingAction(null)}>Cancel</Button>
<Button
variant="contained"
color="error"
onClick={(e) => submitAction(e, confirmingAction)}
>
Confirm
</Button>
</DialogActions>
</Dialog>
</Card>
);
}
10 changes: 9 additions & 1 deletion adminapp/src/pages/VendorAccountDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import api from "../api";
import AdminActions from "../components/AdminActions";
import AdminLink from "../components/AdminLink";
import BoolCheckmark from "../components/BoolCheckmark";
import DetailGrid from "../components/DetailGrid";
Expand All @@ -15,6 +16,7 @@ export default function VendorAccountDetailPage() {
resource="vendor_account"
apiGet={api.getVendorAccount}
apiDelete={api.destroyVendorAccount}
canEdit
properties={(model) => [
{ label: "ID", value: model.id },
{ label: "Created At", value: dayjs(model.createdAt) },
Expand All @@ -39,9 +41,13 @@ export default function VendorAccountDetailPage() {
label: "Latest Access Code Set At",
value: formatDate(model.latestAccessCodeSetAt),
},
{
label: "Pending Closure",
value: model.pendingClosure,
},
]}
>
{(model) => [
{(model, setModel) => [
<DetailGrid
title="Configuration"
properties={[
Expand Down Expand Up @@ -69,11 +75,13 @@ export default function VendorAccountDetailPage() {
title="Member Contact"
properties={[
{ label: "ID", value: <AdminLink model={model.contact} /> },
{ label: "Created At", value: formatDate(model.contact.createdAt) },
{ label: "Address", value: model.contact.formattedAddress },
{ label: "Relay Key", value: model.contact.relayKey },
]}
/>
),
<AdminActions adminActions={model.adminActions} updateModel={setModel} />,
<RelatedList
title="Registrations"
rows={model.registrations}
Expand Down
14 changes: 14 additions & 0 deletions adminapp/src/pages/VendorAccountEditPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import api from "../api";
import ResourceEdit from "../components/ResourceEdit";
import VendorAccountForm from "./VendorAccountForm";
import React from "react";

export default function VendorAccountEditPage() {
return (
<ResourceEdit
apiGet={api.getVendorAccount}
apiUpdate={api.updateVendorAccount}
Form={VendorAccountForm}
/>
);
}
66 changes: 66 additions & 0 deletions adminapp/src/pages/VendorAccountForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import FormLayout from "../components/FormLayout";
import ResponsiveStack from "../components/ResponsiveStack";
import SafeDateTimePicker from "../components/SafeDateTimePicker";
import { formatOrNull } from "../modules/dayConfig";
import { FormControlLabel, Stack, Switch, TextField, Typography } from "@mui/material";
import React from "react";

export default function VendorAccountForm({
isCreate,
resource,
setField,
setFieldFromInput,
register,
isBusy,
onSubmit,
}) {
return (
<FormLayout
title={isCreate ? "Create External Account" : "Update External Account"}
subtitle="External Vendor Accounts represent a member's account within an external vendor configuration."
onSubmit={onSubmit}
isBusy={isBusy}
>
<Typography variant="subtitle1" color="error" sx={{ marginBottom: 4 }}>
<strong>Be careful when changing these values. For technical use only.</strong>
</Typography>
<Stack spacing={2}>
<TextField
{...register("latestAccessCode")}
label="Latest Access Code"
name="latestAccessCode"
value={resource.latestAccessCode}
onChange={setFieldFromInput}
/>
<TextField
{...register("latestAccessCodeMagicLink")}
label="Latest Access Code Magic Link"
name="latestAccessCodeMagicLink"
value={resource.latestAccessCodeMagicLink}
onChange={setFieldFromInput}
/>
<ResponsiveStack>
<SafeDateTimePicker
label="Latest Access Code Set At"
value={resource.latestAccessCodeSetAt}
onChange={(v) => setField("latestAccessCodeSetAt", formatOrNull(v))}
sx={{ width: { xs: "100%", sm: "50%" } }}
/>
<SafeDateTimePicker
label="Latest Access Code Requested At"
value={resource.latestAccessCodeRequestedAt}
onChange={(v) => setField("latestAccessCodeRequestedAt", formatOrNull(v))}
sx={{ width: { xs: "100%", sm: "50%" } }}
/>
</ResponsiveStack>
<FormControlLabel
control={<Switch />}
label="Pending Closure"
name="pendingClosure"
checked={resource.pendingClosure}
onChange={setFieldFromInput}
/>
</Stack>
</FormLayout>
);
}
27 changes: 27 additions & 0 deletions db/migrations/110_multiple_contacts_per_vendor_account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

Sequel.migration do
tbl = :anon_proxy_member_contacts
up do
# Coming from the previous migration, we need to drop the constraint (since the index depends on it),
# but using add_index in the DOWN, then coming back UP, we don't have the constraint,
# we have to drop the index. I am not really sure why.
run "ALTER TABLE #{tbl} DROP CONSTRAINT IF EXISTS anon_proxy_member_contacts_email_relay_key_key"
run "ALTER TABLE #{tbl} DROP CONSTRAINT IF EXISTS anon_proxy_member_contacts_phone_relay_key_key"
alter_table tbl do
drop_index([], name: :anon_proxy_member_contacts_email_relay_key_key, if_exists: true)
drop_index([], name: :anon_proxy_member_contacts_phone_relay_key_key, if_exists: true)
add_index :email
add_index :phone
end
end

down do
alter_table tbl do
drop_index :email
drop_index :phone
add_index [:email, :relay_key], unique: true, name: :anon_proxy_member_contacts_email_relay_key_key
add_index [:phone, :relay_key], unique: true, name: :anon_proxy_member_contacts_phone_relay_key_key
end
end
end
6 changes: 3 additions & 3 deletions lib/suma/admin_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
# The response value of the action is displayed in the admin UI.
module Suma::AdminActions
class Action < Suma::TypedStruct
attr_reader :label, :url, :params
attr_reader :label, :url, :params, :confirmation_prompt

def _defaults = {params: {}}
def _defaults = {params: {}, confirmation_prompt: ""}
end

def admin_actions = _admin_actions_self.select(&:itself)
# @return [Array<Action>]
def _admin_actions_self = raise NotImplementedError
def _admin_action(label, url, params: {}) = Action.new(label:, url:, params:)
def _admin_action(label, url, params: {}, **) = Action.new(label:, url:, params:, **)
end
54 changes: 54 additions & 0 deletions lib/suma/admin_api/anon_proxy_vendor_accounts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class DetailedVendorAccountEntity < AnonProxyVendorAccountEntity
expose :latest_access_code_set_at
expose :latest_access_code_requested_at
expose :latest_access_code_magic_link
expose :pending_closure
expose :contact, with: AnonProxyMemberContactEntity
expose :registrations, with: VendorAccountRegistrationEntity
end
Expand All @@ -37,10 +38,63 @@ class DetailedVendorAccountEntity < AnonProxyVendorAccountEntity
Suma::AnonProxy::VendorAccount,
DetailedVendorAccountEntity,
)
Suma::AdminAPI::CommonEndpoints.update(
self,
Suma::AnonProxy::VendorAccount,
DetailedVendorAccountEntity,
) do
params do
optional :latest_access_code, type: String
optional :latest_access_code_magic_link, type: String
optional :latest_access_code_set_at, type: Time
optional :latest_access_code_requested_at, type: Time
optional :pending_closure, type: Boolean
end
end

Suma::AdminAPI::CommonEndpoints.destroy(
self,
Suma::AnonProxy::VendorAccount,
DetailedVendorAccountEntity,
)

route_param :id, type: Integer do
helpers do
def lookup!(rw)
check_admin_role_access!(rw, Suma::AnonProxy::VendorAccount)
(m = Suma::AnonProxy::VendorAccount[params[:id]]) or forbidden!
return m
end
end

post :revoke_lime_login do
a = lookup!(:write)
a.member.audit_activity("revokelime", action: a)
Suma::Program::ServiceRevoker.new(dry_run: false).close_lime_account(a)
created_resource_headers(a.id, a.admin_link)
admin_action_handler :update
status 200
present a, with: DetailedVendorAccountEntity
end

post "revoke_lime_login/finish" do
a = lookup!(:write)
a.update(pending_closure: false, contact: nil)
created_resource_headers(a.id, a.admin_link)
admin_action_handler :update
status 200
present a, with: DetailedVendorAccountEntity
end

post :revoke_lyft_pass do
a = lookup!(:write)
a.member.audit_activity("revokelyft", action: a)
Suma::Program::ServiceRevoker.new(dry_run: false).revoke_lyft_passes(a.registrations)
created_resource_headers(a.id, a.admin_link)
admin_action_handler :update
status 200
present a, with: DetailedVendorAccountEntity
end
end
end
end
6 changes: 3 additions & 3 deletions lib/suma/anon_proxy/auth_to_vendor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ class Suma::AnonProxy::AuthToVendor
extend Suma::SimpleRegistry

require_relative "auth_to_vendor/fake"
register(:fake, Fake)
register(Fake)

require_relative "auth_to_vendor/lime"
register(:lime, Lime)
register(Lime)

require_relative "auth_to_vendor/lyft_pass"
register(:lyft_pass, LyftPass)
register(LyftPass)

# @return [Suma::AnonProxy::Provision]
def self.create!(key, vendor_account:)
Expand Down
2 changes: 2 additions & 0 deletions lib/suma/anon_proxy/auth_to_vendor/fake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

class Suma::AnonProxy::AuthToVendor::Fake < Suma::AnonProxy::AuthToVendor
class << self
def key = :fake

attr_accessor :calls, :auth, :needs_polling

def reset
Expand Down
Loading
Loading