MED-147: add doctor actions logging (#59)
* MED-147: add doctor actions logging * enum casing
This commit is contained in:
@@ -5,7 +5,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import {
|
import {
|
||||||
PAGE_VIEW_ACTION,
|
PageViewAction,
|
||||||
createPageViewLog,
|
createPageViewLog,
|
||||||
} from '~/lib/services/audit/pageView.service';
|
} from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ async function MembershipConfirmation() {
|
|||||||
}
|
}
|
||||||
await createPageViewLog({
|
await createPageViewLog({
|
||||||
accountId: user.id,
|
accountId: user.id,
|
||||||
action: PAGE_VIEW_ACTION.REGISTRATION_SUCCESS,
|
action: PageViewAction.REGISTRATION_SUCCESS,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <MembershipConfirmationNotification userId={user.id} />;
|
return <MembershipConfirmationNotification userId={user.id} />;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { cache } from 'react';
|
|||||||
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
|
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DoctorPageViewAction,
|
||||||
|
createDoctorPageViewLog,
|
||||||
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import AnalysisView from '../../_components/analysis-view';
|
import AnalysisView from '../../_components/analysis-view';
|
||||||
import { DoctorGuard } from '../../_components/doctor-guard';
|
import { DoctorGuard } from '../../_components/doctor-guard';
|
||||||
|
|
||||||
@@ -20,6 +25,14 @@ async function AnalysisPage({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (analysisResultDetails) {
|
||||||
|
await createDoctorPageViewLog({
|
||||||
|
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
|
recordKey: id,
|
||||||
|
dataOwnerUserId: analysisResultDetails.patient.userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
import {
|
||||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
DoctorPageViewAction,
|
||||||
|
createDoctorPageViewLog,
|
||||||
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import { DoctorGuard } from '../_components/doctor-guard';
|
import { DoctorGuard } from '../_components/doctor-guard';
|
||||||
|
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||||
|
|
||||||
async function CompletedJobsPage() {
|
async function CompletedJobsPage() {
|
||||||
|
await createDoctorPageViewLog({
|
||||||
|
action: DoctorPageViewAction.VIEW_DONE_JOBS,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
|
||||||
|
import {
|
||||||
|
DoctorPageViewAction,
|
||||||
|
createDoctorPageViewLog,
|
||||||
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import { DoctorGuard } from '../_components/doctor-guard';
|
import { DoctorGuard } from '../_components/doctor-guard';
|
||||||
|
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||||
|
|
||||||
async function MyReviewsPage() {
|
async function MyReviewsPage() {
|
||||||
|
await createDoctorPageViewLog({
|
||||||
|
action: DoctorPageViewAction.VIEW_OWN_JOBS,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DoctorPageViewAction,
|
||||||
|
createDoctorPageViewLog,
|
||||||
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import { DoctorGuard } from '../_components/doctor-guard';
|
import { DoctorGuard } from '../_components/doctor-guard';
|
||||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||||
|
|
||||||
async function OpenJobsPage() {
|
async function OpenJobsPage() {
|
||||||
|
await createDoctorPageViewLog({
|
||||||
|
action: DoctorPageViewAction.VIEW_OPEN_JOBS,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DoctorPageViewAction,
|
||||||
|
createDoctorPageViewLog,
|
||||||
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import Dashboard from './_components/doctor-dashboard';
|
import Dashboard from './_components/doctor-dashboard';
|
||||||
import { DoctorGuard } from './_components/doctor-guard';
|
import { DoctorGuard } from './_components/doctor-guard';
|
||||||
|
|
||||||
async function DoctorPage() {
|
async function DoctorPage() {
|
||||||
|
await createDoctorPageViewLog({
|
||||||
|
action: DoctorPageViewAction.VIEW_DASHBOARD,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { pathsConfig } from '@kit/shared/config';
|
|||||||
|
|
||||||
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||||
import {
|
import {
|
||||||
PAGE_VIEW_ACTION,
|
PageViewAction,
|
||||||
createPageViewLog,
|
createPageViewLog,
|
||||||
} from '~/lib/services/audit/pageView.service';
|
} from '~/lib/services/audit/pageView.service';
|
||||||
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
|
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
|
||||||
@@ -50,7 +50,7 @@ async function AnalysisResultsPage() {
|
|||||||
|
|
||||||
await createPageViewLog({
|
await createPageViewLog({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
|
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
|
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||||
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
import OrderAnalysesCards from '../../_components/order-analyses-cards';
|
||||||
import { createPageViewLog, PAGE_VIEW_ACTION } from '~/lib/services/audit/pageView.service';
|
import { createPageViewLog, PageViewAction } from '~/lib/services/audit/pageView.service';
|
||||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
@@ -27,7 +27,7 @@ async function OrderAnalysisPage() {
|
|||||||
|
|
||||||
await createPageViewLog({
|
await createPageViewLog({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
action: PAGE_VIEW_ACTION.VIEW_ORDER_ANALYSIS,
|
action: PageViewAction.VIEW_ORDER_ANALYSIS,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { PageBody } from '@kit/ui/page';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
import {
|
import {
|
||||||
PAGE_VIEW_ACTION,
|
PageViewAction,
|
||||||
createPageViewLog,
|
createPageViewLog,
|
||||||
} from '~/lib/services/audit/pageView.service';
|
} from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
|||||||
use(
|
use(
|
||||||
createPageViewLog({
|
createPageViewLog({
|
||||||
accountId: teamAccount.id,
|
accountId: teamAccount.id,
|
||||||
action: PAGE_VIEW_ACTION.VIEW_TEAM_ACCOUNT_DASHBOARD,
|
action: PageViewAction.VIEW_TEAM_ACCOUNT_DASHBOARD,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
46
lib/services/audit/doctorPageView.service.ts
Normal file
46
lib/services/audit/doctorPageView.service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export enum DoctorPageViewAction {
|
||||||
|
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
||||||
|
VIEW_DASHBOARD = 'VIEW_DASHBOARD',
|
||||||
|
VIEW_OPEN_JOBS = 'VIEW_OPEN_JOBS',
|
||||||
|
VIEW_OWN_JOBS = 'VIEW_OWN_JOBS',
|
||||||
|
VIEW_DONE_JOBS = 'VIEW_DONE_JOBS',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDoctorPageViewLog = async ({
|
||||||
|
action,
|
||||||
|
recordKey,
|
||||||
|
dataOwnerUserId,
|
||||||
|
}: {
|
||||||
|
action: DoctorPageViewAction;
|
||||||
|
recordKey?: string;
|
||||||
|
dataOwnerUserId?: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const supabase = getSupabaseServerClient();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
error: userError,
|
||||||
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (userError || !user) {
|
||||||
|
console.error('No authenticated user found; skipping audit insert');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.schema('audit')
|
||||||
|
.from('doctor_page_views')
|
||||||
|
.insert({
|
||||||
|
viewer_user_id: user.id,
|
||||||
|
data_owner_user_id: dataOwnerUserId,
|
||||||
|
viewed_record_key: recordKey,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
.throwOnError();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert doctor page view log', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
export enum PAGE_VIEW_ACTION {
|
export enum PageViewAction {
|
||||||
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
||||||
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
||||||
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
||||||
@@ -12,7 +12,7 @@ export const createPageViewLog = async ({
|
|||||||
action,
|
action,
|
||||||
}: {
|
}: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
action: PAGE_VIEW_ACTION;
|
action: PageViewAction;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const selectJobAction = doctorAction(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to select job', e);
|
logger.error('Failed to select job', e);
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
|
revalidateDoctorAnalysis();
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
reason:
|
reason:
|
||||||
|
|||||||
@@ -42,6 +42,33 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
|
doctor_page_views: {
|
||||||
|
Row: {
|
||||||
|
action: Database["audit"]["Enums"]["doctor_page_view_action"]
|
||||||
|
created_at: string
|
||||||
|
data_owner_user_id: string | null
|
||||||
|
id: number
|
||||||
|
viewed_record_key: string | null
|
||||||
|
viewer_user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
action: Database["audit"]["Enums"]["doctor_page_view_action"]
|
||||||
|
created_at?: string
|
||||||
|
data_owner_user_id?: string | null
|
||||||
|
id?: number
|
||||||
|
viewed_record_key?: string | null
|
||||||
|
viewer_user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
action?: Database["audit"]["Enums"]["doctor_page_view_action"]
|
||||||
|
created_at?: string
|
||||||
|
data_owner_user_id?: string | null
|
||||||
|
id?: number
|
||||||
|
viewed_record_key?: string | null
|
||||||
|
viewer_user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
log_entries: {
|
log_entries: {
|
||||||
Row: {
|
Row: {
|
||||||
changed_at: string
|
changed_at: string
|
||||||
@@ -182,6 +209,12 @@ export type Database = {
|
|||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
|
doctor_page_view_action:
|
||||||
|
| "VIEW_ANALYSIS_RESULTS"
|
||||||
|
| "VIEW_DASHBOARD"
|
||||||
|
| "VIEW_OPEN_JOBS"
|
||||||
|
| "VIEW_OWN_JOBS"
|
||||||
|
| "VIEW_DONE_JOBS"
|
||||||
request_status: "SUCCESS" | "FAIL"
|
request_status: "SUCCESS" | "FAIL"
|
||||||
sync_status: "SUCCESS" | "FAIL"
|
sync_status: "SUCCESS" | "FAIL"
|
||||||
}
|
}
|
||||||
@@ -1778,7 +1811,6 @@ export type Database = {
|
|||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
personal_code: string
|
|
||||||
picture_url: string
|
picture_url: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -7887,6 +7919,13 @@ export type CompositeTypes<
|
|||||||
export const Constants = {
|
export const Constants = {
|
||||||
audit: {
|
audit: {
|
||||||
Enums: {
|
Enums: {
|
||||||
|
doctor_page_view_action: [
|
||||||
|
"VIEW_ANALYSIS_RESULTS",
|
||||||
|
"VIEW_DASHBOARD",
|
||||||
|
"VIEW_OPEN_JOBS",
|
||||||
|
"VIEW_OWN_JOBS",
|
||||||
|
"VIEW_DONE_JOBS",
|
||||||
|
],
|
||||||
request_status: ["SUCCESS", "FAIL"],
|
request_status: ["SUCCESS", "FAIL"],
|
||||||
sync_status: ["SUCCESS", "FAIL"],
|
sync_status: ["SUCCESS", "FAIL"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
create type audit.doctor_page_view_action as ENUM('VIEW_ANALYSIS_RESULTS','VIEW_DASHBOARD','VIEW_OPEN_JOBS','VIEW_OWN_JOBS','VIEW_DONE_JOBS');
|
||||||
|
|
||||||
|
create table audit.doctor_page_views (
|
||||||
|
"id" bigint generated by default as identity not null,
|
||||||
|
"viewer_user_id" uuid references auth.users (id) not null,
|
||||||
|
"data_owner_user_id" uuid references auth.users (id),
|
||||||
|
"viewed_record_key" text,
|
||||||
|
"action" audit.doctor_page_view_action not null,
|
||||||
|
"created_at" timestamp with time zone not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
grant usage on schema audit to authenticated;
|
||||||
|
grant select, insert, update, delete on table audit.doctor_page_views to authenticated;
|
||||||
|
|
||||||
|
alter table "audit"."page_views" enable row level security;
|
||||||
|
|
||||||
|
create policy "insert_own"
|
||||||
|
on audit.doctor_page_views
|
||||||
|
as permissive
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (auth.uid() = viewer_user_id);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION medreport.log_doctor_analysis_feedback_changes()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
current_user_id uuid;
|
||||||
|
current_user_role text;
|
||||||
|
operation_type text;
|
||||||
|
BEGIN
|
||||||
|
begin
|
||||||
|
current_user_id := auth.uid();
|
||||||
|
current_user_role := auth.jwt() ->> 'role';
|
||||||
|
end;
|
||||||
|
IF (OLD.doctor_user_id IS DISTINCT FROM NEW.doctor_user_id) THEN
|
||||||
|
operation_type := CASE
|
||||||
|
WHEN NEW.doctor_user_id IS NULL THEN 'UNSELECT_JOB'
|
||||||
|
ELSE 'SELECT_JOB'
|
||||||
|
END;
|
||||||
|
ELSIF (NEW.status = 'DRAFT' OR (OLD.status IS DISTINCT FROM NEW.status AND NEW.status = 'COMPLETED')) THEN
|
||||||
|
operation_type := CASE
|
||||||
|
WHEN NEW.status = 'DRAFT' THEN 'UPDATED_DRAFT'
|
||||||
|
WHEN NEW.status = 'COMPLETED' THEN 'PUBLISHED_SUMMARY'
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
ELSE
|
||||||
|
operation_type := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF operation_type IS NOT NULL THEN
|
||||||
|
INSERT INTO audit.log_entries (
|
||||||
|
schema_name,
|
||||||
|
table_name,
|
||||||
|
record_key,
|
||||||
|
operation,
|
||||||
|
changed_by,
|
||||||
|
changed_by_role,
|
||||||
|
changed_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'medreport',
|
||||||
|
'doctor_analysis_feedback',
|
||||||
|
NEW.id::text,
|
||||||
|
operation_type,
|
||||||
|
current_user_id,
|
||||||
|
current_user_role,
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS doctor_analysis_feedback_audit_trigger ON medreport.doctor_analysis_feedback;
|
||||||
|
|
||||||
|
CREATE TRIGGER doctor_analysis_feedback_audit_trigger
|
||||||
|
AFTER UPDATE ON medreport.doctor_analysis_feedback
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION medreport.log_doctor_analysis_feedback_changes();
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION medreport.log_doctor_analysis_feedback_changes() TO authenticated;
|
||||||
|
|
||||||
|
alter table audit.doctor_page_views enable row level security;
|
||||||
Reference in New Issue
Block a user