快速入门|使用MemFire Cloud构建Next.js应用程序

MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。

教学视频

此示例提供了使用 MemFire Cloud 和 Next.js构建简单用户管理应用程序(从头开始)的步骤。这包括:

  • MemFire Cloud云数据库:用于存储用户数据的 MemFireDB数据库。

  • MemFire Cloud用户验证:用户可以使用魔法链接登录(不需要密码,只需要电子邮件)。

  • MemFire Cloud存储:用户可以上传照片。

  • 行级安全策略:数据受到保护,因此个人只能访问自己的数据。

  • 即时API:创建数据库表时会自动生成 API。

在本指南结束时,您将拥有一个允许用户登录和更新一些基本个人资料详细信息的应用程序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6Vnosrg-1663723902918)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDUyZjU2MWZmZmI0NjdhMjBlZDUzZWQxZWY2NmNhNWVfd3ZVZk03Qmw3bGtkTndqYU9tVEtCVndJdjF4ZmhxNFBfVG9rZW46Ym94Y25PUDNDcDZ5bWJxV0hVWTR4ZHZWNlRiXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

创建应用

目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。

登录https://cloud.memfiredb.com/auth/login创建应用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44KaEAKR-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=N2I3ZmRjMDRjOTVjMzk3NTg3MWVjNTcxNTdiZmNhZjdfVFFERDZGMWZaanBwUFc4ckdvY1JyVTRRQXBFMzBHWXNfVG9rZW46Ym94Y24yMzJuYWN2YTF4SjRITUlrYVluQWplXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

创建数据表

点击应用,视图化创建数据表

  1. 创建profiles表;

在数据表页面,点击“新建数据表”,页面配置如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qQ3sQCXy-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=NTA2NmQyOTkwZmY0NDQ3MjNjNmU4OGY4OWRmY2JjZDFfTlJlSVAyRG9RZkV3UGpUTWRPN0lVNG1nT1VrWWRJSnVfVG9rZW46Ym94Y25jTEVUQ0NuODNDWnFUMFpGRHJUZFBlXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

其中profiles表字段id和auth.users表中的id字段(uuid类型)外键关联。

  1. 开启Profiles的RLS数据安全访问规则;

选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kV37ml76-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=OWMxMWMzZDE1Yzk4MDc2ODg1M2VmMzE4NDU0OTdmNDZfZWJwWW5tb2ZJZTJHUk5xcVpTaGIwcU1vZjEyWE9sMzFfVG9rZW46Ym94Y25oeWtyQmdZYllidWUwTHVXcU9vYTllXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

  1. 允许每个用户可以查看公共的个人信息资料;

点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2l1hVXUA-1663723902920)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDI3NjMxNzZhNGU4YzU3MDNiNDJiZGYxZjNkNDc0NzBfaW9TeU1ZbTVHV2pBOXhYVnphbDZRaGtOM1RoZVMyYkFfVG9rZW46Ym94Y25aWXBsbnRNSkpPYWdHeUlmMzJlM0xjXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

  1. 仅允许用户增删改查本人的个人资料信息;

点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbIoZJgF-1663723902925)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=YzIwNDhiNTJlNDA2M2QzZTViMGVlODc4ZDYwOWMxNTBfUmpPd2JsbGR2dk85ZTh3SXlYcjFnZVpGNmxndXE1b0ZfVG9rZW46Ym94Y25mRGkwSUtCYXM3RG5BNWh2UUR5MkpnXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

创建avatars存储桶

创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:

  1. 创建一个存储桶avatars

在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crdjl4dB-1663723902926)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MWYxMDM5Y2YyMThlMDJkZDhjYWZhM2ZkY2JjMDE3NDRfR3FOVHVDQ0ptSVRWV0RoTVpPVW52ZFdLbGx5RUdQTVpfVG9rZW46Ym94Y25SSDV0Mlh6bVp6aVRIZWdBQmtLYU44XzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

  1. 允许每个用户可以查看存储桶avatars

选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-puWZV2DP-1663723902927)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=NjNmMmU4OTlkNWIzYjk1NDM5MDUxM2UxZjlkODZkMGNfV29IUU90dEEyQlRBVzNaaXhyNFVtQ0l6ZE4zOUxXcGlfVG9rZW46Ym94Y25jUnVkbEVFVWV4VDdHNFdHTklDdXhkXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xd2SHI8A-1663723902927)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MTBiNWQ4MTU4NTRhOTFmNjBiMWZkZjdhYTkzNzllZWRfa20yTjBUZmFsZThFd1hQamNaZXpUYnMwbldTUmIwVFZfVG9rZW46Ym94Y25JRkdXTHg1bDk0WGdxVlA5WGRIMkxiXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

  1. 允许用户上传存储桶avatars;

