@rensblitz/customer-instant-feedback-app 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -17
- package/dist/customer-instant-feedback.js +1591 -1608
- package/dist/customer-instant-feedback.umd.cjs +15 -15
- package/dist/feedback/FeedbackAuthContext.d.ts +0 -1
- package/dist/feedback/FeedbackReviewPage.d.ts +1 -1
- package/dist/feedback/supabase.d.ts +1 -1
- package/migrations/rls_central.sql +13 -5
- package/package.json +1 -1
- package/supabase/config.toml +5 -0
- package/supabase/functions/README.md +4 -0
- package/supabase/functions/create-reviewer/index.ts +0 -110
- package/supabase/functions/delete-reviewer/index.ts +0 -111
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface FeedbackReviewPageProps {
|
|
2
|
-
/** Host app
|
|
2
|
+
/** Host app's project (public id: `short-code_uuid`). */
|
|
3
3
|
projectId: string;
|
|
4
4
|
}
|
|
5
5
|
export declare function FeedbackReviewPage({ projectId }: FeedbackReviewPageProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -22,7 +22,7 @@ export interface DbFeedbackItem {
|
|
|
22
22
|
comment: string;
|
|
23
23
|
screenshot?: string;
|
|
24
24
|
project_id?: string;
|
|
25
|
-
|
|
25
|
+
stakeholder_id?: string;
|
|
26
26
|
}
|
|
27
27
|
export declare function toDbItem(item: FeedbackItem): DbFeedbackItem;
|
|
28
28
|
export declare function fromDbItem(dbItem: DbFeedbackItem): FeedbackItem;
|
|
@@ -100,10 +100,12 @@ drop policy if exists "Reviewers can insert feedback for their project" on publi
|
|
|
100
100
|
create policy "Reviewers can insert feedback for their project"
|
|
101
101
|
on public.feedback_items for insert
|
|
102
102
|
with check (
|
|
103
|
-
public.reviewer_has_project(project_id)
|
|
103
|
+
public.reviewer_has_project(feedback_items.project_id)
|
|
104
104
|
and exists (
|
|
105
105
|
select 1 from public.reviewers r
|
|
106
|
-
where r.id = reviewer_id
|
|
106
|
+
where r.id = feedback_items.reviewer_id
|
|
107
|
+
and r.user_id = auth.uid()
|
|
108
|
+
and r.project_id = feedback_items.project_id
|
|
107
109
|
)
|
|
108
110
|
);
|
|
109
111
|
|
|
@@ -115,13 +117,17 @@ on public.feedback_items for update
|
|
|
115
117
|
using (
|
|
116
118
|
exists (
|
|
117
119
|
select 1 from public.reviewers r
|
|
118
|
-
where r.id = reviewer_id
|
|
120
|
+
where r.id = feedback_items.reviewer_id
|
|
121
|
+
and r.user_id = auth.uid()
|
|
122
|
+
and r.project_id = feedback_items.project_id
|
|
119
123
|
)
|
|
120
124
|
)
|
|
121
125
|
with check (
|
|
122
126
|
exists (
|
|
123
127
|
select 1 from public.reviewers r
|
|
124
|
-
where r.id = reviewer_id
|
|
128
|
+
where r.id = feedback_items.reviewer_id
|
|
129
|
+
and r.user_id = auth.uid()
|
|
130
|
+
and r.project_id = feedback_items.project_id
|
|
125
131
|
)
|
|
126
132
|
);
|
|
127
133
|
|
|
@@ -131,6 +137,8 @@ on public.feedback_items for delete
|
|
|
131
137
|
using (
|
|
132
138
|
exists (
|
|
133
139
|
select 1 from public.reviewers r
|
|
134
|
-
where r.id = reviewer_id
|
|
140
|
+
where r.id = feedback_items.reviewer_id
|
|
141
|
+
and r.user_id = auth.uid()
|
|
142
|
+
and r.project_id = feedback_items.project_id
|
|
135
143
|
)
|
|
136
144
|
);
|
package/package.json
CHANGED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
2
|
-
|
|
3
|
-
const corsHeaders = {
|
|
4
|
-
'Access-Control-Allow-Origin': '*',
|
|
5
|
-
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
interface CreateReviewerBody {
|
|
9
|
-
email: string;
|
|
10
|
-
password: string;
|
|
11
|
-
projectId: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
Deno.serve(async (req) => {
|
|
15
|
-
if (req.method === 'OPTIONS') {
|
|
16
|
-
return new Response('ok', { headers: corsHeaders });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const authHeader = req.headers.get('Authorization');
|
|
21
|
-
if (!authHeader) {
|
|
22
|
-
return new Response(
|
|
23
|
-
JSON.stringify({ error: 'Missing Authorization header' }),
|
|
24
|
-
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
|
|
29
|
-
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '';
|
|
30
|
-
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
31
|
-
|
|
32
|
-
const anonClient = createClient(supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
|
|
33
|
-
global: { headers: { Authorization: authHeader } },
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const { data: { user: caller } } = await anonClient.auth.getUser();
|
|
37
|
-
if (!caller) {
|
|
38
|
-
return new Response(
|
|
39
|
-
JSON.stringify({ error: 'Not authenticated' }),
|
|
40
|
-
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const { data: adminRow } = await supabase
|
|
45
|
-
.from('admins')
|
|
46
|
-
.select('id')
|
|
47
|
-
.eq('user_id', caller.id)
|
|
48
|
-
.maybeSingle();
|
|
49
|
-
|
|
50
|
-
if (!adminRow) {
|
|
51
|
-
return new Response(
|
|
52
|
-
JSON.stringify({ error: 'Admin access required' }),
|
|
53
|
-
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const body = (await req.json()) as CreateReviewerBody;
|
|
58
|
-
const { email, password, projectId } = body;
|
|
59
|
-
|
|
60
|
-
if (!email?.trim() || !password || !projectId) {
|
|
61
|
-
return new Response(
|
|
62
|
-
JSON.stringify({ error: 'email, password, and projectId are required' }),
|
|
63
|
-
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const { data: newUser, error: createError } = await supabase.auth.admin.createUser({
|
|
68
|
-
email: email.trim(),
|
|
69
|
-
password,
|
|
70
|
-
email_confirm: true,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (createError) {
|
|
74
|
-
return new Response(
|
|
75
|
-
JSON.stringify({ error: createError.message }),
|
|
76
|
-
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!newUser.user) {
|
|
81
|
-
return new Response(
|
|
82
|
-
JSON.stringify({ error: 'Failed to create user' }),
|
|
83
|
-
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const { error: insertError } = await supabase.from('reviewers').insert({
|
|
88
|
-
user_id: newUser.user.id,
|
|
89
|
-
project_id: projectId,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (insertError) {
|
|
93
|
-
await supabase.auth.admin.deleteUser(newUser.user.id);
|
|
94
|
-
return new Response(
|
|
95
|
-
JSON.stringify({ error: insertError.message }),
|
|
96
|
-
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return new Response(
|
|
101
|
-
JSON.stringify({ userId: newUser.user.id }),
|
|
102
|
-
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
103
|
-
);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
return new Response(
|
|
106
|
-
JSON.stringify({ error: err instanceof Error ? err.message : 'Internal error' }),
|
|
107
|
-
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
2
|
-
|
|
3
|
-
const corsHeaders = {
|
|
4
|
-
'Access-Control-Allow-Origin': '*',
|
|
5
|
-
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
interface DeleteReviewerBody {
|
|
9
|
-
reviewerId: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
Deno.serve(async (req) => {
|
|
13
|
-
if (req.method === 'OPTIONS') {
|
|
14
|
-
return new Response('ok', { headers: corsHeaders });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const authHeader = req.headers.get('Authorization');
|
|
19
|
-
if (!authHeader) {
|
|
20
|
-
return new Response(
|
|
21
|
-
JSON.stringify({ error: 'Missing Authorization header' }),
|
|
22
|
-
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
|
|
27
|
-
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '';
|
|
28
|
-
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
29
|
-
|
|
30
|
-
const anonClient = createClient(supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
|
|
31
|
-
global: { headers: { Authorization: authHeader } },
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const { data: { user: caller } } = await anonClient.auth.getUser();
|
|
35
|
-
if (!caller) {
|
|
36
|
-
return new Response(
|
|
37
|
-
JSON.stringify({ error: 'Not authenticated' }),
|
|
38
|
-
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { data: adminRow } = await supabase
|
|
43
|
-
.from('admins')
|
|
44
|
-
.select('id')
|
|
45
|
-
.eq('user_id', caller.id)
|
|
46
|
-
.maybeSingle();
|
|
47
|
-
|
|
48
|
-
if (!adminRow) {
|
|
49
|
-
return new Response(
|
|
50
|
-
JSON.stringify({ error: 'Admin access required' }),
|
|
51
|
-
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const body = (await req.json()) as DeleteReviewerBody;
|
|
56
|
-
const { reviewerId } = body;
|
|
57
|
-
|
|
58
|
-
if (!reviewerId) {
|
|
59
|
-
return new Response(
|
|
60
|
-
JSON.stringify({ error: 'reviewerId is required' }),
|
|
61
|
-
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { data: reviewer, error: fetchError } = await supabase
|
|
66
|
-
.from('reviewers')
|
|
67
|
-
.select('user_id')
|
|
68
|
-
.eq('id', reviewerId)
|
|
69
|
-
.maybeSingle();
|
|
70
|
-
|
|
71
|
-
if (fetchError || !reviewer) {
|
|
72
|
-
return new Response(
|
|
73
|
-
JSON.stringify({ error: fetchError?.message ?? 'Reviewer not found' }),
|
|
74
|
-
{ status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const { error: deleteReviewerError } = await supabase
|
|
79
|
-
.from('reviewers')
|
|
80
|
-
.delete()
|
|
81
|
-
.eq('id', reviewerId);
|
|
82
|
-
|
|
83
|
-
if (deleteReviewerError) {
|
|
84
|
-
return new Response(
|
|
85
|
-
JSON.stringify({ error: deleteReviewerError.message }),
|
|
86
|
-
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const { error: deleteUserError } = await supabase.auth.admin.deleteUser(reviewer.user_id);
|
|
91
|
-
|
|
92
|
-
if (deleteUserError) {
|
|
93
|
-
return new Response(
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
error: 'Reviewer row removed but failed to delete auth user: ' + deleteUserError.message,
|
|
96
|
-
}),
|
|
97
|
-
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return new Response(
|
|
102
|
-
JSON.stringify({ success: true }),
|
|
103
|
-
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
104
|
-
);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
return new Response(
|
|
107
|
-
JSON.stringify({ error: err instanceof Error ? err.message : 'Internal error' }),
|
|
108
|
-
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
});
|