Merged in feature/2021-07-30 (pull request #149)
feature/2021-07-30 Approved-by: Patrick Fic
This commit is contained in:
@@ -1111,6 +1111,37 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>audit_trail</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>messages</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>jobstatuschange</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>billlines</name>
|
<name>billlines</name>
|
||||||
<children>
|
<children>
|
||||||
|
|||||||
@@ -4,39 +4,39 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:5000",
|
"proxy": "http://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.17",
|
"@apollo/client": "^3.3.21",
|
||||||
"@craco/craco": "^5.9.0",
|
"@craco/craco": "^6.2.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
"@fingerprintjs/fingerprintjs": "^3.2.0",
|
||||||
"@lourenci/react-kanban": "^2.1.0",
|
"@lourenci/react-kanban": "^2.1.0",
|
||||||
"@sentry/react": "^6.3.6",
|
"@sentry/react": "^6.10.0",
|
||||||
"@sentry/tracing": "^6.3.6",
|
"@sentry/tracing": "^6.10.0",
|
||||||
"@stripe/react-stripe-js": "^1.4.0",
|
"@stripe/react-stripe-js": "^1.4.0",
|
||||||
"@stripe/stripe-js": "^1.14.0",
|
"@stripe/stripe-js": "^1.16.0",
|
||||||
"@tanem/react-nprogress": "^3.0.65",
|
"@tanem/react-nprogress": "^3.0.74",
|
||||||
"antd": "^4.15.5",
|
"antd": "^4.16.8",
|
||||||
"apollo-link-logger": "^2.0.0",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.18.0",
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.9.0",
|
||||||
"dotenv": "^9.0.2",
|
"dotenv": "^10.0.0",
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.0.0",
|
"exifr": "^7.1.2",
|
||||||
"firebase": "^8.6.0",
|
"firebase": "^8.7.1",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.1",
|
||||||
"i18next": "^20.2.2",
|
"i18next": "^20.3.4",
|
||||||
"i18next-browser-languagedetector": "^6.1.1",
|
"i18next-browser-languagedetector": "^6.1.2",
|
||||||
"jsoneditor": "^9.4.1",
|
"jsoneditor": "^9.5.2",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.9.17",
|
"libphonenumber-js": "^1.9.22",
|
||||||
"logrocket": "^1.2.0",
|
"logrocket": "^1.2.0",
|
||||||
"markerjs2": "^2.8.1",
|
"markerjs2": "^2.9.0",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"phone": "^2.4.21",
|
"phone": "^3.1.2",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.1",
|
||||||
"rc-queue-anim": "^1.8.5",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-big-calendar": "^0.33.2",
|
"react-big-calendar": "^0.33.2",
|
||||||
@@ -45,26 +45,26 @@
|
|||||||
"react-drag-listview": "^0.1.8",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-grid-layout": "^1.2.5",
|
"react-grid-layout": "^1.2.5",
|
||||||
"react-i18next": "^11.8.15",
|
"react-i18next": "^11.11.3",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-number-format": "^4.5.5",
|
"react-number-format": "^4.6.4",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-resizable": "^3.0.1",
|
"react-resizable": "^3.0.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.3",
|
||||||
"recharts": "^2.0.7",
|
"recharts": "^2.0.10",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"redux-state-sync": "^3.1.2",
|
"redux-state-sync": "^3.1.2",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"sass": "^1.32.13",
|
"sass": "^1.35.2",
|
||||||
"socket.io-client": "^4.1.2",
|
"socket.io-client": "^4.1.3",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
"web-vitals": "^1.1.2",
|
"web-vitals": "^2.1.0",
|
||||||
"workbox-background-sync": "^6.1.5",
|
"workbox-background-sync": "^6.1.5",
|
||||||
"workbox-broadcast-update": "^6.1.5",
|
"workbox-broadcast-update": "^6.1.5",
|
||||||
"workbox-cacheable-response": "^6.1.5",
|
"workbox-cacheable-response": "^6.1.5",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Card, Table } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export default function JobAuditTrail({ jobId }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
|
variables: { jobid: jobId },
|
||||||
|
skip: !jobId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("audit.fields.created"),
|
||||||
|
dataIndex: "created",
|
||||||
|
key: "created",
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.useremail"),
|
||||||
|
dataIndex: "useremail",
|
||||||
|
key: "useremail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.operation"),
|
||||||
|
dataIndex: "operation",
|
||||||
|
key: "operation",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("jobs.labels.audit")}>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data ? data.audit_trail : []}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,18 +6,21 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||||
@@ -29,6 +32,10 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
|||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobstatuschange(status),
|
||||||
|
});
|
||||||
// refetch();
|
// refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
import { gql } from "@apollo/client";
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
export const QUERY_AUDIT_TRAIL = gql`
|
export const QUERY_AUDIT_TRAIL = gql`
|
||||||
query QUERY_AUDIT_TRAIL($id: uuid!) {
|
query QUERY_AUDIT_TRAIL($jobid: uuid!) {
|
||||||
audit_trail(where: { recordid: { _eq: $id } }) {
|
audit_trail(
|
||||||
|
where: { jobid: { _eq: $jobid } }
|
||||||
|
order_by: { created: desc }
|
||||||
|
) {
|
||||||
useremail
|
useremail
|
||||||
tabname
|
jobid
|
||||||
schemaname
|
|
||||||
recordid
|
|
||||||
operation
|
operation
|
||||||
old_val
|
|
||||||
new_val
|
|
||||||
id
|
id
|
||||||
created
|
created
|
||||||
bodyshopid
|
bodyshopid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INSERT_AUDIT_TRAIL = gql`
|
||||||
|
mutation INSERT_AUDIT_TRAIL($auditObj: audit_trail_insert_input!) {
|
||||||
|
insert_audit_trail_one(object: $auditObj) {
|
||||||
|
id
|
||||||
|
jobid
|
||||||
|
billid
|
||||||
|
bodyshopid
|
||||||
|
created
|
||||||
|
operation
|
||||||
|
useremail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import ScheduleJobModalContainer from "../../components/schedule-job-modal/sched
|
|||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -279,6 +280,17 @@ export function JobsDetailPage({
|
|||||||
>
|
>
|
||||||
<JobNotesContainer jobId={job.id} />
|
<JobNotesContainer jobId={job.id} />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab={
|
||||||
|
<span>
|
||||||
|
<Icon component={FaRegStickyNote} />
|
||||||
|
{t("jobs.labels.audit")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
key="audit"
|
||||||
|
>
|
||||||
|
<JobAuditTrail jobId={job.id} />
|
||||||
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,3 +53,8 @@ export const setOnline = (isOnline) => ({
|
|||||||
type: ApplicationActionTypes.SET_ONLINE_STATUS,
|
type: ApplicationActionTypes.SET_ONLINE_STATUS,
|
||||||
payload: isOnline,
|
payload: isOnline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertAuditTrail = ({ jobid, billid, operation }) => ({
|
||||||
|
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||||
|
payload: { jobid, billid, operation },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
||||||
|
import { INSERT_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
import client from "../../utils/GraphQLClient";
|
import client from "../../utils/GraphQLClient";
|
||||||
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
|
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
|
||||||
import {
|
import {
|
||||||
@@ -125,6 +126,56 @@ export function* calculateScheduleLoad({ payload: end }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* applicationSagas() {
|
export function* onInsertAuditTrail() {
|
||||||
yield all([call(onCalculateScheduleLoad)]);
|
yield takeLatest(
|
||||||
|
ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||||
|
insertAuditTrailSaga
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* insertAuditTrailSaga({
|
||||||
|
payload: { jobid, billid, operation },
|
||||||
|
}) {
|
||||||
|
const state = yield select();
|
||||||
|
const bodyshop = state.user.bodyshop;
|
||||||
|
const currentUser = state.user.currentUser;
|
||||||
|
console.log(
|
||||||
|
"Inserting audit trail for",
|
||||||
|
bodyshop.shopname,
|
||||||
|
currentUser.email,
|
||||||
|
jobid,
|
||||||
|
billid,
|
||||||
|
operation
|
||||||
|
);
|
||||||
|
const variables = {
|
||||||
|
auditObj: {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid,
|
||||||
|
billid,
|
||||||
|
operation,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
yield client.mutate({
|
||||||
|
mutation: INSERT_AUDIT_TRAIL,
|
||||||
|
variables,
|
||||||
|
update(cache, { data }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
audit_trail(existingAuditTrail, { readField }) {
|
||||||
|
const newAuditTrail = cache.writeQuery({
|
||||||
|
data: data.insert_audit_trail_one,
|
||||||
|
query: INSERT_AUDIT_TRAIL,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
return [...existingAuditTrail, newAuditTrail];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* applicationSagas() {
|
||||||
|
yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail)]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ const ApplicationActionTypes = {
|
|||||||
SET_JOB_READONLY: "SET_JOB_READONLY",
|
SET_JOB_READONLY: "SET_JOB_READONLY",
|
||||||
SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
|
SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
|
||||||
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||||
|
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||||
};
|
};
|
||||||
export default ApplicationActionTypes;
|
export default ApplicationActionTypes;
|
||||||
|
|||||||
@@ -82,6 +82,11 @@
|
|||||||
"values": "Values"
|
"values": "Values"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"audit_trail": {
|
||||||
|
"messages": {
|
||||||
|
"jobstatuschange": "Job status changed to {{status}}."
|
||||||
|
}
|
||||||
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"newline": "New Line"
|
"newline": "New Line"
|
||||||
|
|||||||
@@ -82,6 +82,11 @@
|
|||||||
"values": ""
|
"values": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"audit_trail": {
|
||||||
|
"messages": {
|
||||||
|
"jobstatuschange": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"newline": ""
|
"newline": ""
|
||||||
|
|||||||
@@ -82,6 +82,11 @@
|
|||||||
"values": ""
|
"values": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"audit_trail": {
|
||||||
|
"messages": {
|
||||||
|
"jobstatuschange": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"newline": ""
|
"newline": ""
|
||||||
|
|||||||
8
client/src/utils/AuditTrailMappings.js
Normal file
8
client/src/utils/AuditTrailMappings.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
|
||||||
|
const AuditTrailMapping = {
|
||||||
|
jobstatuschange: (status) =>
|
||||||
|
i18n.t("audit_trail.messages.jobstatuschange", { status }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuditTrailMapping;
|
||||||
724
client/yarn.lock
724
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
allow_aggregations: false
|
||||||
|
columns:
|
||||||
|
- id
|
||||||
|
- new_val
|
||||||
|
- old_val
|
||||||
|
- operation
|
||||||
|
- schemaname
|
||||||
|
- tabname
|
||||||
|
- useremail
|
||||||
|
- created
|
||||||
|
- bodyshopid
|
||||||
|
- recordid
|
||||||
|
computed_fields: []
|
||||||
|
filter:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: create_select_permission
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: drop_select_permission
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "schemaname" text;
|
||||||
|
type: run_sql
|
||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "schemaname" DROP NOT NULL;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "schemaname" CASCADE;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "tabname" text;
|
||||||
|
type: run_sql
|
||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "tabname" DROP NOT NULL;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "tabname" CASCADE;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "recordid" uuid;
|
||||||
|
type: run_sql
|
||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "recordid" DROP NOT NULL;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "recordid" CASCADE;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "jobid";
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "jobid" uuid NULL;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "billid";
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "billid" uuid NULL;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey";
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail"
|
||||||
|
add constraint "audit_trail_billid_fkey"
|
||||||
|
foreign key ("billid")
|
||||||
|
references "public"."bills"
|
||||||
|
("id") on update cascade on delete cascade;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey",
|
||||||
|
add constraint "audit_trail_billid_fkey"
|
||||||
|
foreign key ("billid")
|
||||||
|
references "public"."bills"
|
||||||
|
("id")
|
||||||
|
on update cascade
|
||||||
|
on delete cascade;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey",
|
||||||
|
add constraint "audit_trail_billid_fkey"
|
||||||
|
foreign key ("billid")
|
||||||
|
references "public"."bills"
|
||||||
|
("id") on update cascade on delete set null;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey",
|
||||||
|
add constraint "audit_trail_billid_fkey"
|
||||||
|
foreign key ("billid")
|
||||||
|
references "public"."bills"
|
||||||
|
("id")
|
||||||
|
on update cascade
|
||||||
|
on delete set null;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey",
|
||||||
|
add constraint "audit_trail_billid_fkey"
|
||||||
|
foreign key ("billid")
|
||||||
|
references "public"."bills"
|
||||||
|
("id") on update cascade on delete set null;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: alter table "public"."audit_trail" drop constraint "audit_trail_jobid_fkey";
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail"
|
||||||
|
add constraint "audit_trail_jobid_fkey"
|
||||||
|
foreign key ("jobid")
|
||||||
|
references "public"."jobs"
|
||||||
|
("id") on update cascade on delete set null;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_useremail_fkey",
|
||||||
|
add constraint "audit_trail_useremail_fkey"
|
||||||
|
foreign key ("useremail")
|
||||||
|
references "public"."users"
|
||||||
|
("email")
|
||||||
|
on update restrict
|
||||||
|
on delete restrict;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: |-
|
||||||
|
alter table "public"."audit_trail" drop constraint "audit_trail_useremail_fkey",
|
||||||
|
add constraint "audit_trail_useremail_fkey"
|
||||||
|
foreign key ("useremail")
|
||||||
|
references "public"."users"
|
||||||
|
("email") on update cascade on delete set null;
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
- args:
|
||||||
|
relationship: audit_trails
|
||||||
|
table:
|
||||||
|
name: jobs
|
||||||
|
schema: public
|
||||||
|
type: drop_relationship
|
||||||
|
- args:
|
||||||
|
relationship: audit_trails
|
||||||
|
table:
|
||||||
|
name: bills
|
||||||
|
schema: public
|
||||||
|
type: drop_relationship
|
||||||
|
- args:
|
||||||
|
relationship: job
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: drop_relationship
|
||||||
|
- args:
|
||||||
|
relationship: bill
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: drop_relationship
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
- args:
|
||||||
|
name: audit_trails
|
||||||
|
table:
|
||||||
|
name: jobs
|
||||||
|
schema: public
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: create_array_relationship
|
||||||
|
- args:
|
||||||
|
name: audit_trails
|
||||||
|
table:
|
||||||
|
name: bills
|
||||||
|
schema: public
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: billid
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: create_array_relationship
|
||||||
|
- args:
|
||||||
|
name: job
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: jobid
|
||||||
|
type: create_object_relationship
|
||||||
|
- args:
|
||||||
|
name: bill
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: billid
|
||||||
|
type: create_object_relationship
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: drop_insert_permission
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
allow_upsert: true
|
||||||
|
backend_only: false
|
||||||
|
check:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
columns:
|
||||||
|
- id
|
||||||
|
- created
|
||||||
|
- operation
|
||||||
|
- new_val
|
||||||
|
- old_val
|
||||||
|
- useremail
|
||||||
|
- bodyshopid
|
||||||
|
- jobid
|
||||||
|
- billid
|
||||||
|
set: {}
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: create_insert_permission
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: drop_select_permission
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
allow_aggregations: false
|
||||||
|
backend_only: false
|
||||||
|
columns:
|
||||||
|
- id
|
||||||
|
- new_val
|
||||||
|
- old_val
|
||||||
|
- operation
|
||||||
|
- useremail
|
||||||
|
- created
|
||||||
|
- billid
|
||||||
|
- bodyshopid
|
||||||
|
- jobid
|
||||||
|
computed_fields: []
|
||||||
|
filter:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
limit: null
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: audit_trail
|
||||||
|
schema: public
|
||||||
|
type: create_select_permission
|
||||||
@@ -226,12 +226,41 @@ tables:
|
|||||||
schema: public
|
schema: public
|
||||||
name: audit_trail
|
name: audit_trail
|
||||||
object_relationships:
|
object_relationships:
|
||||||
|
- name: bill
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: billid
|
||||||
- name: bodyshop
|
- name: bodyshop
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: bodyshopid
|
foreign_key_constraint_on: bodyshopid
|
||||||
|
- name: job
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: jobid
|
||||||
- name: user
|
- name: user
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: useremail
|
foreign_key_constraint_on: useremail
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
columns:
|
||||||
|
- id
|
||||||
|
- created
|
||||||
|
- operation
|
||||||
|
- new_val
|
||||||
|
- old_val
|
||||||
|
- useremail
|
||||||
|
- bodyshopid
|
||||||
|
- jobid
|
||||||
|
- billid
|
||||||
|
backend_only: false
|
||||||
select_permissions:
|
select_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
@@ -240,12 +269,11 @@ tables:
|
|||||||
- new_val
|
- new_val
|
||||||
- old_val
|
- old_val
|
||||||
- operation
|
- operation
|
||||||
- schemaname
|
|
||||||
- tabname
|
|
||||||
- useremail
|
- useremail
|
||||||
- created
|
- created
|
||||||
|
- billid
|
||||||
- bodyshopid
|
- bodyshopid
|
||||||
- recordid
|
- jobid
|
||||||
filter:
|
filter:
|
||||||
bodyshop:
|
bodyshop:
|
||||||
associations:
|
associations:
|
||||||
@@ -480,6 +508,13 @@ tables:
|
|||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: vendorid
|
foreign_key_constraint_on: vendorid
|
||||||
array_relationships:
|
array_relationships:
|
||||||
|
- name: audit_trails
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: billid
|
||||||
|
table:
|
||||||
|
schema: public
|
||||||
|
name: audit_trail
|
||||||
- name: billlines
|
- name: billlines
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -2248,6 +2283,13 @@ tables:
|
|||||||
table:
|
table:
|
||||||
schema: public
|
schema: public
|
||||||
name: appointments
|
name: appointments
|
||||||
|
- name: audit_trails
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
schema: public
|
||||||
|
name: audit_trail
|
||||||
- name: available_jobs
|
- name: available_jobs
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
|
|||||||
34
package.json
34
package.json
@@ -17,37 +17,37 @@
|
|||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.906.0",
|
"aws-sdk": "^2.951.0",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"cloudinary": "^1.25.0",
|
"cloudinary": "^1.26.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.9.0",
|
||||||
"dotenv": "9.0.2",
|
"dotenv": "10.0.0",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.4",
|
||||||
"firebase-admin": "^9.8.0",
|
"firebase-admin": "^9.11.0",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.1",
|
||||||
"graphql-request": "^3.4.0",
|
"graphql-request": "^3.4.0",
|
||||||
"inline-css": "^3.0.0",
|
"inline-css": "^3.0.0",
|
||||||
"intuit-oauth": "^3.0.2",
|
"intuit-oauth": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-mailjet": "^3.3.1",
|
"node-mailjet": "^3.3.4",
|
||||||
"nodemailer": "^6.6.0",
|
"nodemailer": "^6.6.3",
|
||||||
"phone": "^2.4.20",
|
"phone": "^3.1.2",
|
||||||
"soap": "^0.39.0",
|
"soap": "^0.40.0",
|
||||||
"socket.io": "^4.1.2",
|
"socket.io": "^4.1.3",
|
||||||
"ssh2-sftp-client": "^7.0.0",
|
"ssh2-sftp-client": "^7.0.0",
|
||||||
"stripe": "^8.148.0",
|
"stripe": "^8.164.0",
|
||||||
"twilio": "^3.62.0",
|
"twilio": "^3.66.0",
|
||||||
"xmlbuilder2": "^2.4.1"
|
"xmlbuilder2": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^6.0.2",
|
"concurrently": "^6.2.0",
|
||||||
"eslint": "^7.24.0",
|
"eslint": "^7.31.0",
|
||||||
"eslint-plugin-promise": "^4.3.1",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ app.post(
|
|||||||
fb.validateFirebaseIdToken,
|
fb.validateFirebaseIdToken,
|
||||||
fb.sendNotification
|
fb.sendNotification
|
||||||
);
|
);
|
||||||
|
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
|
||||||
|
|
||||||
//Stripe Processing
|
//Stripe Processing
|
||||||
var stripe = require("./server/stripe/payment");
|
var stripe = require("./server/stripe/payment");
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const ftpSetup = {
|
|||||||
port: process.env.AUTOHOUSE_PORT,
|
port: process.env.AUTOHOUSE_PORT,
|
||||||
username: process.env.AUTOHOUSE_USER,
|
username: process.env.AUTOHOUSE_USER,
|
||||||
password: process.env.AUTOHOUSE_PASSWORD,
|
password: process.env.AUTOHOUSE_PASSWORD,
|
||||||
//debug: console.log,
|
debug: console.log,
|
||||||
|
algorithms: {
|
||||||
|
serverHostKey: ["ssh-rsa", "ssh-dss"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
@@ -127,7 +130,7 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.JSON(error).sendStatus(500);
|
res.status(200).json(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,46 @@ admin.initializeApp({
|
|||||||
|
|
||||||
exports.admin = admin;
|
exports.admin = admin;
|
||||||
|
|
||||||
|
const adminEmail = [
|
||||||
|
"patrick@imex.dev",
|
||||||
|
"patrick@imex.text",
|
||||||
|
"patrick@imex.prod",
|
||||||
|
"patrick@imexsystems.ca",
|
||||||
|
"patrick@thinkimex.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
exports.updateUser = (req, res) => {
|
||||||
|
console.log("USer Requesting", req.user);
|
||||||
|
if (!adminEmail.includes(req.user.email)) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
admin
|
||||||
|
.auth()
|
||||||
|
.updateUser(
|
||||||
|
req.body.uid,
|
||||||
|
req.body.user
|
||||||
|
// {
|
||||||
|
// email: "modifiedUser@example.com",
|
||||||
|
// phoneNumber: "+11234567890",
|
||||||
|
// emailVerified: true,
|
||||||
|
// password: "newPassword",
|
||||||
|
// displayName: "Jane Doe",
|
||||||
|
// photoURL: "http://www.example.com/12345678/photo.png",
|
||||||
|
// disabled: true,
|
||||||
|
// }
|
||||||
|
)
|
||||||
|
.then((userRecord) => {
|
||||||
|
// See the UserRecord reference doc for the contents of userRecord.
|
||||||
|
console.log("Successfully updated user", userRecord.toJSON());
|
||||||
|
res.json(userRecord);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Error updating user:", error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
exports.sendNotification = (req, res) => {
|
exports.sendNotification = (req, res) => {
|
||||||
var registrationToken =
|
var registrationToken =
|
||||||
"fqIWg8ENDFyrRrMWJ1sItR:APA91bHirdZ05Zo66flMlvala97SMXoiQGwP4oCvMwd-vVrSauD_WoNim3kXHGqyP-bzENjkXwA5icyUAReFbeHn6dIaPcbpcsXuY73-eJAXvZiu1gIsrd1BOsnj3dEMT7Q4F6mTPth1";
|
"fqIWg8ENDFyrRrMWJ1sItR:APA91bHirdZ05Zo66flMlvala97SMXoiQGwP4oCvMwd-vVrSauD_WoNim3kXHGqyP-bzENjkXwA5icyUAReFbeHn6dIaPcbpcsXuY73-eJAXvZiu1gIsrd1BOsnj3dEMT7Q4F6mTPth1";
|
||||||
|
|||||||
Reference in New Issue
Block a user