Supabase用户管理 #

一、获取用户信息 #

1.1 获取当前用户 #

typescript
// 获取当前登录用户
const { data: { user }, error } = await supabase.auth.getUser()

if (user) {
  console.log('User ID:', user.id)
  console.log('Email:', user.email)
  console.log('Phone:', user.phone)
  console.log('Metadata:', user.user_metadata)
  console.log('Created:', user.created_at)
  console.log('Last Sign In:', user.last_sign_in_at)
}

1.2 获取会话信息 #

typescript
// 获取当前会话
const { data: { session }, error } = await supabase.auth.getSession()

if (session) {
  console.log('Access Token:', session.access_token)
  console.log('Refresh Token:', session.refresh_token)
  console.log('Expires At:', new Date(session.expires_at! * 1000))
  console.log('User:', session.user)
}

1.3 监听认证状态 #

typescript
// 监听认证状态变化
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    console.log('Event:', event)
    console.log('Session:', session)
    
    switch (event) {
      case 'SIGNED_IN':
        // 用户登录
        break
      case 'SIGNED_OUT':
        // 用户登出
        break
      case 'TOKEN_REFRESHED':
        // Token刷新
        break
      case 'USER_UPDATED':
        // 用户信息更新
        break
    }
  }
)

// 清理监听
subscription.unsubscribe()

二、用户资料管理 #

2.1 用户资料表 #

sql
-- 创建用户资料表
CREATE TABLE public.profiles (
    id UUID REFERENCES auth.users(id) PRIMARY KEY,
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    username TEXT UNIQUE,
    full_name TEXT,
    avatar_url TEXT,
    website TEXT,
    bio TEXT,
    
    CONSTRAINT username_length CHECK (char_length(username) >= 3)
);

-- 启用RLS
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 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);

2.2 自动创建资料 #

sql
-- 创建触发器函数
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO public.profiles (id, full_name, avatar_url)
    VALUES (
        NEW.id,
        NEW.raw_user_meta_data->>'full_name',
        NEW.raw_user_meta_data->>'avatar_url'
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 创建触发器
CREATE TRIGGER on_auth_user_created
    AFTER INSERT ON auth.users
    FOR EACH ROW
    EXECUTE FUNCTION public.handle_new_user();

2.3 更新用户资料 #

typescript
// 更新auth用户元数据
const { data, error } = await supabase.auth.updateUser({
  data: {
    full_name: 'John Doe',
    avatar_url: 'https://example.com/avatar.jpg',
  },
})

// 更新profiles表
const { error } = await supabase
  .from('profiles')
  .update({
    full_name: 'John Doe',
    bio: 'Software Developer',
  })
  .eq('id', user.id)

2.4 用户资料组件 #

tsx
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'

interface Profile {
  id: string
  username: string
  full_name: string
  avatar_url: string
  bio: string
}

export function ProfileEditor() {
  const [profile, setProfile] = useState<Profile | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchProfile()
  }, [])

  async function fetchProfile() {
    const { data: { user } } = await supabase.auth.getUser()
    
    if (user) {
      const { data } = await supabase
        .from('profiles')
        .select('*')
        .eq('id', user.id)
        .single()
      
      setProfile(data)
    }
    setLoading(false)
  }

  async function updateProfile(updates: Partial<Profile>) {
    const { data: { user } } = await supabase.auth.getUser()
    
    if (user) {
      const { error } = await supabase
        .from('profiles')
        .update(updates)
        .eq('id', user.id)
      
      if (!error) {
        setProfile(prev => prev ? { ...prev, ...updates } : null)
      }
    }
  }

  if (loading) return <div>Loading...</div>
  if (!profile) return <div>No profile</div>

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      updateProfile(profile)
    }}>
      <input
        value={profile.username}
        onChange={(e) => setProfile({ ...profile, username: e.target.value })}
        placeholder="Username"
      />
      <input
        value={profile.full_name}
        onChange={(e) => setProfile({ ...profile, full_name: e.target.value })}
        placeholder="Full Name"
      />
      <textarea
        value={profile.bio}
        onChange={(e) => setProfile({ ...profile, bio: e.target.value })}
        placeholder="Bio"
      />
      <button type="submit">Save</button>
    </form>
  )
}

三、会话管理 #

3.1 获取所有会话 #

typescript
// 获取用户所有会话
const { data, error } = await supabase.auth.getSessions()

data?.sessions.forEach(session => {
  console.log('Session ID:', session.user.id)
  console.log('Created:', session.user.created_at)
})

3.2 刷新会话 #

typescript
// 刷新当前会话
const { data, error } = await supabase.auth.refreshSession()

// 或只刷新token
const { data, error } = await supabase.auth.refreshAccessToken()

3.3 登出选项 #

typescript
// 登出当前设备
await supabase.auth.signOut()

// 登出其他设备
await supabase.auth.signOut({ scope: 'others' })

// 登出所有设备
await supabase.auth.signOut({ scope: 'global' })

四、用户角色管理 #

4.1 角色字段 #

sql
-- 在profiles表添加角色字段
ALTER TABLE profiles ADD COLUMN role TEXT DEFAULT 'user';

-- 添加角色约束
ALTER TABLE profiles ADD CONSTRAINT valid_role 
    CHECK (role IN ('user', 'admin', 'moderator'));

