1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-24 11:55:06 +00:00

feat: add various improvements to the table component (#961)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
Elias Schneider
2025-10-13 11:12:55 +02:00
committed by GitHub
parent 24ca6a106d
commit c20e93b55c
76 changed files with 1948 additions and 1434 deletions

View File

@@ -1,21 +1,19 @@
import type { ApiKey, ApiKeyCreate, ApiKeyResponse } from '$lib/types/api-key.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import APIService from './api-service';
export default class ApiKeyService extends APIService {
async list(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/api-keys', {
params: options
});
list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/api-keys', { params: options });
return res.data as Paginated<ApiKey>;
}
};
async create(data: ApiKeyCreate): Promise<ApiKeyResponse> {
create = async (data: ApiKeyCreate): Promise<ApiKeyResponse> => {
const res = await this.api.post('/api-keys', data);
return res.data as ApiKeyResponse;
}
};
async revoke(id: string): Promise<void> {
revoke = async (id: string): Promise<void> => {
await this.api.delete(`/api-keys/${id}`);
}
};
}

View File

@@ -1,15 +1,13 @@
import axios from 'axios';
abstract class APIService {
api = axios.create({
baseURL: '/api'
});
protected api = axios.create({ baseURL: '/api' });
constructor() {
if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
}
}
constructor() {
if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
}
}
}
export default APIService;

View File

@@ -3,39 +3,33 @@ import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-
import APIService from './api-service';
export default class AppConfigService extends APIService {
async list(showAll = false) {
list = async (showAll = false) => {
let url = '/application-configuration';
if (showAll) {
url += '/all';
}
if (showAll) url += '/all';
const { data } = await this.api.get<AppConfigRawResponse>(url);
return this.parseConfigList(data);
}
return parseConfigList(data);
};
async update(appConfig: AllAppConfig) {
update = async (appConfig: AllAppConfig) => {
// Convert all values to string, stringifying JSON where needed
const appConfigConvertedToString: Record<string, string> = {};
for (const key in appConfig) {
const value = (appConfig as any)[key];
if (typeof value === 'object' && value !== null) {
appConfigConvertedToString[key] = JSON.stringify(value);
} else {
appConfigConvertedToString[key] = String(value);
}
appConfigConvertedToString[key] =
typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
}
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
return this.parseConfigList(res.data);
}
return parseConfigList(res.data);
};
async updateFavicon(favicon: File) {
updateFavicon = async (favicon: File) => {
const formData = new FormData();
formData.append('file', favicon!);
await this.api.put(`/application-images/favicon`, formData);
}
async updateLogo(logo: File, light = true) {
updateLogo = async (logo: File, light = true) => {
const formData = new FormData();
formData.append('file', logo!);
@@ -43,52 +37,52 @@ export default class AppConfigService extends APIService {
params: { light }
});
cachedApplicationLogo.bustCache(light);
}
};
async updateBackgroundImage(backgroundImage: File) {
updateBackgroundImage = async (backgroundImage: File) => {
const formData = new FormData();
formData.append('file', backgroundImage!);
await this.api.put(`/application-images/background`, formData);
cachedBackgroundImage.bustCache();
}
};
async sendTestEmail() {
sendTestEmail = async () => {
await this.api.post('/application-configuration/test-email');
}
};
async syncLdap() {
syncLdap = async () => {
await this.api.post('/application-configuration/sync-ldap');
}
};
}
private parseConfigList(data: AppConfigRawResponse) {
const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => {
(appConfig as any)[key] = this.parseValue(value);
});
function parseConfigList(data: AppConfigRawResponse) {
const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => {
(appConfig as any)[key] = parseValue(value);
});
return appConfig as AllAppConfig;
}
return appConfig as AllAppConfig;
}
private parseValue(value: string) {
// Try to parse JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return parsed;
}
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
return parseFloat(value);
} else {
return value;
function parseValue(value: string) {
// Try to parse JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return parsed;
}
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
return parseFloat(value);
} else {
return value;
}
}

View File

