diff --git a/.gitignore b/.gitignore index 72e1136..5ec3ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.tablet.env diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 0000000..a5ccb20 --- /dev/null +++ b/app/api/auth/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import * as crypto from 'crypto' + +const SECRET = process.env.APP_SECRET || 'smart-home-default-secret-change-me' + +function makeToken(pin: string): string { + return crypto.createHmac('sha256', SECRET).update(pin).digest('hex') +} + +export async function POST(req: Request) { + const { pin } = await req.json() + const correctPin = process.env.APP_PIN || '1234' + + if (pin !== correctPin) { + return NextResponse.json({ error: 'wrong_pin' }, { status: 401 }) + } + + const token = makeToken(correctPin) + const res = NextResponse.json({ success: true }) + + res.cookies.set('auth_token', token, { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', + maxAge: 60 * 60 * 24 * 365, // 1 year — tablet stays logged in + }) + + return res +} + +export async function DELETE() { + const res = NextResponse.json({ success: true }) + res.cookies.delete('auth_token') + return res +} diff --git a/app/page.tsx b/app/page.tsx index 547a321..cce7c4e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,8 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' -import { Thermometer, Droplets, Wind, Calendar, Sun, CloudRain, Snowflake as SnowIcon, Cloud, CloudSun, Zap, Settings as SettingsIcon } from 'lucide-react' +import { useState, useEffect, useCallback, Suspense } from 'react' +import { useSearchParams } from 'next/navigation' +import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete } from 'lucide-react' import Sidebar from '@/components/Sidebar' import TopBar from '@/components/TopBar' import RoomTabs from '@/components/RoomTabs' @@ -92,6 +93,161 @@ function getPm25Level(pm25: number): { label: string; color: string; bg: string return { label: 'Плохо', color: '#f87171', bg: 'rgba(248,113,113,0.12)' } } +// ————— Lock Screen ————— +function LockScreen({ onUnlock }: { onUnlock: () => void }) { + const [pin, setPin] = useState('') + const [error, setError] = useState(false) + const [loading, setLoading] = useState(false) + const [time, setTime] = useState(new Date()) + + useEffect(() => { + const t = setInterval(() => setTime(new Date()), 1000) + return () => clearInterval(t) + }, []) + + const submit = async (fullPin: string) => { + setLoading(true) + setError(false) + try { + const r = await fetch('/api/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pin: fullPin }), + }) + if (r.ok) { + onUnlock() + } else { + setError(true) + setPin('') + setTimeout(() => setError(false), 1500) + } + } catch { + setError(true) + setPin('') + } finally { + setLoading(false) + } + } + + const handleDigit = (d: string) => { + if (pin.length >= 6) return + const next = pin + d + setPin(next) + if (next.length === 4) { + submit(next) + } + } + + const handleDelete = () => { + setPin(p => p.slice(0, -1)) + } + + const digits = ['1','2','3','4','5','6','7','8','9','','0','del'] + + return ( +