4.2 自定义JWT声明 #

sql
-- 创建函数添加自定义JWT声明
CREATE OR REPLACE FUNCTION auth.jwt()
RETURNS JSONB AS $$
DECLARE
    claims JSONB;
BEGIN
    SELECT jsonb_build_object(
        'role', role,
        'plan', subscription_plan
    )
    INTO claims
    FROM profiles
    WHERE id = auth.uid();
    
    RETURN COALESCE(claims, '{}'::jsonb);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

4.3 在RLS中使用角色 #

sql
-- 管理员可以访问所有数据
CREATE POLICY "Admins have full access"
    ON posts FOR ALL
    USING (
        EXISTS (
            SELECT 1 FROM profiles
            WHERE id = auth.uid() AND role = 'admin'
        )
    );

-- 普通用户只能访问自己的数据
CREATE POLICY "Users can manage own posts"
    ON posts FOR ALL
    USING (author_id = auth.uid());

4.4 客户端检查角色 #

typescript
// 检查用户角色
async function checkUserRole(userId: string): Promise<string> {
  const { data } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', userId)
    .single()
  
  return data?.role || 'user'
}

// 或从JWT获取
const { data: { user } } = await supabase.auth.getUser()
const role = user?.user_metadata?.role

五、用户搜索 #

5.1 管理员搜索用户 #

typescript
// 服务端搜索用户
import { createClient } from '@supabase/supabase-js'

const supabaseAdmin = createClient(url, serviceRoleKey, {
  auth: { autoRefreshToken: false, persistSession: false },
})

// 列出用户
const { data, error } = await supabaseAdmin.auth.admin.listUsers({
  page: 1,
  perPage: 50,
})

// 搜索特定邮箱
const users = data.users.filter(u => 
  u.email?.includes('example.com')
)

5.2 公开用户搜索 #

sql
-- 创建搜索函数
CREATE OR REPLACE FUNCTION search_users(search_query TEXT)
RETURNS SETOF profiles AS $$
BEGIN
    RETURN QUERY
    SELECT * FROM profiles
    WHERE 
        username ILIKE '%' || search_query || '%' OR
        full_name ILIKE '%' || search_query || '%'
    LIMIT 20;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
typescript
// 调用搜索函数
const { data, error } = await supabase.rpc('search_users', {
  search_query: 'john'
})

六、用户删除 #

6.1 用户自行删除 #

typescript
// 用户删除自己的账号
async function deleteAccount() {
  const { data: { user } } = await supabase.auth.getUser()
  
  if (user) {
    // 1. 删除用户数据
    await supabase.from('profiles').delete().eq('id', user.id)
    await supabase.from('posts').delete().eq('author_id', user.id)
    
    // 2. 删除认证用户(需要服务端)
    // 这需要在服务端使用service_role key
  }
}

6.2 管理员删除用户 #

typescript
// 服务端删除用户
const { error } = await supabaseAdmin.auth.admin.deleteUser(userId)

6.3 级联删除设置 #

sql
-- 设置外键级联删除
ALTER TABLE posts
DROP CONSTRAINT posts_author_id_fkey,
ADD CONSTRAINT posts_author_id_fkey
    FOREIGN KEY (author_id) REFERENCES auth.users(id) ON DELETE CASCADE;

七、用户导入导出 #

7.1 批量创建用户 #

typescript
// 批量创建用户
async function importUsers(users: Array<{ email: string; name: string }>) {
  const results = []
  
  for (const user of users) {
    const { data, error } = await supabaseAdmin.auth.admin.createUser({
      email: user.email,
      email_confirm: true,
      user_metadata: {
        full_name: user.name,
      },
    })
    
    results.push({ email: user.email, success: !error, error })
  }
  
  return results
}

7.2 导出用户数据 #

typescript
// 导出用户数据
async function exportUserData(userId: string) {
  const [profile, posts, comments] = await Promise.all([
    supabase.from('profiles').select('*').eq('id', userId).single(),
    supabase.from('posts').select('*').eq('author_id', userId),
    supabase.from('comments').select('*').eq('user_id', userId),
  ])
  
  return {
    profile: profile.data,
    posts: posts.data,
    comments: comments.data,
  }
}

八、安全最佳实践 #

8.1 敏感操作验证 #

typescript
// 敏感操作前验证密码
async function verifyPassword(email: string, password: string): Promise<boolean> {
  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })
  
  return !error
}

// 删除账号前验证
async function deleteAccountWithVerification(
  email: string, 
  password: string
) {
  const verified = await verifyPassword(email, password)
  
  if (!verified) {
    throw new Error('Invalid password')
  }
  
  // 执行删除...
}

8.2 操作日志 #

sql
-- 创建操作日志表
CREATE TABLE user_activity_logs (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id),
    action TEXT NOT NULL,
    details JSONB,
    ip_address TEXT,
    user_agent TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 记录登录
CREATE OR REPLACE FUNCTION log_user_login()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO user_activity_logs (user_id, action)
    VALUES (NEW.id, 'login');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

九、总结 #

用户管理要点:

操作 方法
获取用户 getUser()
更新用户 updateUser()
获取会话 getSession()
刷新会话 refreshSession()
登出 signOut()
管理员操作 auth.admin.*

下一步,让我们学习存储服务!

最后更新:2026-03-28