-
Notifications
You must be signed in to change notification settings - Fork 84
Feature/contact syncn.new #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87f83b4
0718718
753b8f8
fa759c7
8f883ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # This workflow uses actions that are not certified by GitHub. | ||
| # They are provided by a third-party and are governed by | ||
| # separate terms of service, privacy policy, and support | ||
| # documentation. | ||
|
|
||
| # This workflow will install Deno then run `deno lint` and `deno test`. | ||
| # For more information see: https://github.com/denoland/setup-deno | ||
|
|
||
| name: Deno | ||
|
|
||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
| branches: ["main"] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Setup repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Deno | ||
| # uses: denoland/setup-deno@v1 | ||
| uses: denoland/setup-deno@61fe2df320078202e33d7d5ad347e7dcfa0e8f31 # v1.1.2 | ||
| with: | ||
| deno-version: v1.x | ||
|
|
||
| # Uncomment this step to verify the use of 'deno fmt' on each commit. | ||
| # - name: Verify formatting | ||
| # run: deno fmt --check | ||
|
|
||
| - name: Run linter | ||
| run: deno lint | ||
|
|
||
| - name: Run tests | ||
| run: deno test -A | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,13 +17,20 @@ | |||||||||||||||
| ], | ||||||||||||||||
| "ios": { | ||||||||||||||||
| "supportsTablet": true, | ||||||||||||||||
| "bundleIdentifier": "com.supersimon.whatsapp" | ||||||||||||||||
| "bundleIdentifier": "com.supersimon.whatsapp", | ||||||||||||||||
| "infoPlist": { | ||||||||||||||||
| "NSContactsUsageDescription": "We need access to your contacts to sync them in the app" | ||||||||||||||||
| } | ||||||||||||||||
| }, | ||||||||||||||||
| "android": { | ||||||||||||||||
| "adaptiveIcon": { | ||||||||||||||||
| "foregroundImage": "./assets/images/adaptive-icon.png", | ||||||||||||||||
| "backgroundColor": "#ffffff" | ||||||||||||||||
| }, | ||||||||||||||||
| "permissions": [ | ||||||||||||||||
| "android.permission.READ_CONTACTS", | ||||||||||||||||
| "android.permission.READ_PHONE_STATE" | ||||||||||||||||
| ], | ||||||||||||||||
|
Comment on lines
+30
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove Contact syncing needs 🔒 Proposed permission trim "permissions": [
- "android.permission.READ_CONTACTS",
- "android.permission.READ_PHONE_STATE"
+ "android.permission.READ_CONTACTS"
],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| "package": "com.supersimon.whatsapp" | ||||||||||||||||
| }, | ||||||||||||||||
| "web": { | ||||||||||||||||
|
|
@@ -34,7 +41,13 @@ | |||||||||||||||
| "plugins": [ | ||||||||||||||||
| "expo-router", | ||||||||||||||||
| "expo-secure-store", | ||||||||||||||||
| "expo-font" | ||||||||||||||||
| "expo-font", | ||||||||||||||||
| [ | ||||||||||||||||
| "expo-contacts", | ||||||||||||||||
| { | ||||||||||||||||
| "contactsPermission": "We need access to your contacts to sync them in the app" | ||||||||||||||||
| } | ||||||||||||||||
| ] | ||||||||||||||||
| ], | ||||||||||||||||
| "experiments": { | ||||||||||||||||
| "typedRoutes": true, | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,195 @@ | ||||||||||||||||||||||||||||||||
| import React, { useEffect } from 'react'; | ||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||
| View, | ||||||||||||||||||||||||||||||||
| Text, | ||||||||||||||||||||||||||||||||
| TouchableOpacity, | ||||||||||||||||||||||||||||||||
| ActivityIndicator, | ||||||||||||||||||||||||||||||||
| StyleSheet, | ||||||||||||||||||||||||||||||||
| RefreshControl, | ||||||||||||||||||||||||||||||||
| SectionList, | ||||||||||||||||||||||||||||||||
| Image, | ||||||||||||||||||||||||||||||||
| } from 'react-native'; | ||||||||||||||||||||||||||||||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context'; | ||||||||||||||||||||||||||||||||
| import { MaterialCommunityIcons } from '@expo/vector-icons'; | ||||||||||||||||||||||||||||||||
| import { router } from 'expo-router'; | ||||||||||||||||||||||||||||||||
| import { useSyncContacts, type SyncedContact } from '@/hooks/useSyncContacts'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const NewChatScreen = () => { | ||||||||||||||||||||||||||||||||
| const insets = useSafeAreaInsets(); | ||||||||||||||||||||||||||||||||
| const { contacts, loading, error, permissionDenied, syncContacts, refetch } = useSyncContacts(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||
| syncContacts(); | ||||||||||||||||||||||||||||||||
| }, [syncContacts]); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const getSectionedContacts = () => { | ||||||||||||||||||||||||||||||||
| if (contacts.length === 0) return []; | ||||||||||||||||||||||||||||||||
| const sections: { title: string; data: SyncedContact[] }[] = []; | ||||||||||||||||||||||||||||||||
| const contactsByLetter: { [key: string]: SyncedContact[] } = {}; | ||||||||||||||||||||||||||||||||
| contacts.forEach((contact) => { | ||||||||||||||||||||||||||||||||
| const firstLetter = (contact.device_contact_name || 'A')[0].toUpperCase(); | ||||||||||||||||||||||||||||||||
| if (!contactsByLetter[firstLetter]) { | ||||||||||||||||||||||||||||||||
| contactsByLetter[firstLetter] = []; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| contactsByLetter[firstLetter].push(contact); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| Object.keys(contactsByLetter) | ||||||||||||||||||||||||||||||||
| .sort() | ||||||||||||||||||||||||||||||||
| .forEach((letter) => { | ||||||||||||||||||||||||||||||||
| sections.push({ title: letter, data: contactsByLetter[letter] }); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| return sections; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const handleContactPress = (contact: SyncedContact) => { | ||||||||||||||||||||||||||||||||
| router.push({ | ||||||||||||||||||||||||||||||||
| pathname: '/chat/[id]', | ||||||||||||||||||||||||||||||||
| params: { id: contact.user_id }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const renderContactItem = ({ item }: { item: SyncedContact }) => ( | ||||||||||||||||||||||||||||||||
| <TouchableOpacity | ||||||||||||||||||||||||||||||||
| style={styles.contactItem} | ||||||||||||||||||||||||||||||||
| onPress={() => handleContactPress(item)} | ||||||||||||||||||||||||||||||||
| activeOpacity={0.7} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <View style={styles.avatarContainer}> | ||||||||||||||||||||||||||||||||
| {item.avatar_url ? ( | ||||||||||||||||||||||||||||||||
| <Image source={{ uri: item.avatar_url }} style={styles.avatar} /> | ||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||
| <View style={[styles.avatar, styles.avatarPlaceholder]}> | ||||||||||||||||||||||||||||||||
| <Text style={styles.avatarText}>{item.device_contact_name[0].toUpperCase()}</Text> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard the avatar initial for empty contact names. This path indexes 🛡️ Proposed guard ) : (
<View style={[styles.avatar, styles.avatarPlaceholder]}>
- <Text style={styles.avatarText}>{item.device_contact_name[0].toUpperCase()}</Text>
+ <Text style={styles.avatarText}>
+ {(item.device_contact_name?.trim()?.[0] ?? '#').toUpperCase()}
+ </Text>
</View>
)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| <View style={styles.contactInfo}> | ||||||||||||||||||||||||||||||||
| <Text style={styles.contactName} numberOfLines={1}> | ||||||||||||||||||||||||||||||||
| {item.device_contact_name} | ||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||
| <Text style={styles.contactPhone} numberOfLines={1}> | ||||||||||||||||||||||||||||||||
| {item.phone} | ||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| <MaterialCommunityIcons name="chevron-right" size={24} color="#ccc" /> | ||||||||||||||||||||||||||||||||
| </TouchableOpacity> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const renderSectionHeader = ({ section }: { section: any }) => ( | ||||||||||||||||||||||||||||||||
| <View style={styles.sectionHeader}> | ||||||||||||||||||||||||||||||||
| <Text style={styles.sectionTitle}>{section.title}</Text> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const renderEmptyState = () => ( | ||||||||||||||||||||||||||||||||
| <View style={styles.emptyContainer}> | ||||||||||||||||||||||||||||||||
| {permissionDenied ? ( | ||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||
| <MaterialCommunityIcons name="phone-off" size={64} color="#ccc" /> | ||||||||||||||||||||||||||||||||
| <Text style={styles.emptyTitle}>Permission Denied</Text> | ||||||||||||||||||||||||||||||||
| <Text style={styles.emptySubtitle}> | ||||||||||||||||||||||||||||||||
| Please grant access to contacts in app settings | ||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||
| ) : contacts.length === 0 && !loading ? ( | ||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||
| <MaterialCommunityIcons name="contacts" size={64} color="#ccc" /> | ||||||||||||||||||||||||||||||||
| <Text style={styles.emptyTitle}>No Contacts</Text> | ||||||||||||||||||||||||||||||||
| <Text style={styles.emptySubtitle}> | ||||||||||||||||||||||||||||||||
| No matching contacts found | ||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const renderErrorState = () => ( | ||||||||||||||||||||||||||||||||
| <View style={styles.errorContainer}> | ||||||||||||||||||||||||||||||||
| <MaterialCommunityIcons name="alert-circle" size={64} color="#ff6b6b" /> | ||||||||||||||||||||||||||||||||
| <Text style={styles.errorTitle}>Error</Text> | ||||||||||||||||||||||||||||||||
| <Text style={styles.errorMessage}>{error}</Text> | ||||||||||||||||||||||||||||||||
| <TouchableOpacity style={styles.retryButton} onPress={syncContacts}> | ||||||||||||||||||||||||||||||||
| <Text style={styles.retryButtonText}>Try Again</Text> | ||||||||||||||||||||||||||||||||
| </TouchableOpacity> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const sections = getSectionedContacts(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||
| <View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}> | ||||||||||||||||||||||||||||||||
| <View style={styles.header}> | ||||||||||||||||||||||||||||||||
| <TouchableOpacity onPress={() => router.back()}> | ||||||||||||||||||||||||||||||||
| <MaterialCommunityIcons name="arrow-left" size={24} color="#000" /> | ||||||||||||||||||||||||||||||||
| </TouchableOpacity> | ||||||||||||||||||||||||||||||||
| <Text style={styles.headerTitle}>New Chat</Text> | ||||||||||||||||||||||||||||||||
| <View style={{ width: 24 }} /> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| {loading && sections.length === 0 ? ( | ||||||||||||||||||||||||||||||||
| <View style={styles.loadingContainer}> | ||||||||||||||||||||||||||||||||
| <ActivityIndicator size="large" color="#075E54" /> | ||||||||||||||||||||||||||||||||
| <Text style={styles.loadingText}>Syncing contacts...</Text> | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| ) : error && sections.length === 0 ? ( | ||||||||||||||||||||||||||||||||
| renderErrorState() | ||||||||||||||||||||||||||||||||
| ) : sections.length === 0 ? ( | ||||||||||||||||||||||||||||||||
| renderEmptyState() | ||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||
| <SectionList | ||||||||||||||||||||||||||||||||
| sections={sections} | ||||||||||||||||||||||||||||||||
| keyExtractor={(item) => item.id} | ||||||||||||||||||||||||||||||||
| renderItem={renderContactItem} | ||||||||||||||||||||||||||||||||
| renderSectionHeader={renderSectionHeader} | ||||||||||||||||||||||||||||||||
| stickySectionHeadersEnabled={true} | ||||||||||||||||||||||||||||||||
| refreshControl={<RefreshControl refreshing={loading} onRefresh={refetch} tintColor="#075E54" />} | ||||||||||||||||||||||||||||||||
| contentContainerStyle={styles.listContent} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
| </View> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const styles = StyleSheet.create({ | ||||||||||||||||||||||||||||||||
| container: { flex: 1, backgroundColor: '#fff' }, | ||||||||||||||||||||||||||||||||
| header: { | ||||||||||||||||||||||||||||||||
| flexDirection: 'row', | ||||||||||||||||||||||||||||||||
| alignItems: 'center', | ||||||||||||||||||||||||||||||||
| justifyContent: 'space-between', | ||||||||||||||||||||||||||||||||
| paddingHorizontal: 16, | ||||||||||||||||||||||||||||||||
| paddingVertical: 12, | ||||||||||||||||||||||||||||||||
| borderBottomWidth: 1, | ||||||||||||||||||||||||||||||||
| borderBottomColor: '#f0f0f0', | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| headerTitle: { fontSize: 18, fontWeight: '600', color: '#000' }, | ||||||||||||||||||||||||||||||||
| loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, | ||||||||||||||||||||||||||||||||
| loadingText: { marginTop: 12, fontSize: 16, color: '#666', fontWeight: '500' }, | ||||||||||||||||||||||||||||||||
| listContent: { paddingBottom: 20 }, | ||||||||||||||||||||||||||||||||
| sectionHeader: { paddingHorizontal: 16, paddingVertical: 8, backgroundColor: '#f5f5f5' }, | ||||||||||||||||||||||||||||||||
| sectionTitle: { fontSize: 14, fontWeight: '600', color: '#666' }, | ||||||||||||||||||||||||||||||||
| contactItem: { | ||||||||||||||||||||||||||||||||
| flexDirection: 'row', | ||||||||||||||||||||||||||||||||
| alignItems: 'center', | ||||||||||||||||||||||||||||||||
| paddingHorizontal: 16, | ||||||||||||||||||||||||||||||||
| paddingVertical: 12, | ||||||||||||||||||||||||||||||||
| borderBottomWidth: 1, | ||||||||||||||||||||||||||||||||
| borderBottomColor: '#f0f0f0', | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| avatarContainer: { marginRight: 12 }, | ||||||||||||||||||||||||||||||||
| avatar: { width: 50, height: 50, borderRadius: 25, backgroundColor: '#e0e0e0' }, | ||||||||||||||||||||||||||||||||
| avatarPlaceholder: { justifyContent: 'center', alignItems: 'center', backgroundColor: '#075E54' }, | ||||||||||||||||||||||||||||||||
| avatarText: { fontSize: 20, fontWeight: '600', color: '#fff' }, | ||||||||||||||||||||||||||||||||
| contactInfo: { flex: 1 }, | ||||||||||||||||||||||||||||||||
| contactName: { fontSize: 16, fontWeight: '500', color: '#000', marginBottom: 4 }, | ||||||||||||||||||||||||||||||||
| contactPhone: { fontSize: 14, color: '#999' }, | ||||||||||||||||||||||||||||||||
| emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 32 }, | ||||||||||||||||||||||||||||||||
| emptyTitle: { fontSize: 18, fontWeight: '600', color: '#000', marginTop: 16 }, | ||||||||||||||||||||||||||||||||
| emptySubtitle: { fontSize: 14, color: '#999', marginTop: 8, textAlign: 'center' }, | ||||||||||||||||||||||||||||||||
| errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 32 }, | ||||||||||||||||||||||||||||||||
| errorTitle: { fontSize: 18, fontWeight: '600', color: '#ff6b6b', marginTop: 16 }, | ||||||||||||||||||||||||||||||||
| errorMessage: { fontSize: 14, color: '#666', marginTop: 8, textAlign: 'center' }, | ||||||||||||||||||||||||||||||||
| retryButton: { marginTop: 20, paddingHorizontal: 24, paddingVertical: 12, backgroundColor: '#075E54', borderRadius: 8 }, | ||||||||||||||||||||||||||||||||
| retryButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export default NewChatScreen; | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,8 +16,10 @@ | |
| "@clerk/clerk-expo": "^0.20.1", | ||
| "@expo/vector-icons": "^14.0.0", | ||
| "@react-navigation/native": "^6.0.2", | ||
| "@react-native-async-storage/async-storage": "^1.21.0", | ||
| "date-fns": "^3.3.1", | ||
| "expo": "^50.0.2", | ||
| "expo-contacts": "^14.5.0", | ||
|
Comment on lines
21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Expo SDK 50 compatible expo-contacts version: use expo-contacts sdk-50 tag version 12.8.2. The npm registry dist-tags for expo-contacts include “sdk-50”: “12.8.2” [1]. Does Expo recommend ~-pinned versions for Expo SDK packages in package.json? Expo’s guidance is to use expo install (and expo install --fix / --check) to realign dependency versions with the versions expected for your current Expo SDK, rather than manually guessing version ranges [2]. The dependency-reconciliation doc explains tested ranges (including that ~1.2.3 allows v1.2.3 plus a greater patch version) but it does not recommend “always use ~” specifically for Expo SDK packages; instead it emphasizes using the CLI to keep your versions aligned to the SDK expectations [2]. Citations:
Align
🤖 Prompt for AI Agents |
||
| "expo-dev-client": "~3.3.6", | ||
| "expo-font": "~11.10.2", | ||
| "expo-haptics": "~12.8.1", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| -- Create profiles table with phone field for contact syncing | ||
| CREATE TABLE IF NOT EXISTS public.profiles ( | ||
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | ||
| user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE, | ||
| phone TEXT NOT NULL UNIQUE, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize phone numbers before enforcing uniqueness.
🤖 Prompt for AI Agents |
||
| full_name TEXT NOT NULL, | ||
| avatar_url TEXT, | ||
| email TEXT, | ||
| created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()), | ||
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) | ||
| ); | ||
|
|
||
| -- Create indexes for faster queries | ||
| CREATE INDEX IF NOT EXISTS idx_profiles_phone ON public.profiles(phone); | ||
| CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON public.profiles(user_id); | ||
|
|
||
| -- Enable Row Level Security | ||
| ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| -- Policy: Users can view all profiles | ||
| DROP POLICY IF EXISTS "Users can view profiles" ON public.profiles; | ||
| CREATE POLICY "Users can view profiles" | ||
| ON public.profiles FOR SELECT | ||
| USING (auth.uid() IS NOT NULL); | ||
|
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restrict profile reads instead of exposing the whole table.
🤖 Prompt for AI Agents |
||
|
|
||
| -- Policy: Users can update their own profile | ||
| DROP POLICY IF EXISTS "Users can update their own profile" ON public.profiles; | ||
| CREATE POLICY "Users can update their own profile" | ||
| ON public.profiles FOR UPDATE | ||
| USING (auth.uid() = user_id) | ||
| WITH CHECK (auth.uid() = user_id); | ||
|
|
||
| -- Policy: Users can insert their own profile | ||
| DROP POLICY IF EXISTS "Users can insert their own profile" ON public.profiles; | ||
| CREATE POLICY "Users can insert their own profile" | ||
| ON public.profiles FOR INSERT | ||
| WITH CHECK (auth.uid() = user_id); | ||
|
|
||
| -- Function to automatically update updated_at timestamp | ||
| CREATE OR REPLACE FUNCTION update_updated_at_column() | ||
| RETURNS TRIGGER AS $$ | ||
| BEGIN | ||
| NEW.updated_at = CURRENT_TIMESTAMP; | ||
| RETURN NEW; | ||
| END; | ||
| $$ LANGUAGE plpgsql; | ||
|
|
||
| -- Trigger to call the update function | ||
| DROP TRIGGER IF EXISTS update_profiles_updated_at ON public.profiles; | ||
| CREATE TRIGGER update_profiles_updated_at | ||
| BEFORE UPDATE ON public.profiles | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION update_updated_at_column(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pin checkout action to hash and disable credential persistence.
For security consistency with the pinned
denoland/setup-denoaction,actions/checkoutshould also be pinned to a commit hash rather than a tag. Additionally, setpersist-credentials: falseto prevent the GITHUB_TOKEN from being exposed in subsequent steps where it's not needed.🔒 Proposed security hardening
As per static analysis hints, the unpinned action reference and missing credential persistence settings were flagged by the zizmor tool.
📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 25-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents