@rensblitz/customer-instant-feedback-app 2.0.4 → 3.0.1
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 +131 -21
- package/dist/customer-instant-feedback.js +4966 -4671
- package/dist/customer-instant-feedback.umd.cjs +43 -28
- package/dist/feedback/FeedbackAuthContext.d.ts +1 -0
- package/dist/feedback/FeedbackReadmePage.d.ts +1 -0
- package/dist/feedback/FeedbackReviewPage.d.ts +1 -0
- package/dist/feedback/supabaseClient.d.ts +17 -3
- package/dist/index.d.ts +2 -1
- package/dist/style.css +1 -1
- package/migrations/create_admin.sql +5 -3
- package/migrations/extend_feedback_items.sql +6 -1
- package/migrations/rls_central.sql +20 -4
- package/package.json +10 -5
- package/supabase/functions/create-reviewer/index.ts +110 -0
- package/supabase/functions/delete-reviewer/index.ts +111 -0
- package/supabase_schema.sql +1 -1
- package/dist/feedback/FeedbackAdminPage.d.ts +0 -1
|
@@ -14,6 +14,7 @@ interface FeedbackAuthContextValue {
|
|
|
14
14
|
loading: boolean;
|
|
15
15
|
hasAccessToProject: (projectId: string) => boolean;
|
|
16
16
|
getReviewerProject: (projectId: string) => ReviewerProject | undefined;
|
|
17
|
+
signOut: () => Promise<void>;
|
|
17
18
|
}
|
|
18
19
|
export declare function FeedbackAuthProvider({ children }: {
|
|
19
20
|
children: React.ReactNode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function FeedbackReadmePage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
interface FeedbackReviewPageProps {
|
|
2
|
+
/** Host app’s default project (public id: `short-code_uuid`). Admins may override via `?project=` on `/feedback`. */
|
|
2
3
|
projectId: string;
|
|
3
4
|
}
|
|
4
5
|
export declare function FeedbackReviewPage({ projectId }: FeedbackReviewPageProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { SupabaseClient
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
/**
|
|
3
|
+
* Configure the Supabase client for the feedback library.
|
|
4
|
+
* Call this at app init, or set env vars in your host project's .env file.
|
|
5
|
+
*
|
|
6
|
+
* Host project .env (Vite):
|
|
7
|
+
* VITE_FEEDBACK_SUPABASE_URL=https://your-project.supabase.co
|
|
8
|
+
* VITE_FEEDBACK_SUPABASE_ANON_KEY=your-anon-key
|
|
9
|
+
*
|
|
10
|
+
* Host project .env (Next.js):
|
|
11
|
+
* NEXT_PUBLIC_FEEDBACK_SUPABASE_URL=https://your-project.supabase.co
|
|
12
|
+
* NEXT_PUBLIC_FEEDBACK_SUPABASE_ANON_KEY=your-anon-key
|
|
13
|
+
*/
|
|
14
|
+
export declare function configureFeedbackClient(config: {
|
|
15
|
+
url: string;
|
|
16
|
+
anonKey: string;
|
|
17
|
+
}): void;
|
|
4
18
|
export declare function getSupabaseClient(): SupabaseClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
export { configureFeedbackClient, getSupabaseClient } from './feedback/supabaseClient';
|
|
1
2
|
export { FeedbackProvider } from './feedback/FeedbackProvider';
|
|
2
3
|
export { FeedbackLoginPage } from './feedback/FeedbackLoginPage';
|
|
3
|
-
export { FeedbackAdminPage } from './feedback/FeedbackAdminPage';
|
|
4
4
|
export { FeedbackReviewPage } from './feedback/FeedbackReviewPage';
|
|
5
|
+
export { FeedbackReadmePage } from './feedback/FeedbackReadmePage';
|
|
5
6
|
export { FeedbackAuthProvider, useFeedbackAuth } from './feedback/FeedbackAuthContext';
|
|
6
7
|
export { generateId } from './feedback/storage';
|
|
7
8
|
export * from './feedback/types';
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.feedback-toggle-container{position:fixed;bottom:20px;right:20px;display:flex;align-items:center;gap:10px;z-index:10000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif}.feedback-user-badge{background:#4b5563;color:#fff;padding:8px 14px;border-radius:20px;font-size:13px;font-weight:500;cursor:pointer;box-shadow:0 4px 12px #0003;transition:background .2s}.feedback-user-badge:hover{background:#ef4444}.feedback-view-all-link{color:#fff;background:#4b5563;padding:8px 14px;border-radius:20px;font-size:13px;font-weight:500;text-decoration:none;transition:background .2s}.feedback-view-all-link:hover{background:#6b7280}.feedback-toggle{position:relative;padding:10px 20px;background:#374151;color:#fff;border:none;border-radius:20px;font-size:14px;font-weight:500;cursor:pointer;box-shadow:0 4px 12px #0003;transition:background .2s}.feedback-toggle:hover{background:#4b5563}.feedback-toggle.active{background:#3b82f6}.feedback-toggle.active:hover{background:#2563eb}.feedback-marker{position:fixed;width:28px;height:28px;margin-left:-14px;margin-top:-14px;border-radius:50%;z-index:10000;box-shadow:0 2px 8px #0000004d;display:flex;align-items:center;justify-content:center;font-family:sans-serif}.feedback-marker.new{background:#ef4444;border:3px solid white;pointer-events:none;animation:feedback-pulse 1s ease-in-out infinite}.feedback-marker.existing{background:#3b82f6;border:3px solid white;cursor:pointer;pointer-events:auto;transition:transform .2s,background .2s}.feedback-marker.existing:hover{transform:scale(1.15);background:#2563eb}.feedback-marker.existing.active{background:#ef4444;transform:scale(1.15)}.feedback-marker-label{color:#fff;font-size:12px;font-weight:700;text-transform:uppercase}@keyframes feedback-pulse{0%,to{transform:scale(1)}50%{transform:scale(1.1)}}.feedback-overlay-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:10002;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif}.feedback-user-modal{background:#fff;border-radius:12px;padding:24px;width:320px;box-shadow:0 8px 30px #0000004d}.feedback-user-modal h3{font-size:18px;font-weight:600;margin-bottom:8px;margin-top:0;color:#111827}.feedback-user-modal p{color:#4b5563;font-size:14px;margin-bottom:16px}.feedback-user-modal input{width:100%;padding:12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;margin-bottom:16px;box-sizing:border-box}.feedback-user-modal input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.feedback-modal{background:#fff;border-radius:8px;padding:20px;width:320px;box-shadow:0 8px 30px #0003;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;box-sizing:border-box;color:#1f2937}.feedback-modal *{box-sizing:border-box}.feedback-modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.feedback-modal-header h3{font-size:16px;font-weight:600;margin:0;color:#111827}.feedback-modal-close{background:none;border:none;font-size:24px;cursor:pointer;color:#9ca3af;line-height:1;padding:0}.feedback-modal-close:hover{color:#4b5563}.feedback-modal-info{background:#f9fafb;padding:12px;border-radius:6px;margin-bottom:16px;font-size:12px}.feedback-modal-info div{margin-bottom:4px}.feedback-modal-info div:last-child{margin-bottom:0}.feedback-modal-field{margin-bottom:16px}.feedback-modal-field label{display:block;font-weight:500;margin-bottom:6px;font-size:13px;color:#374151}.feedback-modal-field select,.feedback-modal-field textarea{width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;font-family:inherit}.feedback-modal-field textarea{resize:vertical;min-height:80px}.feedback-modal-field select:focus,.feedback-modal-field textarea:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.feedback-modal-error{color:#ef4444;font-size:12px;margin-top:6px}.feedback-modal-actions{display:flex;gap:12px;justify-content:flex-end}.feedback-overlay-hint{position:fixed;top:20px;left:50%;transform:translate(-50%);background:#1f2937;color:#fff;padding:10px 20px;border-radius:6px;font-size:14px;z-index:9999;box-shadow:0 4px 12px #0003;font-family:sans-serif}.feedback-view-comment{background:#f9fafb;padding:12px;border-radius:6px;white-space:pre-wrap;color:#374151;line-height:1.5}.feedback-view-hint{font-size:12px;color:#9ca3af;flex:1;display:flex;align-items:center}.feedback-modal button{display:inline-flex;align-items:center;justify-content:center;padding:8px 16px;font-size:14px;font-weight:500;border-radius:6px;border:none;cursor:pointer;transition:background .2s,opacity .2s}.feedback-modal .btn-primary{background:#3b82f6;color:#fff}.feedback-modal .btn-primary:hover{background:#2563eb}.feedback-modal .btn-secondary{background:#f3f4f6;color:#374151;border:1px solid #d1d5db}.feedback-modal .btn-secondary:hover{background:#e5e7eb}.feedback-modal .btn-danger{background:#ef4444;color:#fff}.feedback-modal .btn-danger:hover{background:#dc2626}.feedback-review-page{min-height:100vh;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;padding:24px;box-sizing:border-box}.feedback-review-container{max-width:1200px;margin:0 auto}.feedback-review-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-wrap:wrap;gap:16px}.feedback-review-header h1{margin:0;font-size:24px;font-weight:600;color:#111827}.feedback-review-page .btn-secondary{background:#f3f4f6;color:#374151;border:1px solid #d1d5db;padding:8px 16px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center}.feedback-review-page .btn-secondary:hover{background:#e5e7eb}.feedback-review-empty{color:#6b7280;font-size:16px;padding:48px 24px;text-align:center;background:#fff;border-radius:8px;border:1px dashed #d1d5db}.feedback-review-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:20px}.feedback-review-card{background:#fff;border-radius:8px;box-shadow:0 1px 3px #0000001a;overflow:hidden;display:flex;flex-direction:column}.feedback-review-card-header{display:flex;align-items:center;gap:12px;padding:16px;border-bottom:1px solid #f3f4f6}.feedback-review-avatar{width:40px;height:40px;border-radius:50%;background:#3b82f6;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:16px;flex-shrink:0}.feedback-review-card-meta{flex:1;min-width:0}.feedback-review-card-meta strong{display:block;color:#111827;font-size:14px}.feedback-review-date{font-size:12px;color:#6b7280}.feedback-review-category{font-size:11px;font-weight:600;color:#fff;padding:4px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px}.feedback-review-screenshot{padding:0;background:#f9fafb;border-bottom:1px solid #f3f4f6}.feedback-review-screenshot img{display:block;width:100%;height:auto;max-height:280px;object-fit:contain;cursor:pointer}.feedback-review-comment{padding:16px;color:#374151;font-size:14px;line-height:1.6;white-space:pre-wrap;flex:1}.feedback-review-footer{padding:12px 16px;border-top:1px solid #f3f4f6;display:flex;flex-wrap:wrap;gap:12px;align-items:center;font-size:12px}.feedback-review-link{color:#3b82f6;text-decoration:none;display:inline-flex;align-items:center;gap:4px}.feedback-review-link:hover{text-decoration:underline}.feedback-review-element{color:#6b7280;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-4{bottom:1rem}.bottom-5{bottom:1.25rem}.bottom-\[50px\]{bottom:50px}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-5{right:1.25rem}.top-3{top:.75rem}.top-5{top:1.25rem}.z-\[10000\]{z-index:10000}.z-\[10001\]{z-index:10001}.z-\[9998\]{z-index:9998}.z-\[9999\]{z-index:9999}.m-0{margin:0}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-3\.5{margin-left:-.875rem}.-mt-3\.5{margin-top:-.875rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-4{margin-left:1rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.box-border{box-sizing:border-box}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.max-h-full{max-height:100%}.min-h-20{min-height:5rem}.min-h-\[280px\]{min-height:280px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[200px\]{min-width:200px}.max-w-3xl{max-width:48rem}.max-w-6xl{max-width:72rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-110{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-6{gap:1.5rem}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-\[3px\]{border-width:3px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-none{border-style:none}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/80{background-color:#000c}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-feedback-primary{--tw-bg-opacity: 1;background-color:rgb(106 75 247 / var(--tw-bg-opacity, 1))}.bg-feedback-primary\/20{background-color:#6a4bf733}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-0{padding:0}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-5{padding-left:1.25rem}.pr-24{padding-right:6rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[13px\]{font-size:13px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.text-feedback-primary{--tw-text-opacity: 1;color:rgb(106 75 247 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.no-underline{text-decoration-line:none}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-110:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-feedback-primary:focus{--tw-border-opacity: 1;border-color:rgb(106 75 247 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500\/25:focus{--tw-ring-color: rgb(59 130 246 / .25)}.focus\:ring-feedback-primary\/25:focus{--tw-ring-color: rgb(106 75 247 / .25)}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-offset-\[-2px\]:focus-visible{outline-offset:-2px}.focus-visible\:outline-blue-500:focus-visible{outline-color:#3b82f6}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
-- Steps:
|
|
5
5
|
-- 1. In Supabase Dashboard: Authentication → Users → Add user
|
|
6
6
|
-- Create a user with email and password. Copy the user's UUID.
|
|
7
|
-
-- 2. Replace the UUID below with the actual user UUID.
|
|
7
|
+
-- 2. Replace the placeholder UUID below with the actual user UUID before running.
|
|
8
8
|
-- 3. Run this script in the SQL Editor.
|
|
9
9
|
|
|
10
10
|
insert into public.admins (user_id)
|
|
11
|
-
values ('00000000-0000-0000-0000-000000000000')
|
|
11
|
+
values ('00000000-0000-0000-0000-000000000000') -- Replace with actual user UUID from auth.users
|
|
12
|
+
on conflict (user_id) do nothing;
|
|
12
13
|
|
|
13
14
|
-- For a second admin, run again with another user UUID:
|
|
14
15
|
-- insert into public.admins (user_id)
|
|
15
|
-
-- values ('
|
|
16
|
+
-- values ('00000000-0000-0000-0000-000000000001')
|
|
17
|
+
-- on conflict (user_id) do nothing;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
-- Run this if feedback_items already exists (e.g. from previous per-host setup)
|
|
2
2
|
-- Adds project_id and reviewer_id columns for centralized storage
|
|
3
|
+
-- SKIP on fresh installs; run only when upgrading an existing deployment where feedback_items was already created.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
begin;
|
|
6
|
+
|
|
7
|
+
alter table public.feedback_items add column if not exists project_id uuid references public.projects(id) on delete set null;
|
|
5
8
|
alter table public.feedback_items add column if not exists reviewer_id uuid references public.reviewers(id) on delete set null;
|
|
6
9
|
create index if not exists idx_feedback_items_project_id on public.feedback_items(project_id);
|
|
7
10
|
create index if not exists idx_feedback_items_reviewer_id on public.feedback_items(reviewer_id);
|
|
11
|
+
|
|
12
|
+
commit;
|
|
@@ -36,7 +36,7 @@ on public.companies for all
|
|
|
36
36
|
using (public.is_admin())
|
|
37
37
|
with check (public.is_admin());
|
|
38
38
|
|
|
39
|
-
-- Projects:
|
|
39
|
+
-- Projects: admins manage; reviewers can read projects they are assigned to (for hasAccessToProject)
|
|
40
40
|
alter table public.projects enable row level security;
|
|
41
41
|
|
|
42
42
|
drop policy if exists "Admins can manage projects" on public.projects;
|
|
@@ -45,6 +45,16 @@ on public.projects for all
|
|
|
45
45
|
using (public.is_admin())
|
|
46
46
|
with check (public.is_admin());
|
|
47
47
|
|
|
48
|
+
drop policy if exists "Reviewers can read assigned projects" on public.projects;
|
|
49
|
+
create policy "Reviewers can read assigned projects"
|
|
50
|
+
on public.projects for select
|
|
51
|
+
using (
|
|
52
|
+
exists (
|
|
53
|
+
select 1 from public.reviewers r
|
|
54
|
+
where r.project_id = projects.id and r.user_id = auth.uid()
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
|
|
48
58
|
-- Admins: admin can read only (no insert/update/delete from client)
|
|
49
59
|
alter table public.admins enable row level security;
|
|
50
60
|
|
|
@@ -53,7 +63,7 @@ create policy "Admins can read admins"
|
|
|
53
63
|
on public.admins for select
|
|
54
64
|
using (public.is_admin());
|
|
55
65
|
|
|
56
|
-
-- Reviewers:
|
|
66
|
+
-- Reviewers: admins manage; reviewers can read their own records (needed for hasAccessToProject)
|
|
57
67
|
alter table public.reviewers enable row level security;
|
|
58
68
|
|
|
59
69
|
drop policy if exists "Admins can manage reviewers" on public.reviewers;
|
|
@@ -62,6 +72,11 @@ on public.reviewers for all
|
|
|
62
72
|
using (public.is_admin())
|
|
63
73
|
with check (public.is_admin());
|
|
64
74
|
|
|
75
|
+
drop policy if exists "Reviewers can read own records" on public.reviewers;
|
|
76
|
+
create policy "Reviewers can read own records"
|
|
77
|
+
on public.reviewers for select
|
|
78
|
+
using (user_id = auth.uid());
|
|
79
|
+
|
|
65
80
|
-- Feedback items: reviewers can insert/select for their project; admins can select all
|
|
66
81
|
alter table public.feedback_items enable row level security;
|
|
67
82
|
|
|
@@ -93,19 +108,20 @@ with check (
|
|
|
93
108
|
);
|
|
94
109
|
|
|
95
110
|
-- Reviewers can update/delete their own feedback (reviewer_id matches)
|
|
111
|
+
-- Must also verify project_id so reviewers cannot reassign feedback to projects they don't belong to
|
|
96
112
|
drop policy if exists "Reviewers can update own feedback" on public.feedback_items;
|
|
97
113
|
create policy "Reviewers can update own feedback"
|
|
98
114
|
on public.feedback_items for update
|
|
99
115
|
using (
|
|
100
116
|
exists (
|
|
101
117
|
select 1 from public.reviewers r
|
|
102
|
-
where r.id = reviewer_id and r.user_id = auth.uid()
|
|
118
|
+
where r.id = reviewer_id and r.user_id = auth.uid() and r.project_id = project_id
|
|
103
119
|
)
|
|
104
120
|
)
|
|
105
121
|
with check (
|
|
106
122
|
exists (
|
|
107
123
|
select 1 from public.reviewers r
|
|
108
|
-
where r.id = reviewer_id and r.user_id = auth.uid()
|
|
124
|
+
where r.id = reviewer_id and r.user_id = auth.uid() and r.project_id = project_id
|
|
109
125
|
)
|
|
110
126
|
);
|
|
111
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rensblitz/customer-instant-feedback-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A React component library for collecting user feedback with screenshots and annotations",
|
|
5
5
|
"author": "Rens Blitz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,15 +36,17 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"migrations",
|
|
39
|
+
"supabase",
|
|
39
40
|
"supabase_schema.sql",
|
|
40
41
|
"README.md",
|
|
41
42
|
"LICENSE"
|
|
42
43
|
],
|
|
43
44
|
"scripts": {
|
|
44
|
-
"dev": "vite",
|
|
45
45
|
"build": "tsc && vite build",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
46
|
+
"prepublishOnly": "npm run build && cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE",
|
|
47
|
+
"version:patch": "npm version patch --no-git-tag-version",
|
|
48
|
+
"version:minor": "npm version minor --no-git-tag-version",
|
|
49
|
+
"version:major": "npm version major --no-git-tag-version"
|
|
48
50
|
},
|
|
49
51
|
"peerDependencies": {
|
|
50
52
|
"react": "^18.2.0",
|
|
@@ -53,10 +55,13 @@
|
|
|
53
55
|
"@supabase/supabase-js": "^2.48.1"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
|
-
"html2canvas": "^1.4.1"
|
|
58
|
+
"html2canvas-pro": "^1.4.1"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@supabase/supabase-js": "^2.48.1",
|
|
62
|
+
"autoprefixer": "^10.4.16",
|
|
63
|
+
"postcss": "^8.4.32",
|
|
64
|
+
"tailwindcss": "^3.4.0",
|
|
60
65
|
"@types/react": "^18.2.37",
|
|
61
66
|
"@types/react-dom": "^18.2.15",
|
|
62
67
|
"@vitejs/plugin-react": "^4.2.0",
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
});
|
package/supabase_schema.sql
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
-- This package uses a centralized Supabase project.
|
|
3
3
|
-- Run migrations in this order:
|
|
4
4
|
-- 1. migrations/schema_central.sql
|
|
5
|
-
-- 2. migrations/extend_feedback_items.sql
|
|
5
|
+
-- 2. migrations/extend_feedback_items.sql -- SKIP on fresh installs; run only when upgrading an existing deployment where feedback_items was already created. To verify: if schema_central.sql created feedback_items (table was missing), skip step 2. If you had a pre-existing feedback_items table from a previous per-host setup, run step 2.
|
|
6
6
|
-- 3. migrations/rls_central.sql
|
|
7
7
|
-- 4. migrations/create_admin.sql (for first admin)
|
|
8
8
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function FeedbackAdminPage(): import("react/jsx-runtime").JSX.Element | null;
|