Compare commits

...

5 Commits

Author SHA1 Message Date
Yaros
b2f10d6997 fix(mobile): allow view in timeline from deeplink 2026-01-19 17:24:15 +01:00
Jason Rasmussen
b123beae38 fix(server): api key update checks (#25363) 2026-01-19 10:20:06 -05:00
Mees Frensel
1ada7a8340 chore(deps): ignore @parcel/watcher build script (#25361) 2026-01-19 09:08:25 -05:00
Matthew Momjian
5d81cace23 chore(docs): update RAM req (#25344)
* RAM req

* Update requirements.md
2026-01-18 17:52:08 -06:00
yy
65f9a228ba fix: typos in comments and error messages (#25320) 2026-01-17 18:58:26 -06:00
7 changed files with 90 additions and 5 deletions

View File

@@ -17,12 +17,17 @@ Hardware and software requirements for Immich:
- Immich runs well in a virtualized environment when running in a full virtual machine.
The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users.
If you have issues, we recommend that you switch to a supported VM deployment.
- **RAM**: Minimum 4GB, recommended 6GB.
- **RAM**: Minimum 6GB, recommended 8GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
:::tip
:::note RAM requirements
For a smooth experience, especially during asset upload, Immich requires at least 6GB of RAM.
For systems with only 4GB of RAM, Immich can be run with machine learning features disabled.
:::
:::tip Postgres setup
Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience.
The Postgres database files are typically between 1-3 GB in size.
For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind.

View File

@@ -149,7 +149,6 @@ enum ActionButtonType {
ActionButtonType.openInfo => true,
ActionButtonType.viewInTimeline =>
context.timelineOrigin != TimelineOrigin.main &&
context.timelineOrigin != TimelineOrigin.deepLink &&
context.timelineOrigin != TimelineOrigin.trash &&
context.timelineOrigin != TimelineOrigin.lockedFolder &&
context.timelineOrigin != TimelineOrigin.archive &&

View File

@@ -33,7 +33,7 @@ class PersonNameEditForm extends HookConsumerWidget {
decoration: InputDecoration(
hintText: 'name'.tr(),
border: const OutlineInputBorder(),
errorText: isError.value ? 'Error occured' : null,
errorText: isError.value ? 'Error occurred' : null,
),
),
),

View File

@@ -11,6 +11,7 @@ packages:
- .github
ignoredBuiltDependencies:
- '@nestjs/core'
- '@parcel/watcher'
- '@scarf/scarf'
- '@swc/core'
- canvas

View File

@@ -107,6 +107,78 @@ describe(ApiKeyService.name, () => {
permissions: newPermissions,
});
});
describe('api key auth', () => {
it('should prevent adding Permission.all', async () => {
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
const auth = factory.auth({ apiKey: { permissions } });
const apiKey = factory.apiKey({ userId: auth.user.id, permissions });
mocks.apiKey.getById.mockResolvedValue(apiKey);
await expect(sut.update(auth, apiKey.id, { permissions: [Permission.All] })).rejects.toThrow(
'Cannot grant permissions you do not have',
);
expect(mocks.apiKey.update).not.toHaveBeenCalled();
});
it('should prevent adding a new permission', async () => {
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
const auth = factory.auth({ apiKey: { permissions } });
const apiKey = factory.apiKey({ userId: auth.user.id, permissions });
mocks.apiKey.getById.mockResolvedValue(apiKey);
await expect(sut.update(auth, apiKey.id, { permissions: [Permission.AssetCopy] })).rejects.toThrow(
'Cannot grant permissions you do not have',
);
expect(mocks.apiKey.update).not.toHaveBeenCalled();
});
it('should allow removing permissions', async () => {
const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } });
const apiKey = factory.apiKey({
userId: auth.user.id,
permissions: [Permission.AssetRead, Permission.AssetDelete],
});
mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.update.mockResolvedValue(apiKey);
// remove Permission.AssetDelete
await sut.update(auth, apiKey.id, { permissions: [Permission.AssetRead] });
expect(mocks.apiKey.update).toHaveBeenCalledWith(
auth.user.id,
apiKey.id,
expect.objectContaining({ permissions: [Permission.AssetRead] }),
);
});
it('should allow adding new permissions', async () => {
const auth = factory.auth({
apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] },
});
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] });
mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.update.mockResolvedValue(apiKey);
// add Permission.AssetUpdate
await sut.update(auth, apiKey.id, {
name: apiKey.name,
permissions: [Permission.AssetRead, Permission.AssetUpdate],
});
expect(mocks.apiKey.update).toHaveBeenCalledWith(
auth.user.id,
apiKey.id,
expect.objectContaining({ permissions: [Permission.AssetRead, Permission.AssetUpdate] }),
);
});
});
});
describe('delete', () => {

View File

@@ -32,6 +32,14 @@ export class ApiKeyService extends BaseService {
throw new BadRequestException('API Key not found');
}
if (
auth.apiKey &&
dto.permissions &&
!isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })
) {
throw new BadRequestException('Cannot grant permissions you do not have');
}
const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions });
return this.map(key);

View File

@@ -117,7 +117,7 @@ export class SmartInfoService extends BaseService {
const newConfig = await this.getConfig({ withCache: true });
if (machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) {
// Skip the job if the the model has changed since the embedding was generated.
// Skip the job if the model has changed since the embedding was generated.
return JobStatus.Skipped;
}