选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2X3SUcGT-1663723902928)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=Y2MwNTYwNDE5ZjU2M2U2NzgyNzAwNzkzYWNmNmQyMjVfZjZtTkI3M0pDdVV0ZURCaUw4ZEZzMzE2RExacjZ0a0VfVG9rZW46Ym94Y25lTzBYR29LcU9kUXlkbGdYN0hpSU9kXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwStQMiv-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MjljYjViYjM0NDJmMGVjZGY0YjFmYjIwYTQzMjVhMDVfM0N1ZWcyTnl0Znh1S242TWRUa3RyN25nRXZrN0VhTEtfVG9rZW46Ym94Y25vT2tDV0YzMUc4elhydnBxeWMxb29kXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

查看结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7gmJKS6u-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MzJiY2YzMzU4MTNkYjg1M2MxNzgyNzdiZjEzNjU2NDNfS1o1NUprY2l0eWtJVXM0b0Myc0NzOElBSHNQd0dtZjBfVG9rZW46Ym94Y25rcDVubkpGM1c1aG9CaDN6RDhUODJmXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

所有数据表及RLS的sql(策略名称用英文代替)

1-- Create a table for public "profiles" 2create table profiles ( 3 id uuid references auth.users not null, 4 updated_at timestamp with time zone, 5 username text unique, 6 avatar_url text, 7 website text, 8 9 primary key (id), 10 unique(username), 11); 12 13alter table profiles enable row level security; 14 15create policy "Public profiles are viewable by everyone." 16 on profiles for select 17 using ( true ); 18 19create policy "Users can insert their own profile." 20 on profiles for insert 21 with check ( auth.uid() = id ); 22 23create policy "Users can update own profile." 24 on profiles for update 25 using ( auth.uid() = id ); 26-- Set up Storage! 27insert into storage.buckets (id, name) 28values ('avatars', 'avatars'); 29 30create policy "Avatar images are publicly accessible." 31 on storage.objects for select 32 using ( bucket_id = 'avatars' ); 33 34create policy "Anyone can upload an avatar." 35 on storage.objects for insert 36 with check ( bucket_id = 'avatars' );

获取 API密钥

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取您在上面复制的URL和anon的密钥。

在应用->概括页面,获取服务地址以及token信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p172GkuF-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=OTI0OGNkODdkMmQxZWE4ZTZkZDJmNTA1Y2JhMDFhZjVfU3VzY2tMTVlTVmZOR0REN2x1QnBWWHZ2N1lzY0s3TlJfVG9rZW46Ym94Y25neVZSYlQ5MVRrd0p6VVVsS3JKVGZjXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。

注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,绝不能在客户端或浏览器上使用。 在后续示例代码中,需要提供supabaseUrl和supabaseKey。

认证设置

当用户点击邮件内魔法链接进行登录时,是需要跳转到我们应用的登录界面的。这里需要在认证设置中进行相关URL重定向的配置。

因为我们最终的应用会在本地的3000端口启动(亦或者其他端口),所以这里我们暂时将url设置为http://localhost:3000/

除此之外,在此界面也可以自定义使用我们自己的smtp服务器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNdiMOe8-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MDY2NmZkNWEyZjJiZDY2YWU3MTcwM2Y1MTk1ODY3NjZfbXBRekllUWZzMEkzNk9RUVdOc2o5WGoyMmFVd0E4THVfVG9rZW46Ym94Y240eVZQREM4M3lzbEw3YW0wY0Z3TTVnXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

构建应用程序

让我们从头开始构建 NextJs 应用程序。

初始化一个 NextJs 应用程序

我们可以create-next-app用来初始化一个名为memfiredb-nextjs:

1npx create-next-app memfiredb-nextjs 2cd memfiredb-nextjs

然后让我们安装唯一的附加依赖项:supabase-js

1npm install @supabase/supabase-js

最后,我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到supabaseClient.js里面去。

utils/supabaseClient.js

