feat: shared links custom URL (#19999)

* feat: custom url for shared links

* feat: use a separate route and query param

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Jed-Giblin
2025-07-28 14:16:55 -04:00
committed by GitHub
parent 16b14b390f
commit 9b3718120b
65 changed files with 947 additions and 432 deletions

View File

@@ -6,7 +6,7 @@ import { IncomingHttpHeaders } from 'node:http';
import { join } from 'node:path';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { UserAdmin } from 'src/database';
import { AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
import {
AuthDto,
AuthStatusResponseDto,
@@ -196,6 +196,7 @@ export class AuthService extends BaseService {
private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {
const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;
const shareSlug = (headers[ImmichHeader.SharedLinkSlug] || queryParams[ImmichQuery.SharedLinkSlug]) as string;
const session = (headers[ImmichHeader.UserToken] ||
headers[ImmichHeader.SessionToken] ||
queryParams[ImmichQuery.SessionKey] ||
@@ -204,7 +205,11 @@ export class AuthService extends BaseService {
const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;
if (shareKey) {
return this.validateSharedLink(shareKey);
return this.validateSharedLinkKey(shareKey);
}
if (shareSlug) {
return this.validateSharedLinkSlug(shareSlug);
}
if (session) {
@@ -403,18 +408,33 @@ export class AuthService extends BaseService {
return cookies[ImmichCookie.OAuthCodeVerifier] || null;
}
async validateSharedLink(key: string | string[]): Promise<AuthDto> {
async validateSharedLinkKey(key: string | string[]): Promise<AuthDto> {
key = Array.isArray(key) ? key[0] : key;
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
if (sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
return {
user: sharedLink.user,
sharedLink,
};
if (!this.isValidSharedLink(sharedLink)) {
throw new UnauthorizedException('Invalid share key');
}
throw new UnauthorizedException('Invalid share key');
return { user: sharedLink.user, sharedLink };
}
async validateSharedLinkSlug(slug: string | string[]): Promise<AuthDto> {
slug = Array.isArray(slug) ? slug[0] : slug;
const sharedLink = await this.sharedLinkRepository.getBySlug(slug);
if (!this.isValidSharedLink(sharedLink)) {
throw new UnauthorizedException('Invalid share slug');
}
return { user: sharedLink.user, sharedLink };
}
private isValidSharedLink(
sharedLink?: AuthSharedLink & { user: AuthUser | null },
): sharedLink is AuthSharedLink & { user: AuthUser } {
return !!sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date());
}
private async validateApiKey(key: string): Promise<AuthDto> {