前言
VideoTracker v0.0.1 用户记录的视频信息都保存在本地,如果更换电脑或者浏览器,记录就无法同步,体验欠佳
在最新 v0.0.3 版本我加入了云端同步功能,突破本地单浏览器限制,实现随时随地同步
技术选型
服务端只需要登录和保存数据信息,supabase 即可满足需求。第三方登录使用 google 登录,没有 google 账户可以使用邮箱注册账号
正式开发前还需要完成 Google Cloud 和 Supabase 登录设置
Google Cloud
登录Google Cloud,创建项目

打开新建项目video-tracker,左侧菜单选择API和服务-已启动的API和服务,在 api 库开启Google+ API服务

选择API和服务-凭证新建凭证,选择OAuth 客户端 ID,虽然我们的应用是浏览器插件,但是需要在 Supabase 完成登录,所以要选择Web应用,点击创建。创建完成弹框里的客户端ID和客户端密钥记得保存下来,等一下需要用到

Supabase
登录Supabase,创建新项目

左侧菜单选择Authentication-Sign In / Providers,勾选Confirm email,新用户邮箱注册成功会收到确认邮件,点击确认邮件即可完成注册

下滑Auth Providers打开Google设置,输入刚才 Google Cloud 创建 OAuth 创建应用的 IDs 和密钥,回调地址填入刚才的 OAuth 应用,到这里 Google 授权登录配置已经完成

注册邮箱授权
如果是邮箱注册,收到授权链接是插件的地址,这里还需要在 Supabase 添加授权 url 回调,打开Authentication-URL Configuration,添加插件地址

代码实现
先让 ai 总结一下代码实现,我们实现某些功能时候,也可以先让 ai 创建功能文档
plain
插件登录页
-> Supabase signInWithOAuth({ provider: 'google', redirectTo: chrome.identity.getRedirectURL() })
-> 打开 Supabase 生成的 Google OAuth URL
-> 用户在 Google 授权
-> Google 重定向到 Supabase Callback URL
-> Supabase 校验 Google 返回结果
-> Supabase 生成自己的 session / access_token / refresh_token
-> Supabase 再重定向到你传入的 redirectTo
-> redirectTo 是 chrome.identity.getRedirectURL()
-> Chrome 捕获 chromiumapp.org 回调
-> launchWebAuthFlow callback 收到 token URL
-> 插件 setSession 登录成功
- supabase 获取 google 授权地址(data.url),redirectUrl 也就是我们在 supabase 配置的授权回调 url,上面第 8 步的地址
typescript
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: redirectUrl,
skipBrowserRedirect: true,
},
});
- chrome.identity.launchWebAuthFlow 打开授权窗口
typescript
chrome.identity.launchWebAuthFlow(
{
url: data.url,
interactive: true,
},
async (responseUrl) => { })
- google 授权后,经过上面 5-9 的过程,返回 access_token 和 refresh_token 信息,在回调里处理
plain
typescript
async function signInWithOAuth(provider: 'github' | 'google') {
//
const redirectUrl = chrome.identity.getRedirectURL();
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: redirectUrl,
skipBrowserRedirect: true,
},
});
if (error) throw error;
return new Promise((resolve) => {
chrome.identity.launchWebAuthFlow(
{
url: data.url,
interactive: true,
},
async (responseUrl) => {
if (chrome.runtime.lastError) {
resolve({ success: false, error: chrome.runtime.lastError.message });
return;
}
if (!responseUrl) {
resolve({ success: false, error: 'No response URL' });
return;
}
const url = new URL(responseUrl);
const hashParams = new URLSearchParams(url.hash.substring(1));
const accessToken = hashParams.get('access_token');
const refreshToken = hashParams.get('refresh_token');
if (!accessToken) {
resolve({ success: false, error: 'No access token in response' });
return;
}
const { data: { session }, error: sessionError } =
await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken || '',
});
if (sessionError || !session) {
resolve({
success: false,
error: sessionError?.message || 'Failed to establish session',
});
return;
}
await checkSession();
resolve({ success: true });
}
);
});
}