313 lines
No EOL
8.9 KiB
Markdown
313 lines
No EOL
8.9 KiB
Markdown
# Система регистрации пользователей PocketBase
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ sign-up.astro │ -> │ sign-up.ts API │ -> │ PocketBase │
|
||
│ (форма) │ │ (создание) │ │ (запись) │
|
||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
│
|
||
v
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ verify.astro │ -> │ confirm.ts API │ -> │ PocketBase │
|
||
│ (страница) │ │ (верификация) │ │ (обновление) │
|
||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
```
|
||
|
||
## Файловая структура
|
||
|
||
```
|
||
frontend/src/
|
||
├── pages/
|
||
│ ├── auth/
|
||
│ │ ├── sign-up.astro # Форма регистрации
|
||
│ │ ├── sign-in.astro # Форма входа
|
||
│ │ └── verify.astro # Страница подтверждения
|
||
│ └── api/
|
||
│ └── auth/
|
||
│ ├── sign-up.ts # API регистрации
|
||
│ ├── sign-in.ts # API входа
|
||
│ ├── confirm.ts # API подтверждения
|
||
│ └── logout.ts # API выхода
|
||
└── lib/
|
||
└── email.ts # Отправка писем
|
||
```
|
||
|
||
---
|
||
|
||
## 1. Регистрация (sign-up.ts)
|
||
|
||
### Создание пользователя
|
||
|
||
```typescript
|
||
import PocketBase from 'pocketbase';
|
||
|
||
const pb = new PocketBase(POCKETBASE_URL);
|
||
|
||
// Создаём пользователя
|
||
const record = await pb.collection('users').create({
|
||
firstName,
|
||
lastName,
|
||
email,
|
||
phone,
|
||
password,
|
||
passwordConfirm: password,
|
||
emailVisibility: true,
|
||
});
|
||
```
|
||
|
||
**Важно:**
|
||
- `email` должен быть уникальным
|
||
- `password` минимум 8 символов
|
||
- `passwordConfirm` должен совпадать с `password`
|
||
|
||
### Генерация токена подтверждения
|
||
|
||
```typescript
|
||
// Создаём свой токен (не от PocketBase)
|
||
const token = Buffer.from(`${record.id}:${email}:${Date.now()}`)
|
||
.toString('base64')
|
||
.replace(/=/g, '');
|
||
|
||
// Формируем ссылку
|
||
const verifyLink = `${SITE_URL}/auth/verify?token=${token}&userId=${record.id}`;
|
||
```
|
||
|
||
**Формат токена:** `userId:email:timestamp`
|
||
|
||
### Отправка письма
|
||
|
||
```typescript
|
||
import { sendEmail, generateVerifyEmailHtml } from '@/lib/email';
|
||
|
||
const html = generateVerifyEmailHtml(firstName, verifyLink);
|
||
await sendEmail({
|
||
to: email,
|
||
subject: 'Подтверждение регистрации',
|
||
html
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Подтверждение email (confirm.ts)
|
||
|
||
### Валидация токена
|
||
|
||
```typescript
|
||
const decodeToken = (token: string) => {
|
||
const decoded = Buffer.from(token, 'base64').toString('utf8');
|
||
const [userId, email, timestamp] = decoded.split(':');
|
||
|
||
// Проверяем срок (24 часа)
|
||
const tokenTime = parseInt(timestamp);
|
||
const now = Date.now();
|
||
const maxAge = 24 * 60 * 60 * 1000;
|
||
|
||
if (now - tokenTime > maxAge) {
|
||
throw new Error('Срок действия ссылки истёк');
|
||
}
|
||
|
||
return { userId, email };
|
||
};
|
||
```
|
||
|
||
### Обновление verified
|
||
|
||
Для обновления системного поля `verified` нужен админ-доступ:
|
||
|
||
```typescript
|
||
// Аутентификация как superuser
|
||
const authResponse = await fetch(`${PB_URL}/api/collections/_superusers/auth-with-password`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
identity: PB_ADMIN_EMAIL,
|
||
password: PB_ADMIN_PASSWORD,
|
||
}),
|
||
});
|
||
|
||
const { token } = await authResponse.json();
|
||
|
||
// Обновляем пользователя
|
||
await fetch(`${PB_URL}/api/collections/users/records/${userId}`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify({ verified: true }),
|
||
});
|
||
```
|
||
|
||
**Важно:**
|
||
- Endpoint: `_superusers` (с подчёркиванием!)
|
||
- Не `admins` или `/api/admins/...`
|
||
|
||
### Переменные окружения (.env)
|
||
|
||
```env
|
||
POCKETBASE_URL=http://127.0.0.1:8090
|
||
PB_ADMIN_EMAIL=admin@example.com
|
||
PB_ADMIN_PASSWORD=secret_password
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Страница подтверждения (verify.astro)
|
||
|
||
### Client-side логика
|
||
|
||
```astro
|
||
<script>
|
||
async function verifyEmail() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const token = urlParams.get('token');
|
||
const userId = urlParams.get('userId');
|
||
|
||
const response = await fetch('/api/auth/confirm', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token, userId }),
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
// Показать успех
|
||
} else {
|
||
// Показать ошибку
|
||
}
|
||
}
|
||
|
||
if (token && userId) {
|
||
verifyEmail();
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Вход пользователя (sign-in.ts)
|
||
|
||
### Аутентификация
|
||
|
||
```typescript
|
||
const pb = new PocketBase(POCKETBASE_URL);
|
||
|
||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||
|
||
// После успешного входа
|
||
console.log(pb.authStore.isValid); // true
|
||
console.log(pb.authStore.token); // JWT токен
|
||
console.log(pb.authStore.record); // данные пользователя
|
||
```
|
||
|
||
### Проверка верификации
|
||
|
||
```typescript
|
||
// При входе проверяем verified
|
||
if (!authData.record.verified) {
|
||
return new Response(JSON.stringify({
|
||
error: 'Email не подтверждён'
|
||
}), { status: 401 });
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Типичные ошибки
|
||
|
||
### 1. 404 при authWithPassword для админа
|
||
|
||
**Проблема:** Используется неправильный endpoint
|
||
```javascript
|
||
// ❌ Неправильно
|
||
/api/admins/auth-with-password
|
||
|
||
// ✅ Правильно
|
||
/api/collections/_superusers/auth-with-password
|
||
```
|
||
|
||
### 2. Нельзя указать verified при создании
|
||
|
||
**Проблема:** `verified` — системное поле
|
||
```javascript
|
||
// ❌ Ошибка
|
||
pb.collection('users').create({
|
||
email,
|
||
password,
|
||
verified: true // Нельзя!
|
||
});
|
||
```
|
||
|
||
**Решение:** Обновлять через отдельный запрос с админ-доступом
|
||
|
||
### 3. Токен верификации от PocketBase
|
||
|
||
**Проблема:** PocketBase `confirmVerification` требует специальный токен
|
||
|
||
**Решение:** Создать свой API эндпоинт для валидации
|
||
|
||
### 4. CORS ошибки
|
||
|
||
**Реш<D0B5><D188>ние:** Настроить CORS в PocketBase Admin UI
|
||
|
||
---
|
||
|
||
## PocketBase SDK методы
|
||
|
||
### Users
|
||
|
||
```typescript
|
||
// Создать пользователя
|
||
pb.collection('users').create(data)
|
||
|
||
// Аутентифицировать
|
||
pb.collection('users').authWithPassword(email, password)
|
||
|
||
// Запросить верификацию
|
||
pb.collection('users').requestVerification(email)
|
||
|
||
// Подтвердить верификацию
|
||
pb.collection('users').confirmVerification(token)
|
||
|
||
// Обновить данные
|
||
pb.collection('users').update(id, data)
|
||
|
||
// Получить текущего пользователя
|
||
pb.collection('users').authRefresh()
|
||
```
|
||
|
||
### Admins
|
||
|
||
```typescript
|
||
// Аутентификация superuser
|
||
pb.admins.authWithPassword(email, password)
|
||
|
||
// Имперсонация
|
||
pb.collection('users').impersonate(userId, duration)
|
||
```
|
||
|
||
---
|
||
|
||
## Checklist при создании системы регистрации
|
||
|
||
- [ ] 1. Создать API endpoint `/api/auth/sign-up`
|
||
- [ ] 2. Создать форму `sign-up.astro`
|
||
- [ ] 3. Настроить email отправку в `lib/email.ts`
|
||
- [ ] 4. Создать API endpoint `/api/auth/confirm`
|
||
- [ ] 5. Создать страницу `verify.astro`
|
||
- [ ] 6. Добавить переменные в `.env`
|
||
- [ ] 7. Протестировать регистрацию
|
||
- [ ] 8. Протестировать подтверждение
|
||
- [ ] 9. Протестировать вход
|
||
|
||
---
|
||
|
||
## Ссылки
|
||
|
||
- PocketBase Auth: https://pocketbase.io/docs.authentication
|
||
- PocketBase SDK: https://github.com/pocketbase/js-sdk
|
||
- PocketBase API: https://pocketbase.io/docs/api-records |