Merge pull request #36 from varun-raj/feat-auth

Feat: Authentication based on immich's existing credentials
This commit is contained in:
Varun Raj Manoharan authored and GitHub committed 2024-09-04 20:11:45 +05:30
commit c8d0cf8f5e
33 files changed
+487 -87

No files matched your search

BIN
View File
Binary file not shown.
+1
View File
@@ -56,6 +56,7 @@
"@semantic-release/github": "^10.1.7",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.1",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/pg": "^8.11.6",
"@types/react": "^18",
+113
View File
@@ -0,0 +1,113 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useState } from "react"
import { loginUser } from "@/handlers/api/user.handler"
import { useConfig } from "@/contexts/ConfigContext"
import { IUser } from "@/types/user"
import Head from "next/head"
import { Tooltip } from "../ui/tooltip"
import { set } from "date-fns"
export const description =
"A login form with email and password. There's an option to login with Google and a link to sign up if you don't have an account."
interface ILoginFormProps {
onLogin: (user: IUser) => void
}
export function LoginForm(
{ onLogin }: ILoginFormProps
) {
const { exImmichUrl } = useConfig()
const [formData, setFormData] = useState({
email: "",
password: "",
})
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.id]: e.target.value })
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
setErrorMessage(null)
loginUser(formData.email, formData.password)
.then(onLogin)
.catch((error) => {
setErrorMessage(error.message)
}).finally(() => {
setLoading(false)
})
}
return (
<>
<Head>
<title>Immich Power Tools - Login</title>
</Head>
<div className="relative flex min-h-screen justify-center flex-col bg-background">
<Card className="mx-auto max-w-sm">
<CardHeader>
<img
src="/favicon.png"
width={32}
height={32}
alt="Immich Power Tools"
className="w-8 h-8"
/>
<CardTitle className="text-2xl">Login to Immich</CardTitle>
<CardDescription>
Login to your connected Immich instance <Link href={exImmichUrl} className="text-xs" target="_blank">({exImmichUrl})</Link>
</CardDescription>
</CardHeader>
<CardContent>
<form className="grid gap-4" onSubmit={handleSubmit}>
{errorMessage && (
<div className="border text-center border-red-500 text-red-500 p-2 rounded-lg text-xs" role="alert">
<p>{errorMessage}</p>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
onChange={handleChange}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" required
onChange={handleChange}
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
Login
</Button>
<Button variant="outline" className="w-full" disabled={true}>
Login with OAuth (Comming Soon)
</Button>
<p className="text-xs text-gray-400 text-center">
Or Use <code>IMMICH_API_KEY</code> in your environment to use API Key instead of password.
</p>
</form>
</CardContent>
</Card>
</div>
</>
)
}
+42 -13
View File
@@ -8,12 +8,15 @@ import ErrorBlock from "../shared/ErrorBlock";
import Loader from "../ui/loader";
import { ArrowRight, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { LoginForm } from "../auth/LoginForm";
import { useConfig } from "@/contexts/ConfigContext";
type RootLayoutProps = {
children: ReactNode;
};
export default function RootLayout({ children }: RootLayoutProps) {
const { immichURL, exImmichUrl } = useConfig();
const [user, setUser] = useState<IUser | null>(null);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState<{
@@ -25,15 +28,21 @@ export default function RootLayout({ children }: RootLayoutProps) {
return getMe()
.then(setUser)
.catch((error) => {
setErrorMessage(error);
if (error.status === 403) return setUser(null);
return setErrorMessage(error);
})
.finally(() => setLoading(false));
};
const handleLoginCompletion = (user: IUser) => {
setUser(user);
}
useEffect(() => {
fetchData();
}, []);
if (loading) return <Loader />;
if (errorMessage)
return (
@@ -43,24 +52,44 @@ export default function RootLayout({ children }: RootLayoutProps) {
title={errorMessage.error}
description={errorMessage.message}
action={
<Link
className="flex items-center gap-2 text-xs"
href={
"https://github.com/varun-raj/immich-power-tools?tab=readme-ov-file#-getting-started"
}
target="_blank"
>
Setup Instructions
<ArrowRight className="h-4 w-4" />
</Link>
<div className="flex flex-col items-center">
<Link
className="flex items-center gap-2 text-xs"
href={
"https://github.com/varun-raj/immich-power-tools?tab=readme-ov-file#-getting-started"
}
target="_blank"
>
Setup Instructions
<ArrowRight className="h-4 w-4" />
</Link>
<div className="text-xs flex flex-col gap-4 py-4 w-full">
<div className="text-center">
<p className="text-muted-foreground font-mono">Connected to (External)</p>
<Link href={exImmichUrl || ""} target="_blank" className="font-mono">
{exImmichUrl || "Not Set"}
</Link>
</div>
<div className="text-center">
<p className="text-muted-foreground font-mono">Connected to (Internal)</p>
<Link href={immichURL} target="_blank" className="font-mono">
{immichURL}
</Link>
</div>
</div>
</div>
}
/>
</div>
);
if (!user) return null;
if (!user) return <LoginForm onLogin={handleLoginCompletion} />;
return (
<UserContext.Provider value={user}>
<UserContext.Provider value={{
...user,
updateContext: setUser,
}}>
<div className="grid max-h-screen min-h-screen w-full md:grid-cols-[200px_1fr] lg:grid-cols-[240px_1fr]">
<Sidebar />
<div className="flex flex-col">{children}</div>
+2
View File
@@ -1,3 +1,5 @@
import { useConfig } from '@/contexts/ConfigContext';
import { Link } from 'lucide-react';
import React from 'react'
interface ErrorBlockProps {
+24 -22
View File
@@ -1,31 +1,26 @@
import { getMe } from "@/handlers/api/user.handler";
import { IUser } from "@/types/user";
import React, { useEffect, useState } from "react";
import React from "react";
import { Button } from "@/components/ui/button"
import Link from "next/link";
import { useConfig } from "@/contexts/ConfigContext";
import { logoutUser } from "@/handlers/api/user.handler"
import { useCurrentUser } from "@/contexts/CurrentUserContext";
import { useToast } from "../ui/use-toast";
export default function ProfileInfo() {
const { immichURL,exImmichUrl } = useConfig();
const [user, setUser] = React.useState<IUser | null>(null);
const [loading, setLoading] = React.useState(true);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { updateContext, ...user } = useCurrentUser()
const { immichURL, exImmichUrl } = useConfig();
const toast = useToast();
const fetchData = () => {
return getMe()
.then(setUser)
.catch((error) => {
setErrorMessage(error.message);
const handleLogout = () => logoutUser()
.then(() => {
updateContext(null)
}).catch((error) => {
toast.toast({
title: "Error",
description: error.message,
})
.finally(() => setLoading(false));
};
})
useEffect(() => {
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (errorMessage) return <div>{errorMessage}</div>;
return (
<>
@@ -38,7 +33,14 @@ export default function ProfileInfo() {
<Link href={immichURL} target="_blank" className="font-mono">
{immichURL}
</Link>
<p className="text-sm">{user?.name}</p>
{user && (
<>
<p className="text-sm">{user?.name}</p>
{!user.isUsingAPIKey && <Button variant="destructive" className="mx-4" onClick={handleLogout}>
Log Out
</Button>}
</>
)}
</div>
<div className="border text-muted-foreground text-xs text-center py-2">
<p>
-1
View File
@@ -53,7 +53,6 @@ export default function Sidebar() {
</div>
<div>
<ProfileInfo />
</div>
</div>
</div>
+3 -3
View File
@@ -76,12 +76,12 @@ CardFooter.displayName = "CardFooter"
interface CardProps {
title?: string
description?: string
className?: string
children: React.ReactNode
}
const Card = ({ children, title, description }: CardProps) => {
const Card = ({ children, title, description, className }: CardProps) => {
return (
<CardRoot>
<CardRoot className={className}>
{!!(title || description) && (
<CardHeader>
{title && <CardTitle>{title}</CardTitle>}
+6
View File
@@ -0,0 +1,6 @@
import { ENV } from "./environment";
export const appConfig = {
sessionCookieName: 'power-tools-session',
jwtToken: ENV.JWT_SECRET || "jwt-secret", // This is a fallback value, it should be replaced with a proper secret
}
+1
View File
@@ -3,6 +3,7 @@ export const ENV = {
EXTERNAL_IMMICH_URL: (process.env.EXTERNAL_IMMICH_URL || process.env.IMMICH_URL) as string,
IMMICH_API_KEY: process.env.IMMICH_API_KEY as string,
DATABASE_URL: (process.env.DATABASE_URL || `postgresql://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@${(process.env.DB_HOST || 'immich_postgres')}:${(process.env.DB_PORT || '5432')}/${process.env.DB_DATABASE_NAME}`),
JWT_SECRET: process.env.JWT_SECRET as string,
};
+4 -1
View File
@@ -1,7 +1,10 @@
export const BASE_API_ENDPOINT = '/api';
export const BASE_PROXY_ENDPOINT = BASE_API_ENDPOINT + '/immich-proxy';
export const GET_ME_PATH = BASE_PROXY_ENDPOINT + "/users/me";
export const GET_ME_PATH = BASE_API_ENDPOINT + "/users/me";
export const LOGIN_PATH = BASE_API_ENDPOINT + "/users/login";
export const LOGOUT_PATH = BASE_API_ENDPOINT + "/users/logout";
export const LIST_PEOPLE_PATH = BASE_API_ENDPOINT + "/people/list";
export const SEARCH_PEOPLE_PATH = BASE_PROXY_ENDPOINT + "/search/person";
+3 -1
View File
@@ -1,7 +1,9 @@
import { IUser } from "@/types/user";
import { createContext, useContext } from "react";
interface IUserContextType extends IUser {}
interface IUserContextType extends IUser {
updateContext: (user: IUser | null) => void;
}
const UserContext = createContext<IUserContextType | null>(null);
+9 -1
View File
@@ -1,6 +1,14 @@
import { GET_ME_PATH } from "@/config/routes";
import { GET_ME_PATH, LOGIN_PATH, LOGOUT_PATH } from "@/config/routes";
import API from "@/lib/api";
export const getMe = async () => {
return API.get(GET_ME_PATH);
}
export const loginUser = async (email: string, password: string) => {
return API.post(LOGIN_PATH, { email, password });
}
export const logoutUser = async () => {
return API.post(LOGOUT_PATH);
}
+75 -2
View File
@@ -1,15 +1,88 @@
import { appConfig } from "@/config/app.config"
import { ENV } from "@/config/environment"
import { getCookie } from "@/lib/cookie"
import { NextApiRequest } from "next"
export const getCurrentUserFromAPIKey = () => {
if (!ENV.IMMICH_URL || !ENV.IMMICH_API_KEY) return null;
export const getCurrentUser = () => {
return fetch(ENV.IMMICH_URL + "/api/users/me", {
headers: {
'x-api-key': ENV.IMMICH_API_KEY,
Accept: "application/json",
},
}).then((res) => {
if (res.ok) {
return res.json().then((user) => {
return {
...user,
isUsingAPIKey: true,
}
})
}
return null
})
}
export const getCurrentUserFromAccessToken = (token: string) => {
return fetch(ENV.IMMICH_URL + "/api/users/me", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
}).then((res) => {
if (res.ok) {
return res.json()
}
throw new Error("Failed to fetch user")
return null
})
}
export const getCurrentUser = async (req: NextApiRequest) => {
const session = getCookie(req, appConfig.sessionCookieName)
if (session) {
const user = await getCurrentUserFromAccessToken(session)
if (!user) return null
return {
...user,
accessToken: session,
}
}
return getCurrentUserFromAPIKey()
}
export const loginUser = async (email: string, password: string) => {
const res = await fetch(ENV.IMMICH_URL + "/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
})
if (res.ok) {
return res.json()
}
return null
}
export const logoutUser = async (Authorization: string) => {
const res = await fetch(ENV.IMMICH_URL + "/api/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization" : "Bearer " + Authorization
},
})
if (res.ok) {
return res.json()
}
return null
}
+21
View File
@@ -0,0 +1,21 @@
import { ENV } from "@/config/environment"
import { IUser } from "@/types/user"
export const getUserHeaders = (user: IUser, otherHeaders?: {
'Content-Type': string
}) => {
let headers: {
'Content-Type': string;
'x-api-key'?: string;
'Authorization'?: string
} = {
'Content-Type': 'application/json',
}
if (user.isUsingAPIKey) {
headers['x-api-key'] = ENV.IMMICH_API_KEY
}
else {
headers['Authorization'] = `Bearer ${user.accessToken}`
}
return {...headers, ...otherHeaders}
}
+5 -13
View File
@@ -7,23 +7,15 @@ const handleErrors = (error: AxiosError | any) => {
if (error.code === "ERR_BAD_RESPONSE") {
return Promise.reject({
error: "Unable to process this request, please try again later",
message: error.code
message: error.code,
});
}
let errorObject = {}
if (error.response) {
const { status } = error.response
if (status === 422) {
errorObject = {
...error.response.data,
message: 'Unable to process this request, please try again later',
}
} else {
errorObject = {
...error.response.data,
}
}
return Promise.reject(errorObject)
return Promise.reject({
...error.response.data,
status: error.response.status,
})
}
errorObject = {
message: 'Network Error. Please check your connection and try again!',
+30
View File
@@ -0,0 +1,30 @@
import type { CookieSerializeOptions } from 'cookie'
import cookie from 'cookie'
import type { IncomingMessage } from 'http'
import type { NextApiRequest } from 'next'
export const getCookie = (
req: NextApiRequest | IncomingMessage,
name: string
) => {
const cookieData = cookie.parse(req.headers.cookie || '')
if (cookieData && cookieData[name]) {
return cookieData[name]
}
return null
}
export const serializeCookie = (
name: string,
value: string,
options: CookieSerializeOptions = {}
) => {
return cookie.serialize(name, String(value), {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
secure: process.env.NODE_ENV === 'production',
path: '/',
...options,
})
}
+1
View File
@@ -6,6 +6,7 @@ import { ENV } from "@/config/environment";
import ConfigContext from "@/contexts/ConfigContext";
import { useEffect, useRef, useState } from "react";
import { getMe } from "@/handlers/api/user.handler";
import { db } from "@/config/db";
interface AppPropsWithProps extends AppProps {
props: {
@@ -36,6 +36,7 @@ const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) =>
aaa."albumsId" IS NULL
AND a."ownerId" = '${ownerId}'
AND a."localDateTime"::date = '${date}'
AND a."isVisible" = true
`);
export default async function handler(
@@ -43,7 +44,7 @@ export default async function handler(
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser()
const currentUser = await getCurrentUser(req)
const { startDate } = req.query as { startDate: string };
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate, currentUser.id));
return res.status(200).json(rows);
@@ -17,6 +17,7 @@ LEFT JOIN
WHERE
aaa."albumsId" IS NULL
AND a."ownerId" = ${ownerId}
AND a."isVisible" = true
GROUP BY
DATE(a."localDateTime")
ORDER BY
@@ -28,7 +29,7 @@ export default async function handler(
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser()
const currentUser = await getCurrentUser(req)
const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(currentUser.id));
return res.status(200).json(rows);
} catch (error: any) {
+1 -1
View File
@@ -34,7 +34,7 @@ export default async function handler(
}
try {
const currentUser = await getCurrentUser();
const currentUser = await getCurrentUser(req);
const dataFromDB = await db.select({
value: count(),
label: column,
@@ -15,7 +15,7 @@ export default async function handler(
startDate: string;
};
const currentUser = await getCurrentUser();
const currentUser = await getCurrentUser(req);
if (!startDate) {
return res.status(400).json({
@@ -53,6 +53,7 @@ export default async function handler(
lte(exif.dateTimeOriginal, endDateDate),
eq(assets.type, "VIDEO"),
eq(assets.ownerId, currentUser.id),
eq(assets.isVisible, true),
));
return res.status(200).json(rows);
@@ -11,7 +11,7 @@ export default async function handler(
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser();
const currentUser = await getCurrentUser(req);
const rows = await db
.select({
asset_count: desc(count(assets.id)),
@@ -23,6 +23,7 @@ export default async function handler(
isNull(exif.latitude),
eq(assets.type, "VIDEO"),
eq(assets.ownerId, currentUser.id),
eq(assets.isVisible, true),
))
.groupBy(sql`DATE(${exif.dateTimeOriginal})`)
.orderBy(desc(count(assets.id)));
+4 -5
View File
@@ -1,6 +1,8 @@
// pages/api/proxy/[...path].ts
import { ENV } from '@/config/environment';
import { getCurrentUser } from '@/handlers/serverUtils/user.utils';
import { getUserHeaders } from '@/helpers/user.helper';
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -8,14 +10,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const targetUrl = `${ENV.IMMICH_URL}/api${proxyPath}`;
const currentUser = await getCurrentUser(req);
try {
const response = await fetch(targetUrl, {
method: req.method,
headers: {
'x-api-key': ENV.IMMICH_API_KEY,
'Content-Type': 'application/json',
},
headers: getUserHeaders(currentUser),
body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : null,
})
@@ -1,6 +1,8 @@
// pages/api/proxy.js
import { ENV } from '@/config/environment';
import { getCurrentUser } from '@/handlers/serverUtils/user.utils';
import { getUserHeaders } from '@/helpers/user.helper';
import { NextApiRequest, NextApiResponse } from 'next'
export const config = {
@@ -17,14 +19,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { id, size } = req.query;
const targetUrl = `${ENV.IMMICH_URL}/api/assets/${id}/thumbnail?size=${size || 'thumbnail'}`;
const user = await getCurrentUser(req);
if (!user) {
return res.status(403).json({ message: 'Unauthorized' })
}
try {
// Forward the request to the target API
const response = await fetch(targetUrl, {
method: 'GET',
headers: {
headers: getUserHeaders(user, {
'Content-Type': 'application/octet-stream',
'x-api-key': ENV.IMMICH_API_KEY,
},
}),
})
if (!response.ok) {
@@ -42,7 +48,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Send the image data
res.send(Buffer.from(imageBuffer))
} catch (error) {
console.error('Error:', error)
res.status(500).json({ message: 'Internal Server Error' })
}
}
@@ -1,6 +1,8 @@
// pages/api/proxy.js
import { ENV } from '@/config/environment';
import { getCurrentUser } from '@/handlers/serverUtils/user.utils';
import { getUserHeaders } from '@/helpers/user.helper';
import { NextApiRequest, NextApiResponse } from 'next'
export const config = {
@@ -17,14 +19,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { id } = req.query;
const targetUrl = `${ENV.IMMICH_URL}/api/assets/${id}/original`;
const user = await getCurrentUser(req);
if (!user) {
return res.status(403).json({ message: 'Unauthorized' })
}
try {
// Forward the request to the target API
const response = await fetch(targetUrl, {
method: 'GET',
headers: {
headers: getUserHeaders(user, {
'Content-Type': 'application/octet-stream',
'x-api-key': ENV.IMMICH_API_KEY,
},
}),
})
if (!response.ok) {
+10 -3
View File
@@ -1,6 +1,8 @@
// pages/api/proxy.js
import { ENV } from '@/config/environment';
import { getCurrentUser } from '@/handlers/serverUtils/user.utils';
import { getUserHeaders } from '@/helpers/user.helper';
import { NextApiRequest, NextApiResponse } from 'next'
export const config = {
@@ -17,14 +19,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { id } = req.query;
const targetUrl = `${ENV.IMMICH_URL}/api/people/${id}/thumbnail`;
const currentUser = await getCurrentUser(req);
if (!currentUser) {
return res.status(403).json({ message: 'Forbidden' })
}
try {
// Forward the request to the target API
const response = await fetch(targetUrl, {
method: 'GET',
headers: {
headers: getUserHeaders(currentUser, {
'Content-Type': 'application/octet-stream',
'x-api-key': ENV.IMMICH_API_KEY,
},
}),
})
if (!response.ok) {
+1 -7
View File
@@ -26,15 +26,9 @@ export default async function handler(
try {
const {
id,
page = 1,
perPage = 60,
nameLessOnly = false,
minimumAssetCount = 0,
sort = "assetCount",
sortOrder = "desc",
} = req.query as any as IQuery;
const currentUser = await getCurrentUser();
const currentUser = await getCurrentUser(req);
const personRecords = await db
.select()
.from(person)
+1 -3
View File
@@ -44,7 +44,7 @@ export default async function handler(
sortOrder = "desc",
} = req.query as any as IQuery;
const currentUser = await getCurrentUser();
const currentUser = await getCurrentUser(req);
const maximumAssetCount = !maxValue || maxValue <= 0 ? 1000000 : maxValue;
const whereClause = and(
@@ -86,8 +86,6 @@ export default async function handler(
}
const people = await query.limit(perPage).offset((page - 1) * perPage);
return res.status(200).json({
people,
total: 10,
+36
View File
@@ -0,0 +1,36 @@
// pages/api/proxy.js
import { appConfig } from "@/config/app.config";
import { getCurrentUser, loginUser } from "@/handlers/serverUtils/user.utils";
import { serializeCookie } from "@/lib/cookie";
import { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: true,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method Not Allowed" });
}
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: "Email and password are required" });
}
const user = await loginUser(email, password);
if (user) {
res.setHeader("Set-Cookie", serializeCookie(appConfig.sessionCookieName, user.accessToken));
return res.status(200).json(user);
}
else {
return res.status(403).json({ message: "Invalid User Credentials" });
}
}
+36
View File
@@ -0,0 +1,36 @@
// pages/api/proxy.js
import { appConfig } from "@/config/app.config";
import { getCurrentUser, logoutUser } from "@/handlers/serverUtils/user.utils";
import { serializeCookie } from "@/lib/cookie";
import { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: true,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method Not Allowed" });
}
try {
const user = await getCurrentUser(req);
const result = await logoutUser(user.accessToken as string);
if (result.successful) {
res.setHeader("Set-Cookie", serializeCookie(appConfig.sessionCookieName, ""));
return res.status(200).json({ message: "Logged out successfully" });
} else {
return res.status(403).json({ message: "Invalid User Credentials" });
}
} catch (error) {
console.error("Logout error:", error);
return res.status(500).json({ message: "Internal Server Error" });
}
}
+31
View File
@@ -0,0 +1,31 @@
// pages/api/proxy.js
import { ENV } from "@/config/environment";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method Not Allowed" });
}
try {
const user = await getCurrentUser(req);
if (user) return res.status(200).json(user);
return res.status(403).json({ message: "Forbidden" });
} catch (error) {
return res.status(500).json(error);
}
}
+2
View File
@@ -15,4 +15,6 @@ export interface IUser {
quotaUsageInBytes: number;
status: string;
license: null;
isUsingAPIKey?: boolean;
accessToken?: string;
}