Merge pull request #36 from varun-raj/feat-auth
Feat: Authentication based on immich's existing credentials
This commit is contained in:
33 files changed
+487
-87
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
import { Link } from 'lucide-react';
|
||||
import React from 'react'
|
||||
|
||||
interface ErrorBlockProps {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,7 +53,6 @@ export default function Sidebar() {
|
||||
</div>
|
||||
<div>
|
||||
<ProfileInfo />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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!',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -15,4 +15,6 @@ export interface IUser {
|
||||
quotaUsageInBytes: number;
|
||||
status: string;
|
||||
license: null;
|
||||
isUsingAPIKey?: boolean;
|
||||
accessToken?: string;
|
||||
}
|
||||
Reference in new issue
Block a user