add auth and nicer styling
This commit is contained in:
40
package-lock.json
generated
40
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.84.2",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -2426,6 +2427,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3713,6 +3723,30 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iron-session": {
|
||||||
|
"version": "8.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.4.tgz",
|
||||||
|
"integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/vvo",
|
||||||
|
"https://github.com/sponsors/brc-dd"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^0.7.2",
|
||||||
|
"iron-webcrypto": "^1.2.1",
|
||||||
|
"uncrypto": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iron-webcrypto": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/brc-dd"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -6024,6 +6058,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uncrypto": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.84.2",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
26
src/app/api/login/route.ts
Normal file
26
src/app/api/login/route.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getSession } from "@/lib/auth";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { password } = await req.json();
|
||||||
|
|
||||||
|
if (password !== process.env.PASSWORD) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid password" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getSession();
|
||||||
|
session.authenticated = true;
|
||||||
|
await session.save();
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Authentication failed" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
src/app/api/logout/route.ts
Normal file
17
src/app/api/logout/route.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getSession } from "@/lib/auth";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
session.destroy();
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Logout failed" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getIronSession } from "iron-session";
|
||||||
|
import { sessionOptions, SessionData } from "@/lib/auth";
|
||||||
|
|
||||||
type MerchantUserResponse = {
|
type MerchantUserResponse = {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -59,6 +61,19 @@ async function fetchOidcToken(): Promise<string> {
|
|||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
// require valid session
|
||||||
|
const session = await getIronSession<SessionData>(
|
||||||
|
req,
|
||||||
|
NextResponse.next(),
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
if (!session.authenticated) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Not authenticated" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const token = await fetchOidcToken();
|
const token = await fetchOidcToken();
|
||||||
|
|
||||||
const apiBase = process.env.ID_CARD_API_BASE_URL;
|
const apiBase = process.env.ID_CARD_API_BASE_URL;
|
||||||
|
13
src/app/api/session/route.ts
Normal file
13
src/app/api/session/route.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getSession } from "@/lib/auth";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
const isAuthenticated = session.authenticated || false;
|
||||||
|
|
||||||
|
return NextResponse.json({ authenticated: isAuthenticated });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ authenticated: false });
|
||||||
|
}
|
||||||
|
}
|
@@ -1,23 +1,30 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
/* :root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
} */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #161516;
|
||||||
|
--foreground: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
--color-golden-orange: #ff6915;
|
||||||
|
--color-neuf-black: #161516;
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ededed;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Scanner from "@/components/Scanner";
|
import Scanner from "@/components/Scanner";
|
||||||
|
import LoginForm from "@/components/LoginForm";
|
||||||
import {
|
import {
|
||||||
useMutation,
|
useMutation,
|
||||||
QueryClient,
|
QueryClient,
|
||||||
@@ -18,25 +19,40 @@ type SessionResponse = {
|
|||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SessionInfo({ session }: { session: SessionResponse }) {
|
function SessionInfo({
|
||||||
|
session,
|
||||||
|
onClear,
|
||||||
|
}: {
|
||||||
|
session: SessionResponse;
|
||||||
|
onClear: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto mt-6 p-6 rounded-xl shadow-lg border border-gray-200 bg-gradient-to-br from-gray-50 to-gray-200 space-y-4">
|
<div className="max-w-md mx-auto mt-2 p-2 bg-neuf-black space-y-4">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={onClear}
|
||||||
|
className="w-8 h-8 bg-red-600 text-white rounded-full hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 flex items-center justify-center font-bold text-lg transition-colors duration-200"
|
||||||
|
aria-label="Clear session"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Image
|
<Image
|
||||||
src={`data:image/jpeg;base64,${session.documentPhoto}`}
|
src={`data:image/jpeg;base64,${session.documentPhoto}`}
|
||||||
alt="Document"
|
alt="Document"
|
||||||
width={200}
|
width={200}
|
||||||
height={261}
|
height={261}
|
||||||
className="w-40 h-50 object-cover rounded-lg mb-4 border border-gray-300 shadow"
|
className="w-40 h-50 object-cover rounded-lg mb-4 border border-black-300 shadow"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
{(session.firstName || session.lastName) && (
|
{(session.firstName || session.lastName) && (
|
||||||
<div className="text-xl font-bold text-gray-800">
|
<div className="text-xl font-bold text-white">
|
||||||
{[session.firstName, session.lastName].filter(Boolean).join(" ")}
|
{[session.firstName, session.lastName].filter(Boolean).join(" ")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{typeof session.age === "number" && (
|
{typeof session.age === "number" && (
|
||||||
<div className="text-3xl font-extrabold text-blue-700 mt-2">
|
<div className="text-3xl font-extrabold text-white mt-2">
|
||||||
{session.age} år
|
{session.age} år
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -57,8 +73,17 @@ async function lookupSession(sessionId: string) {
|
|||||||
function HomeInner() {
|
function HomeInner() {
|
||||||
const [session, setSession] = useState<SessionResponse | null>(null);
|
const [session, setSession] = useState<SessionResponse | null>(null);
|
||||||
const [scannedCodes, setScannedCodes] = useState<Set<string>>(new Set());
|
const [scannedCodes, setScannedCodes] = useState<Set<string>>(new Set());
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||||
const pendingCodeRef = useRef<string | null>(null);
|
const pendingCodeRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// check for valid session on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/session")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setIsAuthenticated(data.authenticated))
|
||||||
|
.catch(() => setIsAuthenticated(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: lookupSession,
|
mutationFn: lookupSession,
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
@@ -91,10 +116,52 @@ function HomeInner() {
|
|||||||
mutation.mutate(decodedText);
|
mutation.mutate(decodedText);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearSession = () => {
|
||||||
|
setSession(null);
|
||||||
|
setScannedCodes(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleLogout = async () => {
|
||||||
|
// try {
|
||||||
|
// await fetch("/api/logout", { method: "POST" });
|
||||||
|
// setIsAuthenticated(false);
|
||||||
|
// setSession(null);
|
||||||
|
// setScannedCodes(new Set());
|
||||||
|
// } catch {
|
||||||
|
// alert("Noe gikk galt ved utlogging");
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
if (isAuthenticated === null) {
|
||||||
|
return (
|
||||||
|
<div className="text-center mt-20 text-white bg-neuf-black min-h-screen">
|
||||||
|
Laster...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<div className="bg-neuf-black min-h-screen">
|
||||||
|
<LoginForm onLogin={() => setIsAuthenticated(true)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="bg-neuf-black min-h-screen">
|
||||||
|
{/* <div className="flex justify-end p-4">
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||||
|
>
|
||||||
|
Logg ut
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
<Scanner onScan={handleScan} />
|
<Scanner onScan={handleScan} />
|
||||||
{session && <SessionInfo session={session} />}
|
{session && (
|
||||||
|
<SessionInfo session={session} onClear={handleClearSession} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
77
src/components/LoginForm.tsx
Normal file
77
src/components/LoginForm.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
onLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginForm({ onLogin }: LoginFormProps) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
onLogin();
|
||||||
|
} else {
|
||||||
|
setError("Ugyldig passord");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Noe gikk galt :(");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mt-20 p-6 bg-neuf-black">
|
||||||
|
<h1 className="text-2xl font-bold text-center mb-6 text-white">
|
||||||
|
Oldeneuf?
|
||||||
|
</h1>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-300 mb-1"
|
||||||
|
>
|
||||||
|
Passord
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-golden-orange text-neuf-black py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 disabled:opacity-50"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Logger inn..." : "Logg inn"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -24,6 +24,8 @@ export default function Scanner({ onScan }: ScannerProps) {
|
|||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
const [highlightStyle, setHighlightStyle] = useState("default-style");
|
const [highlightStyle, setHighlightStyle] = useState("default-style");
|
||||||
const [showScanRegion, setShowScanRegion] = useState(false);
|
const [showScanRegion, setShowScanRegion] = useState(false);
|
||||||
|
const [hasStartedOnce, setHasStartedOnce] = useState(false);
|
||||||
|
const showControls = false;
|
||||||
|
|
||||||
const handleScanResult = (result: QrScanner.ScanResult) => {
|
const handleScanResult = (result: QrScanner.ScanResult) => {
|
||||||
onScan(result.data);
|
onScan(result.data);
|
||||||
@@ -53,6 +55,7 @@ export default function Scanner({ onScan }: ScannerProps) {
|
|||||||
|
|
||||||
await scannerRef.current.start();
|
await scannerRef.current.start();
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
|
setHasStartedOnce(true);
|
||||||
await updateFlashAvailability();
|
await updateFlashAvailability();
|
||||||
|
|
||||||
// expects both light and dark QR codes
|
// expects both light and dark QR codes
|
||||||
@@ -70,6 +73,7 @@ export default function Scanner({ onScan }: ScannerProps) {
|
|||||||
try {
|
try {
|
||||||
await scannerRef.current.start();
|
await scannerRef.current.start();
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
|
setHasStartedOnce(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Failed to start scanner: ${err}`);
|
setError(`Failed to start scanner: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -137,124 +141,133 @@ export default function Scanner({ onScan }: ScannerProps) {
|
|||||||
<div className={getVideoContainerClass()}>
|
<div className={getVideoContainerClass()}>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className="w-full h-auto rounded-lg border-2 border-gray-300"
|
className="w-full h-auto rounded-lg border-2 border-gray-300 cursor-pointer"
|
||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
|
onClick={isScanning ? stopScanner : startScanner}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-2 space-y-2">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{/* Video Container */}
|
{!hasStartedOnce && (
|
||||||
|
<p className="text-white-600 mb-4">
|
||||||
|
Trykk på området over for å skru av og på kameraet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="mt-4 space-y-4">
|
{showControls && (
|
||||||
{/* Start/Stop Toggle Button */}
|
<>
|
||||||
<div className="flex gap-2 justify-center">
|
<div className="mt-0 space-y-2">
|
||||||
<button
|
{/* Start/Stop Toggle Button */}
|
||||||
onClick={isScanning ? stopScanner : startScanner}
|
<div className="flex gap-2 justify-center">
|
||||||
className={`px-4 py-2 text-white rounded-lg transition-colors ${
|
<button
|
||||||
isScanning
|
onClick={isScanning ? stopScanner : startScanner}
|
||||||
? "bg-red-500 hover:bg-red-600"
|
className={`px-2 py-2 text-white rounded-lg transition-colors ${
|
||||||
: "bg-green-500 hover:bg-green-600"
|
isScanning
|
||||||
}`}
|
? "bg-red-500 hover:bg-red-600"
|
||||||
>
|
: "bg-green-500 hover:bg-green-600"
|
||||||
{isScanning ? "Stop Scanner" : "Start Scanner"}
|
}`}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Flash Toggle */}
|
|
||||||
{/* {hasFlash && (
|
|
||||||
<button
|
|
||||||
onClick={toggleFlash}
|
|
||||||
className="px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
|
|
||||||
>
|
|
||||||
📸 Flash: {isFlashOn ? "on" : "off"}
|
|
||||||
</button>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: "none" }}>
|
|
||||||
{/* Settings */}
|
|
||||||
<div className="mt-6 space-y-4 text-left">
|
|
||||||
{/* Camera Selection */}
|
|
||||||
{cameras.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Camera:
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={selectedCamera}
|
|
||||||
onChange={(e) => handleCameraChange(e.target.value)}
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
>
|
||||||
<option value="environment">
|
{isScanning ? "Stopp" : "Start"}
|
||||||
Environment Facing (Back Camera)
|
</button>
|
||||||
</option>
|
|
||||||
<option value="user">User Facing (Front Camera)</option>
|
|
||||||
{cameras.map((camera) => (
|
|
||||||
<option key={camera.id} value={camera.id}>
|
|
||||||
{camera.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{/* Flash Toggle */}
|
||||||
|
{hasFlash && (
|
||||||
{/* Highlight Style */}
|
<button
|
||||||
<div>
|
onClick={toggleFlash}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
className="px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
|
||||||
Highlight Style:
|
>
|
||||||
</label>
|
📸 Flash: {isFlashOn ? "on" : "off"}
|
||||||
<select
|
</button>
|
||||||
value={highlightStyle}
|
)}
|
||||||
onChange={(e) => setHighlightStyle(e.target.value)}
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
<option value="default-style">Default style</option>
|
|
||||||
<option value="example-style-1">Custom style 1</option>
|
|
||||||
<option value="example-style-2">Custom style 2</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show Scan Region */}
|
<div>
|
||||||
<div className="flex items-center">
|
{/* Settings */}
|
||||||
<input
|
<div className="mt-2 space-y-2 text-left">
|
||||||
id="show-scan-region"
|
{/* Camera Selection */}
|
||||||
type="checkbox"
|
{cameras.length > 0 && (
|
||||||
checked={showScanRegion}
|
<div>
|
||||||
onChange={(e) => setShowScanRegion(e.target.checked)}
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
className="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
Camera:
|
||||||
/>
|
</label>
|
||||||
<label
|
<select
|
||||||
htmlFor="show-scan-region"
|
value={selectedCamera}
|
||||||
className="text-sm font-medium text-gray-700"
|
onChange={(e) => handleCameraChange(e.target.value)}
|
||||||
>
|
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
Show scan region canvas
|
>
|
||||||
</label>
|
<option value="environment">
|
||||||
</div>
|
Environment Facing (Back Camera)
|
||||||
</div>
|
</option>
|
||||||
|
<option value="user">User Facing (Front Camera)</option>
|
||||||
|
{cameras.map((camera) => (
|
||||||
|
<option key={camera.id} value={camera.id}>
|
||||||
|
{camera.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Status Information */}
|
{/* Highlight Style */}
|
||||||
<div className="mt-6 space-y-2 text-sm text-gray-600">
|
<div>
|
||||||
<div>
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<span className="font-medium">Device has camera:</span>{" "}
|
Highlight Style:
|
||||||
{hasCamera !== null
|
</label>
|
||||||
? hasCamera
|
<select
|
||||||
? "Yes"
|
value={highlightStyle}
|
||||||
: "No"
|
onChange={(e) => setHighlightStyle(e.target.value)}
|
||||||
: "Checking..."}
|
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
</div>
|
>
|
||||||
<div>
|
<option value="default-style">Default style</option>
|
||||||
<span className="font-medium">Camera has flash:</span>{" "}
|
<option value="example-style-1">Custom style 1</option>
|
||||||
{hasFlash ? "Yes" : "No"}
|
<option value="example-style-2">Custom style 2</option>
|
||||||
</div>
|
</select>
|
||||||
{error && (
|
</div>
|
||||||
<div className="text-red-600 font-medium">
|
|
||||||
<span className="font-medium">Error:</span> {error}
|
{/* Show Scan Region */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
id="show-scan-region"
|
||||||
|
type="checkbox"
|
||||||
|
checked={showScanRegion}
|
||||||
|
onChange={(e) => setShowScanRegion(e.target.checked)}
|
||||||
|
className="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-scan-region"
|
||||||
|
className="text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Show scan region canvas
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* Status Information */}
|
||||||
</div>
|
<div className="mt-6 space-y-2 text-sm text-gray-600">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Device has camera:</span>{" "}
|
||||||
|
{hasCamera !== null
|
||||||
|
? hasCamera
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: "Checking..."}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Camera has flash:</span>{" "}
|
||||||
|
{hasFlash ? "Yes" : "No"}
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-600 font-medium">
|
||||||
|
<span className="font-medium">Error:</span> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
|
21
src/lib/auth.ts
Normal file
21
src/lib/auth.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { SessionOptions } from "iron-session";
|
||||||
|
import { getIronSession } from "iron-session";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export interface SessionData {
|
||||||
|
authenticated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionOptions: SessionOptions = {
|
||||||
|
password: process.env.SESSION_SECRET!,
|
||||||
|
cookieName: "oldeneuf-session",
|
||||||
|
cookieOptions: {
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getSession() {
|
||||||
|
return await getIronSession<SessionData>(await cookies(), sessionOptions);
|
||||||
|
}
|
Reference in New Issue
Block a user