diff --git a/bun.lockb b/bun.lockb index 422f823..9d2a89a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e3d4190..3c3d6e7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..8bec9d1 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -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(null); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.id]: e.target.value }) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setErrorMessage(null) + loginUser(formData.email, formData.password) + .then(onLogin) + .catch((error) => { + setErrorMessage(error.message) + }).finally(() => { + setLoading(false) + }) + } + + return ( + <> + + Immich Power Tools - Login + +
+ + + Immich Power Tools + Login to Immich + + Login to your connected Immich instance ({exImmichUrl}) + + + +
+ {errorMessage && ( +
+

{errorMessage}

+
+ )} +
+ + +
+
+ + +
+ + +

+ Or Use IMMICH_API_KEY in your environment to use API Key instead of password. +

+
+
+
+
+ + ) +} diff --git a/src/components/layouts/RootLayout.tsx b/src/components/layouts/RootLayout.tsx index bf6e093..91ff2b5 100644 --- a/src/components/layouts/RootLayout.tsx +++ b/src/components/layouts/RootLayout.tsx @@ -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(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 ; if (errorMessage) return ( @@ -43,24 +52,44 @@ export default function RootLayout({ children }: RootLayoutProps) { title={errorMessage.error} description={errorMessage.message} action={ - - Setup Instructions - - +
+ + Setup Instructions + + +
+
+

Connected to (External)

+ + {exImmichUrl || "Not Set"} + +
+
+

Connected to (Internal)

+ + {immichURL} + +
+
+
} /> ); - if (!user) return null; + + if (!user) return ; return ( - +
{children}
diff --git a/src/components/shared/ErrorBlock.tsx b/src/components/shared/ErrorBlock.tsx index cf6605c..6cdf5a9 100644 --- a/src/components/shared/ErrorBlock.tsx +++ b/src/components/shared/ErrorBlock.tsx @@ -1,3 +1,5 @@ +import { useConfig } from '@/contexts/ConfigContext'; +import { Link } from 'lucide-react'; import React from 'react' interface ErrorBlockProps { diff --git a/src/components/shared/ProfileInfo.tsx b/src/components/shared/ProfileInfo.tsx index 587d012..9349f03 100644 --- a/src/components/shared/ProfileInfo.tsx +++ b/src/components/shared/ProfileInfo.tsx @@ -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(null); - const [loading, setLoading] = React.useState(true); - const [errorMessage, setErrorMessage] = useState(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
Loading...
; - if (errorMessage) return
{errorMessage}
; return ( <> @@ -38,7 +33,14 @@ export default function ProfileInfo() { {immichURL} -

{user?.name}

+ {user && ( + <> +

{user?.name}

+ {!user.isUsingAPIKey && } + + )}

diff --git a/src/components/shared/Sidebar.tsx b/src/components/shared/Sidebar.tsx index 458f8c1..e3ef727 100644 --- a/src/components/shared/Sidebar.tsx +++ b/src/components/shared/Sidebar.tsx @@ -53,7 +53,6 @@ export default function Sidebar() {

-
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 3c9063a..c0ac4db 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -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 ( - + {!!(title || description) && ( {title && {title}} diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..749a616 --- /dev/null +++ b/src/config/app.config.ts @@ -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 +} \ No newline at end of file diff --git a/src/config/environment.ts b/src/config/environment.ts index f0972b5..3062eda 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -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, }; diff --git a/src/config/routes.ts b/src/config/routes.ts index 2d59e96..4aa8025 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -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"; diff --git a/src/contexts/CurrentUserContext.tsx b/src/contexts/CurrentUserContext.tsx index 8e74e30..a81bf4e 100644 --- a/src/contexts/CurrentUserContext.tsx +++ b/src/contexts/CurrentUserContext.tsx @@ -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(null); diff --git a/src/handlers/api/user.handler.ts b/src/handlers/api/user.handler.ts index 534de92..5aa974c 100644 --- a/src/handlers/api/user.handler.ts +++ b/src/handlers/api/user.handler.ts @@ -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); } \ No newline at end of file diff --git a/src/handlers/serverUtils/user.utils.ts b/src/handlers/serverUtils/user.utils.ts index fd46860..f71ca3d 100644 --- a/src/handlers/serverUtils/user.utils.ts +++ b/src/handlers/serverUtils/user.utils.ts @@ -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 } \ No newline at end of file diff --git a/src/helpers/user.helper.ts b/src/helpers/user.helper.ts new file mode 100644 index 0000000..fe805fd --- /dev/null +++ b/src/helpers/user.helper.ts @@ -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} +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index bb00bf0..0483d40 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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!', diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts new file mode 100644 index 0000000..2174af5 --- /dev/null +++ b/src/lib/cookie.ts @@ -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, + }) +} \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f90289b..d3eaa84 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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: { diff --git a/src/pages/api/albums/potential-albums-assets.ts b/src/pages/api/albums/potential-albums-assets.ts index e17c7ba..0cd9bfe 100644 --- a/src/pages/api/albums/potential-albums-assets.ts +++ b/src/pages/api/albums/potential-albums-assets.ts @@ -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); diff --git a/src/pages/api/albums/potential-albums-dates.ts b/src/pages/api/albums/potential-albums-dates.ts index 0108b4c..cb2c751 100644 --- a/src/pages/api/albums/potential-albums-dates.ts +++ b/src/pages/api/albums/potential-albums-dates.ts @@ -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) { diff --git a/src/pages/api/analytics/exif/[property].ts b/src/pages/api/analytics/exif/[property].ts index cb30ba3..13e99ed 100644 --- a/src/pages/api/analytics/exif/[property].ts +++ b/src/pages/api/analytics/exif/[property].ts @@ -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, diff --git a/src/pages/api/assets/missing-location-assets.ts b/src/pages/api/assets/missing-location-assets.ts index bdb7cba..2d6d16a 100644 --- a/src/pages/api/assets/missing-location-assets.ts +++ b/src/pages/api/assets/missing-location-assets.ts @@ -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); diff --git a/src/pages/api/assets/missing-location-dates.ts b/src/pages/api/assets/missing-location-dates.ts index 1680d7f..86af049 100644 --- a/src/pages/api/assets/missing-location-dates.ts +++ b/src/pages/api/assets/missing-location-dates.ts @@ -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))); diff --git a/src/pages/api/immich-proxy/[...path].ts b/src/pages/api/immich-proxy/[...path].ts index 791e789..c7641f7 100644 --- a/src/pages/api/immich-proxy/[...path].ts +++ b/src/pages/api/immich-proxy/[...path].ts @@ -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, }) diff --git a/src/pages/api/immich-proxy/asset/thumbnail/[id].ts b/src/pages/api/immich-proxy/asset/thumbnail/[id].ts index 5a73569..aa29165 100644 --- a/src/pages/api/immich-proxy/asset/thumbnail/[id].ts +++ b/src/pages/api/immich-proxy/asset/thumbnail/[id].ts @@ -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' }) } } \ No newline at end of file diff --git a/src/pages/api/immich-proxy/asset/video/[id].ts b/src/pages/api/immich-proxy/asset/video/[id].ts index 66c475d..bdc5031 100644 --- a/src/pages/api/immich-proxy/asset/video/[id].ts +++ b/src/pages/api/immich-proxy/asset/video/[id].ts @@ -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) { diff --git a/src/pages/api/immich-proxy/thumbnail/[id].ts b/src/pages/api/immich-proxy/thumbnail/[id].ts index e4042fe..43f45d9 100644 --- a/src/pages/api/immich-proxy/thumbnail/[id].ts +++ b/src/pages/api/immich-proxy/thumbnail/[id].ts @@ -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) { diff --git a/src/pages/api/people/[id]/similar-faces.ts b/src/pages/api/people/[id]/similar-faces.ts index 0a0729f..e1466b7 100644 --- a/src/pages/api/people/[id]/similar-faces.ts +++ b/src/pages/api/people/[id]/similar-faces.ts @@ -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) diff --git a/src/pages/api/people/list.ts b/src/pages/api/people/list.ts index 340a7ef..054ba13 100644 --- a/src/pages/api/people/list.ts +++ b/src/pages/api/people/list.ts @@ -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, diff --git a/src/pages/api/users/login.ts b/src/pages/api/users/login.ts new file mode 100644 index 0000000..9410e7d --- /dev/null +++ b/src/pages/api/users/login.ts @@ -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" }); + } + +} diff --git a/src/pages/api/users/logout.ts b/src/pages/api/users/logout.ts new file mode 100644 index 0000000..ea04370 --- /dev/null +++ b/src/pages/api/users/logout.ts @@ -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" }); + } +} diff --git a/src/pages/api/users/me.ts b/src/pages/api/users/me.ts new file mode 100644 index 0000000..3399615 --- /dev/null +++ b/src/pages/api/users/me.ts @@ -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); + } +} diff --git a/src/types/user.d.ts b/src/types/user.d.ts index 45daa33..55a6ed3 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -15,4 +15,6 @@ export interface IUser { quotaUsageInBytes: number; status: string; license: null; + isUsingAPIKey?: boolean; + accessToken?: string; }