Add ability to upload and manage service images
Integrates Uppy.js for file uploads, adds new API endpoints for requesting upload URLs, and updates UI components to support image uploads. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 804c1330-3360-45df-814d-221ee0d46866 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/JyUisd3 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { File } from "@google-cloud/storage";
|
||||
|
||||
const ACL_POLICY_METADATA_KEY = "custom:aclPolicy";
|
||||
|
||||
// Can be flexibly defined according to the use case.
|
||||
//
|
||||
// Examples:
|
||||
// - USER_LIST: the users from a list stored in the database;
|
||||
// - EMAIL_DOMAIN: the users whose email is in a specific domain;
|
||||
// - GROUP_MEMBER: the users who are members of a specific group;
|
||||
// - SUBSCRIBER: the users who are subscribers of a specific service / content
|
||||
// creator.
|
||||
export enum ObjectAccessGroupType {}
|
||||
|
||||
export interface ObjectAccessGroup {
|
||||
type: ObjectAccessGroupType;
|
||||
// The logic id that identifies qualified group members. Format depends on the
|
||||
// ObjectAccessGroupType — e.g. a user-list DB id, an email domain, a group id.
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum ObjectPermission {
|
||||
READ = "read",
|
||||
WRITE = "write",
|
||||
}
|
||||
|
||||
export interface ObjectAclRule {
|
||||
group: ObjectAccessGroup;
|
||||
permission: ObjectPermission;
|
||||
}
|
||||
|
||||
// Stored as object custom metadata under "custom:aclPolicy" (JSON string).
|
||||
export interface ObjectAclPolicy {
|
||||
owner: string;
|
||||
visibility: "public" | "private";
|
||||
aclRules?: Array<ObjectAclRule>;
|
||||
}
|
||||
|
||||
function isPermissionAllowed(
|
||||
requested: ObjectPermission,
|
||||
granted: ObjectPermission,
|
||||
): boolean {
|
||||
if (requested === ObjectPermission.READ) {
|
||||
return [ObjectPermission.READ, ObjectPermission.WRITE].includes(granted);
|
||||
}
|
||||
return granted === ObjectPermission.WRITE;
|
||||
}
|
||||
|
||||
abstract class BaseObjectAccessGroup implements ObjectAccessGroup {
|
||||
constructor(
|
||||
public readonly type: ObjectAccessGroupType,
|
||||
public readonly id: string,
|
||||
) {}
|
||||
|
||||
public abstract hasMember(userId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
function createObjectAccessGroup(
|
||||
group: ObjectAccessGroup,
|
||||
): BaseObjectAccessGroup {
|
||||
switch (group.type) {
|
||||
// Implement per access group type, e.g.:
|
||||
// case "USER_LIST":
|
||||
// return new UserListAccessGroup(group.id);
|
||||
default:
|
||||
throw new Error(`Unknown access group type: ${group.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setObjectAclPolicy(
|
||||
objectFile: File,
|
||||
aclPolicy: ObjectAclPolicy,
|
||||
): Promise<void> {
|
||||
const [exists] = await objectFile.exists();
|
||||
if (!exists) {
|
||||
throw new Error(`Object not found: ${objectFile.name}`);
|
||||
}
|
||||
|
||||
await objectFile.setMetadata({
|
||||
metadata: {
|
||||
[ACL_POLICY_METADATA_KEY]: JSON.stringify(aclPolicy),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getObjectAclPolicy(
|
||||
objectFile: File,
|
||||
): Promise<ObjectAclPolicy | null> {
|
||||
const [metadata] = await objectFile.getMetadata();
|
||||
const aclPolicy = metadata?.metadata?.[ACL_POLICY_METADATA_KEY];
|
||||
if (!aclPolicy) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(aclPolicy as string);
|
||||
}
|
||||
|
||||
export async function canAccessObject({
|
||||
userId,
|
||||
objectFile,
|
||||
requestedPermission,
|
||||
}: {
|
||||
userId?: string;
|
||||
objectFile: File;
|
||||
requestedPermission: ObjectPermission;
|
||||
}): Promise<boolean> {
|
||||
const aclPolicy = await getObjectAclPolicy(objectFile);
|
||||
if (!aclPolicy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
aclPolicy.visibility === "public" &&
|
||||
requestedPermission === ObjectPermission.READ
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aclPolicy.owner === userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const rule of aclPolicy.aclRules || []) {
|
||||
const accessGroup = createObjectAccessGroup(rule.group);
|
||||
if (
|
||||
(await accessGroup.hasMember(userId)) &&
|
||||
isPermissionAllowed(requestedPermission, rule.permission)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user