mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: new modpack permissions system (#6005)
* Begin external projects moderator database frontend * add copy link button * begin project page permissions settings * MEL database backend routes * include filename in external files * wip: when uploading a version file, fetch its overrides as a list * wip: override license checks * improve FileHost ref counting * file host read capability * scan files when inserting version file * add dependency sha1 field * clean up version files * wip: attributions * update s3 file host * attribution scanning basic works * works * insert attribution info after resolving * add routes * remove dep sha1 stuff * prepr * wip: override file sources * add files_missing_attributions to versions * return extended version info + attributed at/by * hook up frontend to backend (mostly) * expose version date published * withholding version visibility * frontend work * prepr * use api-client for img upload * moar frontend * prepr * Add schema to attribution resolution and Flame project results * sqlx prepare * changes * remove feature flag, fix optional proof images * fix schema * fmt * fix deletion and file fetch * prepare * fix admonition * update frontend stuff to new schema * prepr * attribution on dependencies * fixes * sqlx prepare * fixes * routes * fix routes * Version grandfathering * prepare * wip: bulk routes * pushing what i've got rn * include link in NoPermission * change hash insert to bulk route * query flame even if entry in MEL * delete file with weird name * Prioritise putting override files in existing groups even with ExternalLicense * fix how hex bytes are handled in route * feat: coolbot moderation changes (#6215) * Update moderator checklist * move permissions stage order * Updated nagContext.versions to v3, added nag for permissions * Update permissions.vue default messages * prepr --------- Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com> * QA * prepr * should group by project * return attribution resolution correctly * updated by moderator info * Track what moderator reviewed an attribution moderation status * default deser FMA field * new version page * clean up fetching + add a couple missing features * qa items * prepr * provide moderation package stuff with DI * format? * don't redact moderated_at * move supplementary resources * Reorganize moderation messages. * Quick replies for external content permissions. * prepare * QA * allow exempting projects * Ignore Flame projects which 404 * fix ci * fix cross project attribution stuff * Fix permission error * change what files get cscanned * add more logging * QA Jun 22 * fix * idempotency * Expose route for rescanning * update blog link --------- Co-authored-by: aecsocket <aecsocket@tutanota.com> Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com> Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
This commit is contained in:
co-authored by
coolbot100s
aecsocket
aecsocket
parent
a686a93858
commit
e7926083fb
@@ -20,6 +20,7 @@ import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
|
||||
import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth'
|
||||
import { LabrinthAffiliateInternalModule } from './labrinth/affiliate/internal'
|
||||
import { LabrinthAnalyticsV3Module } from './labrinth/analytics/v3'
|
||||
import { LabrinthAttributionInternalModule } from './labrinth/attribution/internal'
|
||||
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
|
||||
import { LabrinthAuthV2Module } from './labrinth/auth/v2'
|
||||
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
||||
@@ -28,6 +29,7 @@ import { LabrinthCollectionsModule } from './labrinth/collections'
|
||||
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
||||
import { LabrinthFriendsV3Module } from './labrinth/friends/v3'
|
||||
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
||||
import { LabrinthImagesV3Module } from './labrinth/images/v3'
|
||||
import { LabrinthLimitsV3Module } from './labrinth/limits/v3'
|
||||
import { LabrinthModerationInternalModule } from './labrinth/moderation/internal'
|
||||
import { LabrinthNotificationsV2Module } from './labrinth/notifications/v2'
|
||||
@@ -91,12 +93,14 @@ export const MODULE_REGISTRY = {
|
||||
labrinth_analytics_v3: LabrinthAnalyticsV3Module,
|
||||
labrinth_auth_internal: LabrinthAuthInternalModule,
|
||||
labrinth_auth_v2: LabrinthAuthV2Module,
|
||||
labrinth_attribution_internal: LabrinthAttributionInternalModule,
|
||||
labrinth_billing_internal: LabrinthBillingInternalModule,
|
||||
labrinth_campaign_internal: LabrinthCampaignInternalModule,
|
||||
labrinth_collections: LabrinthCollectionsModule,
|
||||
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
||||
labrinth_friends_v3: LabrinthFriendsV3Module,
|
||||
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
||||
labrinth_images_v3: LabrinthImagesV3Module,
|
||||
labrinth_moderation_internal: LabrinthModerationInternalModule,
|
||||
labrinth_notifications_v2: LabrinthNotificationsV2Module,
|
||||
labrinth_oauth_internal: LabrinthOAuthInternalModule,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
const BASE62_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
/**
|
||||
* Decode a base62-encoded ID string into a number.
|
||||
* The backend serializes attribution group IDs as base62 strings in responses,
|
||||
* but the assign/update endpoints expect raw integer IDs in their request payloads.
|
||||
*/
|
||||
function decodeBase62Id(id: string): number {
|
||||
let value = 0
|
||||
for (const char of id) {
|
||||
const digit = BASE62_CHARS.indexOf(char)
|
||||
if (digit < 0) {
|
||||
throw new Error(`Invalid base62 character "${char}" in id "${id}"`)
|
||||
}
|
||||
value = value * 62 + digit
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new Error(`Base62 id "${id}" exceeds safe integer range`)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export class LabrinthAttributionInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_attribution_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* List attribution groups for a project
|
||||
* GET /_internal/attribution/{project_id}
|
||||
*/
|
||||
public async listProjectAttribution(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Attribution.Internal.AttributionGroup[]> {
|
||||
return this.client.request<Labrinth.Attribution.Internal.AttributionGroup[]>(
|
||||
`/attribution/${projectId}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an attribution group's attribution payload.
|
||||
* PATCH /_internal/attribution/group/{group_id}
|
||||
*
|
||||
* @param groupId - The base62 attribution group id (as returned from listProjectAttribution).
|
||||
*/
|
||||
public async updateGroup(
|
||||
groupId: string,
|
||||
body: Labrinth.Attribution.Internal.UpdateGroupRequest,
|
||||
): Promise<void> {
|
||||
const numericId = decodeBase62Id(groupId)
|
||||
return this.client.request<void>(`/attribution/group/${numericId}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reassign a file (by sha1) to another attribution group within the same project.
|
||||
* POST /_internal/attribution/assign
|
||||
*
|
||||
* @param body.target_group_id - The base62 id of the attribution group to assign the file to.
|
||||
*/
|
||||
public async assignFileToGroup(body: {
|
||||
sha1: string
|
||||
target_group_id: string
|
||||
project_id: string
|
||||
}): Promise<void> {
|
||||
const wireBody: Labrinth.Attribution.Internal.AssignRequest = {
|
||||
sha1: body.sha1,
|
||||
target_group_id: decodeBase62Id(body.target_group_id),
|
||||
project_id: body.project_id,
|
||||
}
|
||||
return this.client.request<void>('/attribution/assign', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: wireBody,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a file (by sha1) out of its current attribution group into a new group.
|
||||
* POST /_internal/attribution/split
|
||||
*/
|
||||
public async splitFile(body: Labrinth.Attribution.Internal.SplitRequest): Promise<void> {
|
||||
return this.client.request<void>('/attribution/split', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,18 @@ export class LabrinthExternalProjectsInternalModule extends AbstractModule {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async addFile(
|
||||
data: Labrinth.ExternalProjects.Internal.AddFileRequest,
|
||||
): Promise<Labrinth.ExternalProjects.Internal.ExternalProject> {
|
||||
return this.client.request<Labrinth.ExternalProjects.Internal.ExternalProject>(
|
||||
'/moderation/external-license/file',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle } from '../../../types/upload'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
function buildImageQueryParams(
|
||||
ext: Labrinth.Images.v3.ImageExtension,
|
||||
target: Labrinth.Images.v3.UploadImageParams,
|
||||
): Record<string, string> {
|
||||
const params: Record<string, string> = {
|
||||
ext,
|
||||
context: target.context,
|
||||
}
|
||||
switch (target.context) {
|
||||
case 'project':
|
||||
params.project_id = target.project_id
|
||||
break
|
||||
case 'version':
|
||||
params.version_id = target.version_id
|
||||
break
|
||||
case 'thread_message':
|
||||
params.thread_message_id = target.thread_message_id
|
||||
break
|
||||
case 'report':
|
||||
params.report_id = target.report_id
|
||||
break
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export class LabrinthImagesV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_images_v3'
|
||||
}
|
||||
|
||||
public uploadImage(
|
||||
file: File | Blob,
|
||||
ext: Labrinth.Images.v3.ImageExtension,
|
||||
target: Labrinth.Images.v3.UploadImageParams,
|
||||
): UploadHandle<Labrinth.Images.v3.UploadedImage> {
|
||||
return this.client.upload<Labrinth.Images.v3.UploadedImage>('/image', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
file,
|
||||
params: buildImageQueryParams(ext, target),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './analytics/v3'
|
||||
export * from './attribution/internal'
|
||||
export * from './auth/internal'
|
||||
export * from './auth/v2'
|
||||
export * from './billing/internal'
|
||||
@@ -6,6 +7,7 @@ export * from './collections'
|
||||
export * from './external-projects/internal'
|
||||
export * from './friends/v3'
|
||||
export * from './globals/internal'
|
||||
export * from './images/v3'
|
||||
export * from './limits/v3'
|
||||
export * from './moderation/internal'
|
||||
export * from './notifications/v2'
|
||||
|
||||
@@ -57,4 +57,15 @@ export class LabrinthModerationInternalModule extends AbstractModule {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async setProjectJudgements(
|
||||
judgements: Labrinth.Moderation.Internal.ProjectJudgements,
|
||||
): Promise<void> {
|
||||
return this.client.request<void>('/moderation/project', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: judgements,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +249,144 @@ export namespace Labrinth {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Attribution {
|
||||
export namespace Internal {
|
||||
export type AttributionPermissionKind =
|
||||
| 'license'
|
||||
| 'my_project'
|
||||
| 'special_permissions'
|
||||
| 'globally_allowed'
|
||||
| 'no_permission'
|
||||
export type AttributionResolutionKind = AttributionPermissionKind
|
||||
|
||||
export type AttributionLicense = string | { name: string }
|
||||
|
||||
export type AttributionModerationStatusKind = 'not_allowed' | 'approved' | 'bad_proof'
|
||||
|
||||
export type AttributionModerationStatus = {
|
||||
kind: AttributionModerationStatusKind
|
||||
reason: string
|
||||
moderated_at?: string
|
||||
moderated_by?: string
|
||||
}
|
||||
|
||||
export type AttributionResolutionBase = {
|
||||
notes: string
|
||||
image_urls: string[]
|
||||
moderation_status?: AttributionModerationStatus | null
|
||||
updated_by_moderator: boolean
|
||||
}
|
||||
|
||||
export type AttributionResolution =
|
||||
| (AttributionResolutionBase & {
|
||||
kind: 'license'
|
||||
license: AttributionLicense
|
||||
link_to_work: string
|
||||
})
|
||||
| (AttributionResolutionBase & {
|
||||
kind: 'my_project'
|
||||
license: AttributionLicense
|
||||
})
|
||||
| (AttributionResolutionBase & {
|
||||
kind: 'special_permissions'
|
||||
link_to_work: string
|
||||
})
|
||||
| (AttributionResolutionBase & {
|
||||
kind: 'globally_allowed'
|
||||
link_to_work: string
|
||||
})
|
||||
| (AttributionResolutionBase & {
|
||||
kind: 'no_permission'
|
||||
link_to_work?: string
|
||||
})
|
||||
|
||||
export type FlameProject = {
|
||||
id: number
|
||||
title: string
|
||||
url: string
|
||||
icon_url: string
|
||||
}
|
||||
|
||||
export type AttributionFile = {
|
||||
name: string
|
||||
sha1: string
|
||||
versions: string[]
|
||||
moderation_external_license_id?: number
|
||||
moderation_external_license?: ExternalProjects.Internal.ExternalProject
|
||||
}
|
||||
|
||||
export type AttributionVersionInfo = {
|
||||
id: string
|
||||
name: string
|
||||
version_number: string
|
||||
date_created: string
|
||||
}
|
||||
|
||||
export type AttributionGroup = {
|
||||
id: string
|
||||
flame_project: FlameProject | null
|
||||
attribution: AttributionResolution | null
|
||||
attributed_at: string | null
|
||||
attributed_by: string | null
|
||||
files: AttributionFile[]
|
||||
versions: AttributionVersionInfo[]
|
||||
}
|
||||
|
||||
export type UpdateGroupRequest = {
|
||||
attribution: AttributionResolution
|
||||
}
|
||||
|
||||
export type AssignRequest = {
|
||||
sha1: string
|
||||
target_group_id: number
|
||||
project_id: string
|
||||
}
|
||||
|
||||
export type SplitRequest = {
|
||||
sha1: string
|
||||
project_id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Images {
|
||||
export namespace v3 {
|
||||
/** Extensions accepted by POST /v3/image (Labrinth image pipeline). */
|
||||
export type ImageExtension = 'bmp' | 'gif' | 'jpeg' | 'jpg' | 'png' | 'webp'
|
||||
|
||||
/** `context` query values accepted by POST /v3/image. */
|
||||
export type ImageUploadContext = 'project' | 'version' | 'thread_message' | 'report'
|
||||
|
||||
export type UploadedImage = {
|
||||
id: string
|
||||
url: string
|
||||
size: number
|
||||
created: string
|
||||
owner_id: string
|
||||
} & (
|
||||
| { context: 'project'; project_id: string }
|
||||
| { context: 'version'; version_id: string }
|
||||
| { context: 'thread_message'; thread_message_id: string }
|
||||
| { context: 'report'; report_id: string }
|
||||
)
|
||||
|
||||
export type UploadedImageFor<C extends ImageUploadContext> = Extract<
|
||||
UploadedImage,
|
||||
{ context: C }
|
||||
>
|
||||
|
||||
/**
|
||||
* Target for POST /v3/image (per-context id query params, plus `context`).
|
||||
* `ext` is passed as a separate argument on the client module.
|
||||
*/
|
||||
export type UploadImageParams =
|
||||
| { context: 'project'; project_id: string }
|
||||
| { context: 'version'; version_id: string }
|
||||
| { context: 'thread_message'; thread_message_id: string }
|
||||
| { context: 'report'; report_id: string }
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Analytics {
|
||||
export namespace v3 {
|
||||
export type AnalyticsEventId = number
|
||||
@@ -1147,11 +1285,47 @@ export namespace Labrinth {
|
||||
|
||||
// TODO: consolidate duplicated types between v2 and v3 versions
|
||||
export namespace v3 {
|
||||
export type FlameProject = {
|
||||
id: number
|
||||
title: string
|
||||
url: string
|
||||
icon_url: string
|
||||
}
|
||||
|
||||
export type DependencyAttribution = {
|
||||
flame_project?: FlameProject
|
||||
resolution?: DependencyAttributionResolution
|
||||
}
|
||||
|
||||
export type DependencyAttributionResolution =
|
||||
| {
|
||||
kind: 'license'
|
||||
license: Labrinth.Attribution.Internal.AttributionLicense
|
||||
link_to_work: string
|
||||
}
|
||||
| {
|
||||
kind: 'globally_allowed'
|
||||
link_to_work: string
|
||||
}
|
||||
| {
|
||||
kind: 'my_project'
|
||||
license: Labrinth.Attribution.Internal.AttributionLicense
|
||||
}
|
||||
| {
|
||||
kind: 'special_permissions'
|
||||
link_to_work: string
|
||||
}
|
||||
| {
|
||||
kind: 'no_permission'
|
||||
link_to_work?: string
|
||||
}
|
||||
|
||||
export interface Dependency {
|
||||
dependency_type: Labrinth.Versions.v2.DependencyType
|
||||
project_id?: string
|
||||
file_name?: string
|
||||
version_id?: string
|
||||
attribution?: DependencyAttribution
|
||||
}
|
||||
|
||||
export interface GetProjectVersionsParams {
|
||||
@@ -1174,12 +1348,12 @@ export namespace Labrinth {
|
||||
| 'signature'
|
||||
| 'unknown'
|
||||
|
||||
export interface VersionFileHash {
|
||||
sha512: string
|
||||
sha1: string
|
||||
export type FileHashType = 'sha512' | 'sha1'
|
||||
export type VersionFileHash = {
|
||||
[key in FileHashType]: string
|
||||
}
|
||||
|
||||
interface VersionFile {
|
||||
export interface VersionFile {
|
||||
hashes: VersionFileHash
|
||||
url: string
|
||||
filename: string
|
||||
@@ -1211,6 +1385,7 @@ export namespace Labrinth {
|
||||
date_published: string
|
||||
downloads: number
|
||||
files: VersionFile[]
|
||||
files_missing_attribution?: string[]
|
||||
environment?: Labrinth.Projects.v3.Environment
|
||||
mrpack_loaders?: string[]
|
||||
|
||||
@@ -1658,6 +1833,28 @@ export namespace Labrinth {
|
||||
export type ReleaseLockResponse = {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export type ProjectJudgementStatus = ExternalProjects.Internal.ExternalLicenseStatus
|
||||
|
||||
export type FlameJudgement = {
|
||||
type: 'flame'
|
||||
id: number
|
||||
status: ProjectJudgementStatus
|
||||
link: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type UnknownJudgement = {
|
||||
type: 'unknown'
|
||||
status: ProjectJudgementStatus
|
||||
proof?: string
|
||||
link?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type ProjectJudgement = FlameJudgement | UnknownJudgement
|
||||
|
||||
export type ProjectJudgements = Record<string, ProjectJudgement>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1819,7 +2016,7 @@ export namespace Labrinth {
|
||||
inserted_by: number | null
|
||||
updated_at: string | null
|
||||
updated_by: number | null
|
||||
linked_files: LinkedFile[]
|
||||
linked_files?: LinkedFile[]
|
||||
}
|
||||
|
||||
export type SearchRequest = {
|
||||
@@ -1835,6 +2032,11 @@ export namespace Labrinth {
|
||||
proof?: string
|
||||
flame_project_id?: number
|
||||
}
|
||||
|
||||
export type AddFileRequest = {
|
||||
hashes: string[]
|
||||
license_id: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user