MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。
此示例提供了使用 MemFire Cloud 和 React Native构建简单用户管理应用程序(从头开始)的步骤。这包括:
MemFire Cloud云数据库:用于存储用户数据的 MemFireDB数据库。
MemFire Cloud用户验证:用户可以使用邮箱密码登录。
MemFire Cloud存储:用户可以上传照片。
行级安全策略:数据受到保护,因此个人只能访问自己的数据。
即时API:创建数据库表时会自动生成 API。
在本指南结束时,您将拥有一个允许用户登录和更新一些基本个人资料详细信息的应用程序:
创建应用
目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。
登录https://cloud.memfiredb.com/auth/login创建应用
创建数据表
点击应用,视图化创建数据表
- 创建profiles表;
在数据表页面,点击“新建数据表”,页面配置如下:
其中profiles表字段id和auth.users表中的id字段(uuid类型)外键关联。
- 开启Profiles的RLS数据安全访问规则;
选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮
- 允许每个用户可以查看公共的个人信息资料;
点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。
- 仅允许用户增删改查本人的个人资料信息;
点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。
创建avatars存储桶
创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:
- 创建一个存储桶avatars
在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。
- 允许每个用户可以查看存储桶avatars
选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
- 允许用户上传存储桶avatars;
选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
查看结果
所有数据表及RLS的sql(策略名称用英文代替)
-- Create a table for public "profiles"
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
获取 API密钥
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取您在上面复制的URL和anon的密钥。
在应用->概括页面,获取服务地址以及token信息。
Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。
注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,绝不能在客户端或浏览器上使用。 在后续示例代码中,需要提供supabaseUrl和supabaseKey。
认证设置
当用户点击邮件内魔法链接进行登录时,是需要跳转到我们应用的登录界面的。这里需要在认证设置中进行相关URL重定向的配置。
因为我们最终的应用会在本地的3000端口启动(亦或者其他端口),所以这里我们暂时将url设置为http://localhost:3000/
除此之外,在此界面也可以自定义使用我们自己的smtp服务器。
构建应用程序
让我们从头开始构建 React Native应用程序。
初始化 React Native
我们可以使用create-expo-app来初始化一个名为 memfiredbReactNative:
npx create-expo-app memfiredbReactNative
cd memfiredbReactNative
然后让我们安装额外的依赖项:supabase-js
yarn add @supabase/supabase-js
yarn add react-native-elements
yarn add react-native-safe-area-context
yarn add @react-native-async-storage/async-storage
yarn add react-native-url-polyfill
我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到supabase.js里面去。
lib/supabase.js文件
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js'
const URL = ""
const ANON_KEY = ""
export const supabase = createClient(URL, ANON_KEY, {
localStorage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
});
让我们设置一个 React Native 组件来管理登录和注册。用户将能够使用他们的电子邮件和密码登录。
components/Auth.js
import React, { useState } from 'react'
import { Alert, StyleSheet, View } from 'react-native'
import { supabase } from '../lib/supabase'
import { Button, Input } from 'react-native-elements'
export default function Auth() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
async function signInWithEmail() {
setLoading(true)
const { user, error } = await supabase.auth.signIn({
email: email,
password: password,
})
if (error) Alert.alert(error.message)
setLoading(false)
}
async function signUpWithEmail() {
setLoading(true)
const { user, error } = await supabase.auth.signUp({
email: email,
password: password,
})
if (error) Alert.alert(error.message)
setLoading(false)
}
return (
<View style={styles.mt100}>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input
label="邮箱"
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
onChangeText={(text) => setEmail(text)}
value={email}
placeholder="email@address.com"
autoCapitalize={'none'}
/>
</View>
<View style={styles.verticallySpaced}>
<Input
label="密码"
leftIcon={{ type: 'font-awesome', name: 'lock' }}
onChangeText={(text) => setPassword(text)}
value={password}
secureTextEntry={true}
placeholder="Password"
autoCapitalize={'none'}
/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button title="登录" disabled={loading} onPress={() => signInWithEmail()} />
</View>
<View style={styles.verticallySpaced}>
<Button title="注册" disabled={loading} onPress={() => signUpWithEmail()} />
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: 'stretch',
},
mt20: {
marginTop: 20,
},
mt100:{
marginTop:100
}
})
用户信息页面
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。
让我们为它创建一个名为Account.js
.
components/Account.js
import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import { StyleSheet, View, Alert } from "react-native";
import { Button, Input } from "react-native-elements";
import { ApiError, Session } from "@supabase/supabase-js";
export default function Account({ session }) {
const [loading, setLoading] = useState(false);
const [username, setUsername] = useState("");
const [website, setWebsite] = useState("");
const [avatar_url, setAvatarUrl] = useState("");
useEffect(() => {
if (session) getProfile();
}, [session]);
async function getProfile() {
try {
setLoading(true);
const user = supabase.auth.user();
if (!user) throw new Error("No user on the session!");
let { data, error, status } = await supabase
.from("profiles")
.select(`username, website, avatar_url`)
.eq("id", user.id)
.single();
if (error && status !== 406) {
throw error;
}
if (data) {
setUsername(data.username);
setWebsite(data.website);
setAvatarUrl(data.avatar_url);
}
} catch (error) {
Alert.alert((error).message);
} finally {
setLoading(false);
}
}
async function updateProfile({
username,
website,
avatar_url,
}) {
try {
setLoading(true);
const user = supabase.auth.user();
if (!user) throw new Error(" 会话中没有用户!");
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
let { error } = await supabase
.from("profiles")
.upsert(updates, { returning: "minimal" });
if (error) {
throw error;
}
} catch (error) {
Alert.alert((error).message);
} finally {
setLoading(false);
}
}
return (
<View style={styles.mt100}>
<View>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input label="邮箱" value={session?.user?.email} disabled />
</View>
<View style={styles.verticallySpaced}>
<Input
label="用户名"
value={username || ""}
onChangeText={(text) => setUsername(text)}
/>
</View>
<View style={styles.verticallySpaced}>
<Input
label="网址"
value={website || ""}
onChangeText={(text) => setWebsite(text)}
/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
title={loading ? "正在修改" : "确定"}
onPress={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
/>
</View>
<View style={styles.verticallySpaced}>
<Button title="登出" onPress={() => supabase.auth.signOut()} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
backgroundColor:'black'
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: "stretch",
},
mt20: {
marginTop: 20,
},
mt100:{
marginTop:100
}
});
修改项目入口文件
现在我们已经准备好所有组件,让我们更新App.js:
App.js
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import 'react-native-url-polyfill/auto'
import React, { useRef, useState, useEffect } from 'react';
import { supabase } from './lib/supabase'
import Auth from './components/Auth'
import Account from './components/Account'
export default function App() {
const [session, setSession] = useState()
useEffect(() => {
setSession(supabase.auth.session())
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
return (
<View>
{session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
});
完成后,在终端执行打开浏览器
expo start --web
实现:上传头像及更新用户信息
每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。
创建上传小组件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。
创建上传头像的小组件
components/Avatar.js
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'
import { View } from 'react-native'
import { Button } from 'react-native-elements'
export default function Avatar({ url, size, onUpload }) {
const [avatarUrl, setAvatarUrl] = useState(null)
const [uploading, setUploading] = useState(false)
useEffect(() => {
if (url) downloadImage(url)
}, [url])
async function downloadImage(path) {
try {
const { data, error } = await supabase.storage.from('avatars').download(path)
if (error) {
throw error
}
var blob = new Blob([data], {
type: "text/vtt; charset=utf-8"
});
const fileReaderInstance = new FileReader();
fileReaderInstance.readAsDataURL(blob);
fileReaderInstance.onload = () => {
let base64 = fileReaderInstance.result;
setAvatarUrl(base64)
}
} catch (error) {
console.log('Error downloading image: ', error.message)
}
}
async function uploadAvatar(event) {
try {
setUploading(true)
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) {
throw uploadError
}
onUpload(filePath)
} catch (error) {
alert(error.message)
} finally {
setUploading(false)
}
}
return (
<View>
{avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="avatar image" />
) : (
<View className="avatar no-image" />
)}
<View>
<label className="button primary block" htmlFor="single">
<Button title={uploading ? 'Uploading ...' : 'Upload'}></Button>
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</View>
</View>
)
}
然后我们可以在components/Account.js模板的顶部添加小部件:
components/Account.js
import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import { StyleSheet, View, Alert } from "react-native";
import { Button, Input } from "react-native-elements";
import { ApiError, Session } from "@supabase/supabase-js";
import Avatar from './Avatar'
export default function Account({ session }) {
const [loading, setLoading] = useState(false);
const [username, setUsername] = useState("");
const [website, setWebsite] = useState("");
const [avatar_url, setAvatarUrl] = useState("");
useEffect(() => {
if (session) getProfile();
}, [session]);
async function getProfile() {
try {
setLoading(true);
const user = supabase.auth.user();
if (!user) throw new Error("No user on the session!");
let { data, error, status } = await supabase
.from("profiles")
.select(`username, website, avatar_url`)
.eq("id", user.id)
.single();
if (error && status !== 406) {
throw error;
}
if (data) {
setUsername(data.username);
setWebsite(data.website);
setAvatarUrl(data.avatar_url);
}
} catch (error) {
Alert.alert((error).message);
} finally {
setLoading(false);
}
}
async function updateProfile({
username,
website,
avatar_url,
}) {
try {
setLoading(true);
const user = supabase.auth.user();
if (!user) throw new Error(" 会话中没有用户!");
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
let { error } = await supabase
.from("profiles")
.upsert(updates, { returning: "minimal" });
if (error) {
throw error;
}
} catch (error) {
Alert.alert((error).message);
} finally {
setLoading(false);
}
}
return (
<View style={styles.mt100}>
<View>
<Avatar url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input label="邮箱" value={session?.user?.email} disabled />
</View>
<View style={styles.verticallySpaced}>
<Input
label="用户名"
value={username || ""}
onChangeText={(text) => setUsername(text)}
/>
</View>
<View style={styles.verticallySpaced}>
<Input
label="网址"
value={website || ""}
onChangeText={(text) => setWebsite(text)}
/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
title={loading ? "正在修改" : "确定"}
onPress={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
/>
</View>
<View style={styles.verticallySpaced}>
<Button title="登出" onPress={() => supabase.auth.signOut()} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
backgroundColor:'black'
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: "stretch",
},
mt20: {
marginTop: 20,
},
mt100:{
marginTop:100
}
});
恭喜!在这个阶段,您拥有一个功能齐全的应用程序!