@@ -1,34 +1,25 @@
import type { AuditLog, AuditLogFilter } from '$lib/types/audit-log.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { AuditLog } from '$lib/types/audit-log.type';
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import APIService from './api-service';
class AuditLogService extends APIService {
async list(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/audit-logs', {
params: options
});
export default class AuditLogService extends APIService {
list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/audit-logs', { params: options });
return res.data as Paginated<AuditLog>;
}
};
async listAllLogs(options?: SearchPaginationSortRequest, filters?: AuditLogFilter) {
const res = await this.api.get('/audit-logs/all', {
params: {
...options,
filters
}
});
listAllLogs = async (options?: ListRequestOptions) => {
const res = await this.api.get('/audit-logs/all', { params: options });
return res.data as Paginated<AuditLog>;
}
};
async listClientNames() {
listClientNames = async () => {
const res = await this.api.get<string[]>('/audit-logs/filters/client-names');
return res.data;
}
};
async listUsers() {
listUsers = async () => {
const res = await this.api.get<Record<string, string>>('/audit-logs/filters/users');
return res.data;
}
};
}
export default AuditLogService;

View File

@@ -2,18 +2,18 @@ import type { CustomClaim } from '$lib/types/custom-claim.type';
import APIService from './api-service';
export default class CustomClaimService extends APIService {
async getSuggestions() {
getSuggestions = async () => {
const res = await this.api.get('/custom-claims/suggestions');
return res.data as string[];
}
};
async updateUserCustomClaims(userId: string, claims: CustomClaim[]) {
updateUserCustomClaims = async (userId: string, claims: CustomClaim[]) => {
const res = await this.api.put(`/custom-claims/user/${userId}`, claims);
return res.data as CustomClaim[];
}
};
async updateUserGroupCustomClaims(userGroupId: string, claims: CustomClaim[]) {
updateUserGroupCustomClaims = async (userGroupId: string, claims: CustomClaim[]) => {
const res = await this.api.put(`/custom-claims/user-group/${userGroupId}`, claims);
return res.data as CustomClaim[];
}
};
}

View File

