313 lines
8.9 KiB
Markdown
313 lines
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
|