Skip to content

Commit 8d03d52

Browse files
authored
Prevent creating draft projects with taken ID (#1597)
* Prevent creating draft projects with a taken ID, because it's impossible to approve them. * Fix generate-gql-schema * Display project status instead of create form if already approved.
1 parent 030069c commit 8d03d52

File tree

7 files changed

+75
-4
lines changed

7 files changed

+75
-4
lines changed

backend/LexBoxApi/GraphQL/LexQueries.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ public async Task<IQueryable<Project>> ProjectById(LexBoxDbContext context, IPer
120120
return context.Projects.AsNoTracking().Where(p => p.Id == projectId);
121121
}
122122

123+
public record ProjectStatus(Guid Id, bool Exists, bool Deleted, string? AccessibleCode);
124+
125+
[GraphQLName("projectStatus")]
126+
public async Task<ProjectStatus> GetProjectStatus(LexBoxDbContext context, IPermissionService permissionService, LoggedInContext loggedInContext, Guid projectId)
127+
{
128+
var project = await context.Projects.Include(p => p.Users)
129+
.AsNoTracking().IgnoreQueryFilters()
130+
.SingleOrDefaultAsync(p => p.Id == projectId);
131+
132+
if (project is null) return new ProjectStatus(projectId, false, false, null);
133+
if (project.DeletedDate != null) return new ProjectStatus(projectId, true, true, null);
134+
135+
// explicitly check the project users in the DB, because if this endpoint returns stale results, it will result in confusion
136+
var hasPermission = project.Users.Any(u => u.UserId == loggedInContext.User.Id)
137+
|| await permissionService.CanViewProject(projectId);
138+
139+
if (!hasPermission) return new ProjectStatus(projectId, true, false, null);
140+
141+
return new ProjectStatus(projectId, true, false, project.Code);
142+
}
143+
123144
[UseProjection]
124145
public async Task<Project?> ProjectByCode(
125146
LexBoxDbContext dbContext,

backend/LexBoxApi/Services/DevGqlSchemaWriterService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static async Task GenerateGqlSchema(string[] args)
3232
.AddScoped<LexBoxDbContext>()
3333
.AddScoped<IPermissionService, PermissionService>()
3434
.AddScoped<ProjectService>()
35+
.AddScoped<FwHeadlessClient>()
3536
.AddScoped<UserService>()
3637
.AddScoped<LexAuthService>()
3738
.AddLexGraphQL(builder.Environment, true);

backend/LexBoxApi/Services/ProjectService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ public async Task UpdateFLExModelVersion(Guid projectId)
106106

107107
public async Task<Guid> CreateDraftProject(CreateProjectInput input)
108108
{
109+
var existingProject = await dbContext.Projects.FindAsync(input.Id);
110+
if (existingProject is not null)
111+
{
112+
throw new InvalidOperationException($"Project was already approved ({input.Id}: {existingProject.Code})");
113+
}
114+
109115
// No need for a transaction if we're just saving a single item
110116
var projectId = input.Id ?? Guid.NewGuid();
111117
dbContext.DraftProjects.Add(

frontend/schema.graphql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,13 @@ type ProjectMembersMustBeVerifiedForRole implements Error {
435435
message: String!
436436
}
437437

438+
type ProjectStatus {
439+
id: UUID!
440+
exists: Boolean!
441+
deleted: Boolean!
442+
accessibleCode: String
443+
}
444+
438445
type ProjectUsers {
439446
userId: UUID!
440447
user: User!
@@ -459,6 +466,7 @@ type Query {
459466
projectsByLangCodeAndOrg(input: ProjectsByLangCodeAndOrgInput! orderBy: [ProjectSortInput!] @cost(weight: "10")): [Project!]! @cost(weight: "10")
460467
projectsInMyOrg(input: ProjectsInMyOrgInput! where: ProjectFilterInput @cost(weight: "10") orderBy: [ProjectSortInput!] @cost(weight: "10")): [Project!]! @cost(weight: "10")
461468
projectById(projectId: UUID!): Project @cost(weight: "10")
469+
projectStatus(projectId: UUID!): ProjectStatus! @cost(weight: "10")
462470
projectByCode(code: String!): Project @cost(weight: "10")
463471
orgs(where: OrganizationFilterInput @cost(weight: "10") orderBy: [OrganizationSortInput!] @cost(weight: "10")): [Organization!]! @cost(weight: "10")
464472
myOrgs(where: OrganizationFilterInput @cost(weight: "10") orderBy: [OrganizationSortInput!] @cost(weight: "10")): [Organization!]! @cost(weight: "10")

frontend/src/lib/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ Lexbox is free and [open source](https://github.com/sillsdev/languageforge-lexbo
207207
"language_code_invalid": "Language code can only include lowercase a-z, digits and '-', and cannot start with '-'",
208208
"name": "Name",
209209
"name_description": "E.g. the name of the language you're working on",
210+
"already_approved": "This project request was already approved.",
211+
"already_approved_deleted": "The created project was deleted.",
212+
"go_to_project": "Go to project",
210213
"maybe_related": "Possibly related projects:",
211214
"maybe_related_description": "Perhaps you want to join one of these instead?",
212215
"ask_to_join": "Ask to join",

frontend/src/routes/(authenticated)/project/create/+page.svelte

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
$: user = data.user;
2727
let requestingUser : typeof data.requestingUser;
2828
$: myOrgs = data.myOrgs ?? [];
29+
$: projectStatus = data.projectStatus;
2930
3031
const { notifySuccess, notifyWarning } = useNotifications();
3132
@@ -197,6 +198,19 @@
197198
</script>
198199

199200
<TitlePage title={$t('project.create.title')}>
201+
{#if projectStatus?.exists}
202+
<div class="text-center">
203+
<div>
204+
<p>{$t('project.create.already_approved')}</p>
205+
{#if projectStatus.deleted}
206+
<p>{$t('project.create.already_approved_deleted')}</p>
207+
{/if}
208+
</div>
209+
{#if projectStatus.accessibleCode}
210+
<a class="btn btn-primary mt-4" href={projectUrl({ code: projectStatus.accessibleCode })}>{$t('project.create.go_to_project')}</a>
211+
{/if}
212+
</div>
213+
{:else}
200214
<Form {enhance}>
201215
<Input
202216
label={$t('project.create.name')}
@@ -344,4 +358,5 @@
344358
</SubmitButton>
345359
{/if}
346360
</Form>
361+
{/if}
347362
</TitlePage>

frontend/src/routes/(authenticated)/project/create/+page.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {$OpResult, AskToJoinProjectMutation, CreateProjectInput, CreateProjectMutation, ProjectsByLangCodeAndOrgQuery, ProjectsByNameAndOrgQuery} from '$lib/gql/types';
1+
import type {$OpResult, AskToJoinProjectMutation, CreateProjectInput, CreateProjectMutation, LoadRequestingUserQuery, ProjectStatus, ProjectsByLangCodeAndOrgQuery, ProjectsByNameAndOrgQuery} from '$lib/gql/types';
22
import {getClient, graphql} from '$lib/gql';
33

44
import type {PageLoadEvent} from './$types';
@@ -8,7 +8,7 @@ import {isGuid} from '$lib/util/guid';
88
export async function load(event: PageLoadEvent) {
99
const userIsAdmin = (await event.parent()).user.isAdmin;
1010
const requestingUserId = getSearchParam<CreateProjectInput>('projectManagerId', event.url.searchParams);
11-
let requestingUser = null;
11+
let requestingUser: NonNullable<NonNullable<NonNullable<LoadRequestingUserQuery>['users']>['items']>[number] | undefined;
1212
const client = getClient();
1313
if (userIsAdmin && isGuid(requestingUserId)) {
1414
const userResultsPromise = await client.query(graphql(`
@@ -36,7 +36,7 @@ export async function load(event: PageLoadEvent) {
3636
requestingUser = userResultsPromise.data?.users?.items?.[0];
3737
}
3838

39-
let orgs;
39+
let orgs: undefined | { id: string, name: string }[];
4040
if (userIsAdmin) {
4141
const orgsPromise = await client.query(graphql(`
4242
query loadOrgs {
@@ -58,7 +58,24 @@ export async function load(event: PageLoadEvent) {
5858
`), {}, { fetch: event.fetch });
5959
orgs = myOrgsPromise.data?.myOrgs;
6060
}
61-
return { requestingUser, myOrgs: orgs };
61+
62+
const projectId = getSearchParam<CreateProjectInput>('id', event.url.searchParams);
63+
let projectStatus: ProjectStatus | undefined;
64+
if (projectId) {
65+
const projectStatusResult = await client.query(graphql(`
66+
query loadProjectStatus($projectId: UUID!) {
67+
projectStatus(projectId: $projectId) {
68+
id
69+
exists
70+
deleted
71+
accessibleCode
72+
}
73+
}
74+
`), {projectId}, {fetch: event.fetch});
75+
projectStatus = projectStatusResult.data?.projectStatus;
76+
}
77+
78+
return { requestingUser, myOrgs: orgs, projectStatus };
6279
}
6380

6481
export async function _createProject(input: CreateProjectInput): $OpResult<CreateProjectMutation> {

0 commit comments

Comments
 (0)