@@ -1,3 +1,4 @@
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type {
AccessibleOidcClient,
AuthorizeResponse,
@@ -9,12 +10,11 @@ import type {
OidcClientWithAllowedUserGroupsCount,
OidcDeviceCodeInfo
} from '$lib/types/oidc.type';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import APIService from './api-service';
class OidcService extends APIService {
async authorize(
authorize = async (
clientId: string,
scope: string,
callbackURL: string,
@@ -22,7 +22,7 @@ class OidcService extends APIService {
codeChallenge?: string,
codeChallengeMethod?: string,
reauthenticationToken?: string
) {
) => {
const res = await this.api.post('/oidc/authorize', {
scope,
nonce,
@@ -34,45 +34,41 @@ class OidcService extends APIService {
});
return res.data as AuthorizeResponse;
}
};
async isAuthorizationRequired(clientId: string, scope: string) {
isAuthorizationRequired = async (clientId: string, scope: string) => {
const res = await this.api.post('/oidc/authorization-required', {
scope,
clientId
});
return res.data.authorizationRequired as boolean;
}
};
async listClients(options?: SearchPaginationSortRequest) {
listClients = async (options?: ListRequestOptions) => {
const res = await this.api.get('/oidc/clients', {
params: options
});
return res.data as Paginated<OidcClientWithAllowedUserGroupsCount>;
}
};
async createClient(client: OidcClientCreate) {
return (await this.api.post('/oidc/clients', client)).data as OidcClient;
}
createClient = async (client: OidcClientCreate) =>
(await this.api.post('/oidc/clients', client)).data as OidcClient;
async removeClient(id: string) {
removeClient = async (id: string) => {
await this.api.delete(`/oidc/clients/${id}`);
}
};
async getClient(id: string) {
return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
}
getClient = async (id: string) =>
(await this.api.get(`/oidc/clients/${id}`)).data as OidcClientWithAllowedUserGroups;
async getClientMetaData(id: string) {
return (await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
}
getClientMetaData = async (id: string) =>
(await this.api.get(`/oidc/clients/${id}/meta`)).data as OidcClientMetaData;
async updateClient(id: string, client: OidcClientUpdate) {
return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
}
updateClient = async (id: string, client: OidcClientUpdate) =>
(await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
async updateClientLogo(client: OidcClient, image: File | null) {
updateClientLogo = async (client: OidcClient, image: File | null) => {
if (client.hasLogo && !image) {
await this.removeClientLogo(client.id);
return;
@@ -86,49 +82,45 @@ class OidcService extends APIService {
await this.api.post(`/oidc/clients/${client.id}/logo`, formData);
cachedOidcClientLogo.bustCache(client.id);
}
};
async removeClientLogo(id: string) {
removeClientLogo = async (id: string) => {
await this.api.delete(`/oidc/clients/${id}/logo`);
cachedOidcClientLogo.bustCache(id);
}
};
async createClientSecret(id: string) {
return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
}
createClientSecret = async (id: string) =>
(await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string;
async updateAllowedUserGroups(id: string, userGroupIds: string[]) {
updateAllowedUserGroups = async (id: string, userGroupIds: string[]) => {
const res = await this.api.put(`/oidc/clients/${id}/allowed-user-groups`, { userGroupIds });
return res.data as OidcClientWithAllowedUserGroups;
}
};
async verifyDeviceCode(userCode: string) {
verifyDeviceCode = async (userCode: string) => {
return await this.api.post(`/oidc/device/verify?code=${userCode}`);
}
};
async getDeviceCodeInfo(userCode: string): Promise<OidcDeviceCodeInfo> {
getDeviceCodeInfo = async (userCode: string): Promise<OidcDeviceCodeInfo> => {
const response = await this.api.get(`/oidc/device/info?code=${userCode}`);
return response.data;
}
};
async getClientPreview(id: string, userId: string, scopes: string) {
getClientPreview = async (id: string, userId: string, scopes: string) => {
const response = await this.api.get(`/oidc/clients/${id}/preview/${userId}`, {
params: { scopes }
});
return response.data;
}
async listOwnAccessibleClients(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/oidc/users/me/clients', {
params: options
});
};
listOwnAccessibleClients = async (options?: ListRequestOptions) => {
const res = await this.api.get('/oidc/users/me/clients', { params: options });
return res.data as Paginated<AccessibleOidcClient>;
}
};
async revokeOwnAuthorizedClient(clientId: string) {
revokeOwnAuthorizedClient = async (clientId: string) => {
await this.api.delete(`/oidc/users/me/authorized-clients/${clientId}`);
}
};
}
export default OidcService;

View File

@@ -1,4 +1,4 @@
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type {
UserGroupCreate,
UserGroupWithUserCount,
@@ -7,34 +7,32 @@ import type {
import APIService from './api-service';
export default class UserGroupService extends APIService {
async list(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/user-groups', {
params: options
});
list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/user-groups', { params: options });
return res.data as Paginated<UserGroupWithUserCount>;
}
};
async get(id: string) {
get = async (id: string) => {
const res = await this.api.get(`/user-groups/${id}`);
return res.data as UserGroupWithUsers;
}
};
async create(user: UserGroupCreate) {
create = async (user: UserGroupCreate) => {
const res = await this.api.post('/user-groups', user);
return res.data as UserGroupWithUsers;
}
};
async update(id: string, user: UserGroupCreate) {
update = async (id: string, user: UserGroupCreate) => {
const res = await this.api.put(`/user-groups/${id}`, user);
return res.data as UserGroupWithUsers;
}
};
async remove(id: string) {
remove = async (id: string) => {
await this.api.delete(`/user-groups/${id}`);
}
};
async updateUsers(id: string, userIds: string[]) {
updateUsers = async (id: string, userIds: string[]) => {
const res = await this.api.put(`/user-groups/${id}/users`, { userIds });
return res.data as UserGroupWithUsers;
}
};
}

View File

@@ -1,5 +1,5 @@
import userStore from '$lib/stores/user-store';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type { SignupTokenDto } from '$lib/types/signup-token.type';
import type { UserGroup } from '$lib/types/user-group.type';
import type { User, UserCreate, UserSignUp } from '$lib/types/user.type';
@@ -8,125 +8,113 @@ import { get } from 'svelte/store';
import APIService from './api-service';
export default class UserService extends APIService {
async list(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/users', {
params: options
});
list = async (options?: ListRequestOptions) => {
const res = await this.api.get('/users', { params: options });
return res.data as Paginated<User>;
}
};
async get(id: string) {
get = async (id: string) => {
const res = await this.api.get(`/users/${id}`);
return res.data as User;
}
};
async getCurrent() {
getCurrent = async () => {
const res = await this.api.get('/users/me');
return res.data as User;
}
};
async create(user: UserCreate) {
create = async (user: UserCreate) => {
const res = await this.api.post('/users', user);
return res.data as User;
}
};
async getUserGroups(userId: string) {
getUserGroups = async (userId: string) => {
const res = await this.api.get(`/users/${userId}/groups`);
return res.data as UserGroup[];
}
};
async update(id: string, user: UserCreate) {
update = async (id: string, user: UserCreate) => {
const res = await this.api.put(`/users/${id}`, user);
return res.data as User;
}
};
async updateCurrent(user: UserCreate) {
updateCurrent = async (user: UserCreate) => {
const res = await this.api.put('/users/me', user);
return res.data as User;
}
};
async remove(id: string) {
remove = async (id: string) => {
await this.api.delete(`/users/${id}`);
}
};
async updateProfilePicture(userId: string, image: File) {
updateProfilePicture = async (userId: string, image: File) => {
const formData = new FormData();
formData.append('file', image!);
await this.api.put(`/users/${userId}/profile-picture`, formData);
cachedProfilePicture.bustCache(userId);
}
};
async updateCurrentUsersProfilePicture(image: File) {
updateCurrentUsersProfilePicture = async (image: File) => {
const formData = new FormData();
formData.append('file', image!);
await this.api.put('/users/me/profile-picture', formData);
cachedProfilePicture.bustCache(get(userStore)!.id);
}
};
async resetCurrentUserProfilePicture() {
resetCurrentUserProfilePicture = async () => {
await this.api.delete(`/users/me/profile-picture`);
cachedProfilePicture.bustCache(get(userStore)!.id);
}
};
async resetProfilePicture(userId: string) {
resetProfilePicture = async (userId: string) => {
await this.api.delete(`/users/${userId}/profile-picture`);
cachedProfilePicture.bustCache(userId);
}
};
async createOneTimeAccessToken(userId: string = 'me', ttl?: string|number) {
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
userId,
ttl,
});
createOneTimeAccessToken = async (userId: string = 'me', ttl?: string | number) => {
const res = await this.api.post(`/users/${userId}/one-time-access-token`, { userId, ttl });
return res.data.token;
}
};
async createSignupToken(ttl: string|number, usageLimit: number) {
const res = await this.api.post(`/signup-tokens`, {
ttl,
usageLimit
});
createSignupToken = async (ttl: string | number, usageLimit: number) => {
const res = await this.api.post(`/signup-tokens`, { ttl, usageLimit });
return res.data.token;
}
};
async exchangeOneTimeAccessToken(token: string) {
exchangeOneTimeAccessToken = async (token: string) => {
const res = await this.api.post(`/one-time-access-token/${token}`);
return res.data as User;
}
};
async requestOneTimeAccessEmailAsUnauthenticatedUser(email: string, redirectPath?: string) {
requestOneTimeAccessEmailAsUnauthenticatedUser = async (email: string, redirectPath?: string) => {
await this.api.post('/one-time-access-email', { email, redirectPath });
}
};
async requestOneTimeAccessEmailAsAdmin(userId: string, ttl: string|number) {
requestOneTimeAccessEmailAsAdmin = async (userId: string, ttl: string | number) => {
await this.api.post(`/users/${userId}/one-time-access-email`, { ttl });
}
};
async updateUserGroups(id: string, userGroupIds: string[]) {
updateUserGroups = async (id: string, userGroupIds: string[]) => {
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
return res.data as User;
}
};
async signup(data: UserSignUp) {
signup = async (data: UserSignUp) => {
const res = await this.api.post(`/signup`, data);
return res.data as User;
}
};
async signupInitialUser(data: UserSignUp) {
signupInitialUser = async (data: UserSignUp) => {
const res = await this.api.post(`/signup/setup`, data);
return res.data as User;
}
};
async listSignupTokens(options?: SearchPaginationSortRequest) {
const res = await this.api.get('/signup-tokens', {
params: options
});
listSignupTokens = async (options?: ListRequestOptions) => {
const res = await this.api.get('/signup-tokens', { params: options });
return res.data as Paginated<SignupTokenDto>;
}
};
async deleteSignupToken(tokenId: string) {
deleteSignupToken = async (tokenId: string) => {
await this.api.delete(`/signup-tokens/${tokenId}`);
}
};
}

View File

@@ -1,21 +1,13 @@
import { version as currentVersion } from '$app/environment';
import axios from 'axios';
import APIService from './api-service';
async function getNewestVersion() {
const response = await axios
.get('/api/version/latest', {
timeout: 2000
})
.then((res) => res.data);
export default class VersionService extends APIService {
getNewestVersion = async () => {
const response = await this.api
.get('/version/latest', { timeout: 2000 })
.then((res) => res.data);
return response.latestVersion;
};
return response.latestVersion;
getCurrentVersion = () => currentVersion;
}
function getCurrentVersion() {
return currentVersion;
}
export default {
getNewestVersion,
getCurrentVersion,
};

View File

@@ -3,45 +3,36 @@ import type { User } from '$lib/types/user.type';
import APIService from './api-service';
import userStore from '$lib/stores/user-store';
import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/browser';
class WebAuthnService extends APIService {
async getRegistrationOptions() {
return (await this.api.get(`/webauthn/register/start`)).data;
}
getRegistrationOptions = async () => (await this.api.get(`/webauthn/register/start`)).data;
async finishRegistration(body: RegistrationResponseJSON) {
return (await this.api.post(`/webauthn/register/finish`, body)).data as Passkey;
}
finishRegistration = async (body: RegistrationResponseJSON) =>
(await this.api.post(`/webauthn/register/finish`, body)).data as Passkey;
async getLoginOptions() {
return (await this.api.get(`/webauthn/login/start`)).data;
}
getLoginOptions = async () => (await this.api.get(`/webauthn/login/start`)).data;
async finishLogin(body: AuthenticationResponseJSON) {
return (await this.api.post(`/webauthn/login/finish`, body)).data as User;
}
finishLogin = async (body: AuthenticationResponseJSON) =>
(await this.api.post(`/webauthn/login/finish`, body)).data as User;
async logout() {
await this.api.post(`/webauthn/logout`);
userStore.clearUser();
}
logout = async () => {
await this.api.post(`/webauthn/logout`);
userStore.clearUser();
};
async listCredentials() {
return (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
}
listCredentials = async () => (await this.api.get(`/webauthn/credentials`)).data as Passkey[];
async removeCredential(id: string) {
await this.api.delete(`/webauthn/credentials/${id}`);
}
removeCredential = async (id: string) => {
await this.api.delete(`/webauthn/credentials/${id}`);
};
async updateCredentialName(id: string, name: string) {
await this.api.patch(`/webauthn/credentials/${id}`, { name });
}
updateCredentialName = async (id: string, name: string) => {
await this.api.patch(`/webauthn/credentials/${id}`, { name });
};
async reauthenticate(body?: AuthenticationResponseJSON) {
const res = await this.api.post('/webauthn/reauthenticate', body);
return res.data.reauthenticationToken as string;
}
reauthenticate = async (body?: AuthenticationResponseJSON) => {
const res = await this.api.post('/webauthn/reauthenticate', body);
return res.data.reauthenticationToken as string;
};
}
export default WebAuthnService;