1import { createClient } from '@supabase/supabase-js' 2 3const url = URL 4const key = ANON_KEY 5 6export const supabase = createClient(url, key)

更新样式

可以看到界面实在是不怎么优雅,更新下样式,让它好看一些。 修改styles/globals.css文件。

1html, 2body { 3 --custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, 4 Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 5 --custom-bg-color: #101010; 6 --custom-panel-color: #222; 7 --custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8); 8 --custom-color: #fff; 9 --custom-color-brand: #24b47e; 10 --custom-color-secondary: #666; 11 --custom-border: 1px solid #333; 12 --custom-border-radius: 5px; 13 --custom-spacing: 5px; 14 15 padding: 0; 16 margin: 0; 17 font-family: var(--custom-font-family); 18 background-color: var(--custom-bg-color); 19} 20 21* { 22 color: var(--custom-color); 23 font-family: var(--custom-font-family); 24 box-sizing: border-box; 25} 26 27html, 28body, 29#__next { 30 height: 100vh; 31 width: 100vw; 32 overflow-x: hidden; 33} 34 35/* Grid */ 36 37.container { 38 width: 90%; 39 margin-left: auto; 40 margin-right: auto; 41} 42.row { 43 position: relative; 44 width: 100%; 45} 46.row [class^='col'] { 47 float: left; 48 margin: 0.5rem 2%; 49 min-height: 0.125rem; 50} 51.col-1, 52.col-2, 53.col-3, 54.col-4, 55.col-5, 56.col-6, 57.col-7, 58.col-8, 59.col-9, 60.col-10, 61.col-11, 62.col-12 { 63 width: 96%; 64} 65.col-1-sm { 66 width: 4.33%; 67} 68.col-2-sm { 69 width: 12.66%; 70} 71.col-3-sm { 72 width: 21%; 73} 74.col-4-sm { 75 width: 29.33%; 76} 77.col-5-sm { 78 width: 37.66%; 79} 80.col-6-sm { 81 width: 46%; 82} 83.col-7-sm { 84 width: 54.33%; 85} 86.col-8-sm { 87 width: 62.66%; 88} 89.col-9-sm { 90 width: 71%; 91} 92.col-10-sm { 93 width: 79.33%; 94} 95.col-11-sm { 96 width: 87.66%; 97} 98.col-12-sm { 99 width: 96%; 100} 101.row::after { 102 content: ''; 103 display: table; 104 clear: both; 105} 106.hidden-sm { 107 display: none; 108} 109 110@media only screen and (min-width: 33.75em) { 111 /* 540px */ 112 .container { 113 width: 80%; 114 } 115} 116 117@media only screen and (min-width: 45em) { 118 /* 720px */ 119 .col-1 { 120 width: 4.33%; 121 } 122 .col-2 { 123 width: 12.66%; 124 } 125 .col-3 { 126 width: 21%; 127 } 128 .col-4 { 129 width: 29.33%; 130 } 131 .col-5 { 132 width: 37.66%; 133 } 134 .col-6 { 135 width: 46%; 136 } 137 .col-7 { 138 width: 54.33%; 139 } 140 .col-8 { 141 width: 62.66%; 142 } 143 .col-9 { 144 width: 71%; 145 } 146 .col-10 { 147 width: 79.33%; 148 } 149 .col-11 { 150 width: 87.66%; 151 } 152 .col-12 { 153 width: 96%; 154 } 155 .hidden-sm { 156 display: block; 157 } 158} 159 160@media only screen and (min-width: 60em) { 161 /* 960px */ 162 .container { 163 width: 75%; 164 max-width: 60rem; 165 } 166} 167 168/* Forms */ 169 170label { 171 display: block; 172 margin: 5px 0; 173 color: var(--custom-color-secondary); 174 font-size: 0.8rem; 175 text-transform: uppercase; 176} 177 178input { 179 width: 100%; 180 border-radius: 5px; 181 border: var(--custom-border); 182 padding: 8px; 183 font-size: 0.9rem; 184 background-color: var(--custom-bg-color); 185 color: var(--custom-color); 186} 187 188input[disabled] { 189 color: var(--custom-color-secondary); 190} 191 192/* Utils */ 193 194.block { 195 display: block; 196 width: 100%; 197} 198.inline-block { 199 display: inline-block; 200 width: 100%; 201} 202.flex { 203 display: flex; 204} 205.flex.column { 206 flex-direction: column; 207} 208.flex.row { 209 flex-direction: row; 210} 211.flex.flex-1 { 212 flex: 1 1 0; 213} 214.flex-end { 215 justify-content: flex-end; 216} 217.flex-center { 218 justify-content: center; 219} 220.items-center { 221 align-items: center; 222} 223.text-sm { 224 font-size: 0.8rem; 225 font-weight: 300; 226} 227.text-right { 228 text-align: right; 229} 230.font-light { 231 font-weight: 300; 232} 233.opacity-half { 234 opacity: 50%; 235} 236 237/* Button */ 238 239button, 240.button { 241 color: var(--custom-color); 242 border: var(--custom-border); 243 background-color: var(--custom-bg-color); 244 display: inline-block; 245 text-align: center; 246 border-radius: var(--custom-border-radius); 247 padding: 0.5rem 1rem; 248 cursor: pointer; 249 text-align: center; 250 font-size: 0.9rem; 251 text-transform: uppercase; 252} 253 254button.primary, 255.button.primary { 256 background-color: var(--custom-color-brand); 257 border: 1px solid var(--custom-color-brand); 258} 259 260/* Widgets */ 261 262.card { 263 width: 100%; 264 display: block; 265 border: var(--custom-border); 266 border-radius: var(--custom-border-radius); 267 padding: var(--custom-spacing); 268} 269 270.avatar { 271 border-radius: var(--custom-border-radius); 272 overflow: hidden; 273 max-width: 100%; 274} 275.avatar.image { 276 object-fit: cover; 277} 278.avatar.no-image { 279 background-color: #333; 280 border: 1px solid rgb(200, 200, 200); 281 border-radius: 5px; 282} 283 284.footer { 285 position: absolute; 286 max-width: 100%; 287 bottom: 0; 288 left: 0; 289 right: 0; 290 display: flex; 291 flex-flow: row; 292 border-top: var(--custom-border); 293 background-color: var(--custom-bg-color); 294} 295.footer div { 296 padding: var(--custom-spacing); 297 display: flex; 298 align-items: center; 299 width: 100%; 300} 301.footer div > img { 302 height: 20px; 303 margin-left: 10px; 304} 305.footer > div:first-child { 306 display: none; 307} 308.footer > div:nth-child(2) { 309 justify-content: left; 310} 311 312@media only screen and (min-width: 60em) { 313 /* 960px */ 314 .footer > div:first-child { 315 display: flex; 316 } 317 .footer > div:nth-child(2) { 318 justify-content: center; 319 } 320} 321 322@keyframes spin { 323 from { 324 transform: rotate(0deg); 325 } 326 to { 327 transform: rotate(360deg); 328 } 329} 330 331.mainHeader { 332 width: 100%; 333 font-size: 1.3rem; 334 margin-bottom: 20px; 335} 336 337.avatarPlaceholder { 338 border: var(--custom-border); 339 border-radius: var(--custom-border-radius); 340 width: 35px; 341 height: 35px; 342 background-color: rgba(255, 255, 255, 0.2); 343 display: flex; 344 align-items: center; 345 justify-content: center; 346} 347 348/* Auth */ 349 350.auth-widget { 351 display: flex; 352 flex-direction: column; 353 gap: 20px; 354} 355 356.auth-widget > .button { 357 display: flex; 358 align-items: center; 359 justify-content: center; 360 border: none; 361 background-color: #444444; 362 text-transform: none !important; 363 transition: all 0.2s ease; 364} 365 366.auth-widget .button:hover { 367 background-color: #2a2a2a; 368} 369 370.auth-widget .button > .loader { 371 width: 17px; 372 animation: spin 1s linear infinite; 373 filter: invert(1); 374} 375 376/* Account */ 377 378.account { 379 display: flex; 380 flex-direction: column; 381 gap: 20px; 382} 383 384.account > * > .avatarField { 385 display: flex; 386 align-items: center; 387 margin-bottom: 30px; 388} 389 390.account > * > .avatarField > .avatarContainer { 391 margin-right: 20px; 392} 393 394/* Profile Card */ 395 396.profileCard { 397 border-radius: 5px; 398 display: flex; 399 border: var(--custom-border); 400 background-color: var(--custom-panel-color); 401 padding: 20px 20px; 402 margin-bottom: 20px; 403} 404 405.profileCard:last-child { 406 margin-bottom: 0px; 407} 408 409.profileCard > .userInfo { 410 margin-left: 20px; 411 font-weight: 300; 412 display: flex; 413 flex-direction: column; 414 justify-content: center; 415} 416 417.profileCard > .userInfo > p { 418 margin: 0; 419} 420 421.profileCard > .userInfo > .username { 422 font-size: 1.3rem; 423 font-weight: 500; 424 margin-bottom: 5px; 425} 426 427.profileCard > .userInfo > .website { 428 font-size: 0.9rem; 429 color: var(--custom-color-brand); 430 margin-bottom: 10px; 431 text-decoration: none; 432}

设置登录组件

让我们设置一个 React 组件来管理登录和注册。我们将使用 Magic Links,因此用户无需使用密码即可使用电子邮件登录。

components/Auth.js

1import { useState } from 'react' 2import { supabase } from '../utils/supabaseClient' 3 4export default function Auth() { 5 const [loading, setLoading] = useState(false) 6 const [email, setEmail] = useState('') 7 8 const handleLogin = async (email) => { 9 try { 10 setLoading(true) 11 const { error } = await supabase.auth.signIn({ email }) 12 if (error) throw error 13 alert('请检查您的电子邮件以获取登录链接!') 14 } catch (error) { 15 alert(error.error_description || error.message) 16 } finally { 17 setLoading(false) 18 } 19 } 20 21 return ( 22 <div className="row flex flex-center"> 23 <div className="col-6 form-widget"> 24 <h1 className="header">memfiredb + Next.js</h1> 25 <p className="description">使用下面的电子邮件通过魔法链接登录</p> 26 <div> 27 <input 28 className="inputField" 29 type="email" 30 placeholder="Your email" 31 value={email} 32 onChange={(e) => setEmail(e.target.value)} 33 /> 34 </div> 35 <div> 36 <button 37 onClick={(e) => { 38 e.preventDefault() 39 handleLogin(email) 40 }} 41 className="button block" 42 disabled={loading} 43 > 44 <span>{loading ? 'Loading' : '发送魔法链接'}</span> 45 </button> 46 </div> 47 </div> 48 </div> 49 ) 50}

用户信息页面

用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的用户信息。

让我们为它创建一个名为Account.js.

components/Account.js

1import { useState, useEffect } from 'react' 2import { supabase } from '../utils/supabaseClient' 3 4export default function Account({ session }) { 5 const [loading, setLoading] = useState(true) 6 const [username, setUsername] = useState(null) 7 const [website, setWebsite] = useState(null) 8 const [avatar_url, setAvatarUrl] = useState(null) 9 10 useEffect(() => { 11 getProfile() 12 }, [session]) 13 14 async function getProfile() { 15 try { 16 setLoading(true) 17 const user = supabase.auth.user() 18 19 let { data, error, status } = await supabase 20 .from('profiles') 21 .select(`username, website, avatar_url`) 22 .eq('id', user.id) 23 .single() 24 25 if (error && status !== 406) { 26 throw error 27 } 28 29 if (data) { 30 setUsername(data.username) 31 setWebsite(data.website) 32 setAvatarUrl(data.avatar_url) 33 } 34 } catch (error) { 35 alert(error.message) 36 } finally { 37 setLoading(false) 38 } 39 } 40 41 async function updateProfile({ username, website, avatar_url }) { 42 try { 43 setLoading(true) 44 const user = supabase.auth.user() 45 46 const updates = { 47 id: user.id, 48 username, 49 website, 50 avatar_url, 51 updated_at: new Date(), 52 } 53 54 let { error } = await supabase.from('profiles').upsert(updates, { 55 returning: 'minimal', // Don't return the value after inserting 56 }) 57 58 if (error) { 59 throw error 60 } 61 alert('修改成功') 62 } catch (error) { 63 alert(error.message) 64 } finally { 65 setLoading(false) 66 } 67 } 68 69 return ( 70 <div className="form-widget"> 71 <div> 72 <label htmlFor="email">邮箱</label> 73 <input id="email" type="text" value={session.user.email} disabled /> 74 </div> 75 <div> 76 <label htmlFor="username">名称</label> 77 <input 78 id="username" 79 type="text" 80 value={username || ''} 81 onChange={(e) => setUsername(e.target.value)} 82 /> 83 </div> 84 <div> 85 <label htmlFor="website">网站</label> 86 <input 87 id="website" 88 type="website" 89 value={website || ''} 90 onChange={(e) => setWebsite(e.target.value)} 91 /> 92 </div> 93 <div> 94 <button 95 className="button block primary" 96 onClick={() => updateProfile({ username, website, avatar_url })} 97 disabled={loading} 98 > 99 {loading ? 'Loading ...' : '修改'} 100 </button> 101 </div> 102 103 <div> 104 <button className="button block" onClick={() => supabase.auth.signOut()}> 105 登出 106 </button> 107 </div> 108 </div> 109 ) 110}

修改项目入口文件

pages/index.js

1import { useState, useEffect } from 'react' 2import { supabase } from '../utils/supabaseClient' 3import Auth from '../components/Auth' 4import Account from '../components/Account' 5 6export default function Home() { 7 const [session, setSession] = useState(null) 8 9 useEffect(() => { 10 setSession(supabase.auth.session()) 11 12 supabase.auth.onAuthStateChange((_event, session) => { 13 setSession(session) 14 }) 15 }, []) 16 17 return ( 18 <div className="container" style={{ padding: '50px 0 100px 0' }}> 19 {!session ? <Auth /> : <Account key={session.user.id} session={session} />} 20 </div> 21 ) 22}

完成后,在终端窗口中运行它:

1npm run dev

然后打开浏览器到localhost:3000,你应该会看到完整的应用程序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUdLEUe8-1663723902930)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=NjZlZmI0OGVmZWRmM2ZlZTJjNzk1ZDZlNTJhYzdhNTZfWEJya0FKVWdZVGM2Rm5HSTBWSmpxak1vOE9hVFVZczVfVG9rZW46Ym94Y25QMmdZQ0RQV0hvNnMzYzQ5OTR0dzZkXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]

实现:上传头像及更新用户信息

每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。

创建上传小组件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。我们可以从创建一个新组件开始:

components/Avatar.js

1import { useEffect, useState } from 'react' 2import { supabase } from '../utils/supabaseClient' 3 4export default function Avatar({ url, size, onUpload }) { 5 const [avatarUrl, setAvatarUrl] = useState(null) 6 const [uploading, setUploading] = useState(false) 7 8 useEffect(() => { 9 if (url) downloadImage(url) 10 }, [url]) 11 12 async function downloadImage(path) { 13 try { 14 const { data, error } = await supabase.storage.from('avatars').download(path) 15 if (error) { 16 throw error 17 } 18 const url = URL.createObjectURL(data) 19 setAvatarUrl(url) 20 } catch (error) { 21 console.log('Error downloading image: ', error.message) 22 } 23 } 24 25 async function uploadAvatar(event) { 26 try { 27 setUploading(true) 28 29 if (!event.target.files || event.target.files.length === 0) { 30 throw new Error('请上传图片') 31 } 32 33 const file = event.target.files[0] 34 const fileExt = file.name.split('.').pop() 35 const fileName = `${Math.random()}.${fileExt}` 36 const filePath = `${fileName}` 37 38 let { error: uploadError } = await supabase.storage 39 .from('avatars') 40 .upload(filePath, file) 41 42 if (uploadError) { 43 throw uploadError 44 } 45 46 onUpload(filePath) 47 } catch (error) { 48 alert(error.message) 49 } finally { 50 setUploading(false) 51 } 52 } 53 54 return ( 55 <div> 56 {avatarUrl ? ( 57 <img 58 src={avatarUrl} 59 alt="Avatar" 60 className="avatar image" 61 style={{ height: size, width: size }} 62 /> 63 ) : ( 64 <div className="avatar no-image" style={{ height: size, width: size }} /> 65 )} 66 <div style={{ width: size }}> 67 <label className="button primary block" htmlFor="single"> 68 {uploading ? 'Uploading ...' : 'Upload'} 69 </label> 70 <input 71 style={{ 72 visibility: 'hidden', 73 position: 'absolute', 74 }} 75 type="file" 76 id="single" 77 accept="image/*" 78 onChange={uploadAvatar} 79 disabled={uploading} 80 /> 81 </div> 82 </div> 83 ) 84}

将组建引入到Account页面

components/Account.js

1import { useState, useEffect } from 'react' 2import { supabase } from '../utils/supabaseClient' 3import Avatar from './Avatar' 4 5export default function Account({ session }) { 6 const [loading, setLoading] = useState(true) 7 const [username, setUsername] = useState(null) 8 const [website, setWebsite] = useState(null) 9 const [avatar_url, setAvatarUrl] = useState(null) 10 11 useEffect(() => { 12 getProfile() 13 }, [session]) 14 15 async function getProfile() { 16 try { 17 setLoading(true) 18 const user = supabase.auth.user() 19 20 let { data, error, status } = await supabase 21 .from('profiles') 22 .select(`username, website, avatar_url`) 23 .eq('id', user.id) 24 .single() 25 26 if (error && status !== 406) { 27 throw error 28 } 29 30 if (data) { 31 setUsername(data.username) 32 setWebsite(data.website) 33 setAvatarUrl(data.avatar_url) 34 } 35 } catch (error) { 36 alert(error.message) 37 } finally { 38 setLoading(false) 39 } 40 } 41 42 async function updateProfile({ username, website, avatar_url }) { 43 try { 44 setLoading(true) 45 const user = supabase.auth.user() 46 47 const updates = { 48 id: user.id, 49 username, 50 website, 51 avatar_url, 52 updated_at: new Date(), 53 } 54 55 let { error } = await supabase.from('profiles').upsert(updates, { 56 returning: 'minimal', // Don't return the value after inserting 57 }) 58 59 if (error) { 60 throw error 61 } 62 alert('修改成功') 63 } catch (error) { 64 alert(error.message) 65 } finally { 66 setLoading(false) 67 } 68 } 69 70 return ( 71 <div className="form-widget"> 72 <div> 73 <label htmlFor="email">邮箱</label> 74 <input id="email" type="text" value={session.user.email} disabled /> 75 </div> 76 <div> 77 <label htmlFor="username">名称</label> 78 <input 79 id="username" 80 type="text" 81 value={username || ''} 82 onChange={(e) => setUsername(e.target.value)} 83 /> 84 </div> 85 <div> 86 <label htmlFor="website">网站</label> 87 <input 88 id="website" 89 type="website" 90 value={website || ''} 91 onChange={(e) => setWebsite(e.target.value)} 92 /> 93 </div> 94 <Avatar 95 url={avatar_url} 96 size={150} 97 onUpload={(url) => { 98 setAvatarUrl(url) 99 updateProfile({ username, website, avatar_url: url }) 100 }} 101 /> 102 <div> 103 <button 104 className="button block primary" 105 onClick={() => updateProfile({ username, website, avatar_url })} 106 disabled={loading} 107 > 108 {loading ? 'Loading ...' : '修改'} 109 </button> 110 </div> 111 112 <div> 113 <button className="button block" onClick={() => supabase.auth.signOut()}> 114 登出 115 </button> 116 </div> 117 </div> 118 ) 119}

恭喜!在这个阶段,您拥有一个功能齐全的应用程序!

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

超轻量级的VSCode插件,数据库想用就用~

MemFireCloud是一个便捷、灵活、高效的云服务平台,依托于分布式数据库MemFireDB的云原生和线性扩展能力,为互联网用户提供一站式数据库自助服务。介绍MemFireCloud推出VSCode的可视化数据库管理插件,提供操作数据库的图形界面,帮助开发人员轻松简单的写代码,边操作数据库。其主要功能包括:连接管理:统一管理所有的数据库连接;

快速入门|使用MemFire Cloud构建React Native应用程序

MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专

快速入门|使用MemFire Cloud构建Vue3应用程序

MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。此示例提供了使用MemFireCloud和Vue3

重磅来袭|MemFire Cloud 推出的后端即服务内测版本上线啦~

新版本与大家正式见面啦!该版本,MemFireCloud致力于提供“开箱即用”的后端服务,为应用开发人员全面“减负”,快来开启您的应用之旅吧。MemFireCloud推出基于Supabase与MemFireDB打造的后端服务,主要包含以下功能特性:兼容PG的MemFireDB数据库托管,存储结构化数据,自动拓展身份验证和授权,统一管理注册用户,控

系统公告 | MemFire Cloud推出微信小程序SDK

“超能力”数据库~拿来即用,应用开发人员再也不用为撰写API而发愁。MemFireCloud为开发者提供了简单易用的云数据库(表编辑器、自动生成API、SQL编辑器、备份恢复、托管运维),很大地降低开发者的使用门槛。简介MemFireCloud推出基于SupabaseJavaScript库进行改造的supabasewechatstableSDK,