Skip to content

Commit

Permalink
Resolve #8: add support for featured users
Browse files Browse the repository at this point in the history
  • Loading branch information
big213 committed Apr 3, 2021
1 parent fe85a07 commit 0c60b9b
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Knex from "knex";

export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable("user", function (t) {
t.boolean("is_featured").notNullable().defaultTo(false);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable("user", function (t) {
t.dropColumn("is_featured");
});
}
1 change: 1 addition & 0 deletions backend/functions/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function up(knex: Knex): Promise<void[]> {
table.string("avatar").nullable();
table.string("country").nullable();
table.boolean("is_public").notNullable().defaultTo(true);
table.boolean("is_featured").notNullable().defaultTo(false);
table.integer("role").notNullable().defaultTo(2);
table.json("permissions").nullable();
table.dateTime("created_at").notNullable().defaultTo(knex.fn.now());
Expand Down
12 changes: 11 additions & 1 deletion backend/functions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ export type FilterByField<T> = {
| "user_create"
| "user_delete"
| "personalBest_create"
| "product_create";
| "product_create"
| "userUserFollowLink_get";
/**Enum stored as is*/ scoreMethod: "STANDARD" | "FMC" | "MBLD";
userSortByKey: "id" | "createdAt" | "updatedAt";
userGroupByKey: undefined;
Expand Down Expand Up @@ -130,6 +131,7 @@ export type FilterByField<T> = {
"userFilterByField/role": FilterByField<Scalars["userRole"]>;
"userFilterByField/name": FilterByField<Scalars["string"]>;
"userFilterByField/isPublic": FilterByField<Scalars["boolean"]>;
"userFilterByField/isFeatured": FilterByField<Scalars["boolean"]>;
"userFilterByField/userUserFollowLink/user.id": FilterByField<Scalars["id"]>;
"userFilterByField/userUserFollowLink/target.id": FilterByField<
Scalars["id"]
Expand All @@ -141,6 +143,7 @@ export type FilterByField<T> = {
role?: InputTypes["userFilterByField/role"];
name?: InputTypes["userFilterByField/name"];
isPublic?: InputTypes["userFilterByField/isPublic"];
isFeatured?: InputTypes["userFilterByField/isFeatured"];
"userUserFollowLink/user.id"?: InputTypes["userFilterByField/userUserFollowLink/user.id"];
"userUserFollowLink/target.id"?: InputTypes["userFilterByField/userUserFollowLink/target.id"];
};
Expand All @@ -164,6 +167,7 @@ export type FilterByField<T> = {
avatar?: Scalars["string"] | null;
country?: Scalars["string"] | null;
isPublic?: Scalars["boolean"];
isFeatured?: Scalars["boolean"];
role?: Scalars["userRole"];
permissions?: Scalars["userPermission"][] | null;
};
Expand All @@ -173,6 +177,7 @@ export type FilterByField<T> = {
avatar?: Scalars["string"] | null;
country?: Scalars["string"] | null;
isPublic?: Scalars["boolean"];
isFeatured?: Scalars["boolean"];
role?: Scalars["userRole"];
permissions?: Scalars["userPermission"][] | null;
};
Expand Down Expand Up @@ -290,6 +295,9 @@ export type FilterByField<T> = {
>;
"personalBestFilterByField/isCurrent": FilterByField<Scalars["boolean"]>;
"personalBestFilterByField/setSize": FilterByField<Scalars["number"]>;
"personalBestFilterByField/createdBy.userUserFollowLink/user.id": FilterByField<
Scalars["id"]
>;
personalBestFilterByObject: {
id?: InputTypes["personalBestFilterByField/id"];
"createdBy.id"?: InputTypes["personalBestFilterByField/createdBy.id"];
Expand All @@ -300,6 +308,7 @@ export type FilterByField<T> = {
happenedOn?: InputTypes["personalBestFilterByField/happenedOn"];
isCurrent?: InputTypes["personalBestFilterByField/isCurrent"];
setSize?: InputTypes["personalBestFilterByField/setSize"];
"createdBy.userUserFollowLink/user.id"?: InputTypes["personalBestFilterByField/createdBy.userUserFollowLink/user.id"];
};
personalBestPaginator: {
first?: Scalars["number"];
Expand Down Expand Up @@ -515,6 +524,7 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
avatar: { Type: Scalars["string"] | null; Args: undefined };
country: { Type: Scalars["string"] | null; Args: undefined };
isPublic: { Type: Scalars["boolean"]; Args: undefined };
isFeatured: { Type: Scalars["boolean"]; Args: undefined };
role: { Type: Scalars["userRole"]; Args: undefined };
permissions: { Type: Scalars["userPermission"][] | null; Args: undefined };
allPermissions: { Type: Scalars["userPermission"][]; Args: undefined };
Expand Down
22 changes: 18 additions & 4 deletions backend/functions/src/schema/models/user/service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AccessControlMap } from "../../../types";
import { PaginatedService } from "../../core/services";
import { userRoleKenum } from "../../enums";

Expand All @@ -19,6 +20,7 @@ export class UserService extends PaginatedService {
role: {},
name: {},
isPublic: {},
isFeatured: {},
"userUserFollowLink/user.id": {},
"userUserFollowLink/target.id": {},
};
Expand All @@ -33,12 +35,12 @@ export class UserService extends PaginatedService {
name: {},
};

accessControl = {
accessControl: AccessControlMap = {
getMultiple: ({ req, args, query }) => {
// if role, permissions, all_permissions, or email field requested, must be ADMIN
if (
query &&
Object.keys(query).some((field) =>
Object.keys(<any>query).some((field) =>
["role", "permissions", "allPermissions", "email"].includes(field)
) &&
req.user?.role !== userRoleKenum.ADMIN
Expand All @@ -63,7 +65,7 @@ export class UserService extends PaginatedService {
// if role, permissions, all_permissions, or email field requested, must be ADMIN
if (
query &&
Object.keys(query).some((field) =>
Object.keys(<any>query).some((field) =>
["role", "permissions", "allPermissions", "email"].includes(field)
) &&
req.user?.role !== userRoleKenum.ADMIN
Expand All @@ -88,6 +90,18 @@ export class UserService extends PaginatedService {
return result.isPublic === true || result.createdBy === req.user?.id;
},
// allowed to update if user created the item
update: generateItemCreatedByUserGuard(this),
update: async (inputs) => {
// only allowed to update "isPublic" if user was created by current user (admin will skip this check through A_A permissions)
const allowedUpdateFields = ["isPublic"];
if (
!Object.keys(inputs.args.fields).every((field) =>
allowedUpdateFields.includes(field)
)
) {
return false;
}

return generateItemCreatedByUserGuard(this)(inputs);
},
};
}
5 changes: 5 additions & 0 deletions backend/functions/src/schema/models/user/typeDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
defaultValue: true,
sqlOptions: { field: "is_public" },
}),
isFeatured: generateBooleanField({
allowNull: false,
defaultValue: false,
sqlOptions: { field: "is_featured" },
}),
role: generateEnumField({
scalarDefinition: Scalars.userRole,
allowNull: false,
Expand Down
23 changes: 16 additions & 7 deletions frontend/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -246,19 +246,28 @@ export default {
],
navItems: [
{
icon: 'mdi-account',
title: 'Public Users',
to: generateRoute('/public-users', {
icon: 'mdi-star',
title: 'Latest PBs',
to: generateRoute('/public-pbs', {
sortBy: ['happenedOn'],
sortDesc: [true],
}),
loginRequired: false,
},
{
icon: 'mdi-account-star',
title: 'Featured Users',
to: generateRoute('/featured-users', {
sortBy: ['createdAt'],
sortDesc: [true],
}),
loginRequired: false,
},
{
icon: 'mdi-star',
title: 'Latest PBs',
to: generateRoute('/public-pbs', {
sortBy: ['happenedOn'],
icon: 'mdi-account-details',
title: 'User Directory',
to: generateRoute('/public-users', {
sortBy: ['createdAt'],
sortDesc: [true],
}),
loginRequired: false,
Expand Down
7 changes: 7 additions & 0 deletions frontend/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export const User = <RecordInfo<'user'>>{
parseQueryValue: (val) => val === 'true',
inputType: 'switch',
},
isFeatured: {
text: 'Is Featured',
parseQueryValue: (val) => val === 'true',
inputType: 'switch',
},
createdAt: {
text: 'Created At',
component: TimeagoColumn,
Expand Down Expand Up @@ -121,6 +126,7 @@ export const User = <RecordInfo<'user'>>{
'role',
'permissions',
'isPublic',
'isFeatured',
],
},
viewOptions: {
Expand All @@ -133,6 +139,7 @@ export const User = <RecordInfo<'user'>>{
'role',
'permissions',
'isPublic',
'isFeatured',
'currentUserFollowing',
],
component: ViewUserInterface,
Expand Down
43 changes: 43 additions & 0 deletions frontend/pages/featured-users.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<CrudRecordPage
:record-info="recordInfo"
:locked-filters="lockedFilters"
:title="title"
:head="head"
icon="mdi-account-star"
></CrudRecordPage>
</template>

<script>
import CrudRecordPage from '~/components/page/crudRecordPage.vue'
import { PublicUsers } from '~/models/special'
export default {
components: {
CrudRecordPage,
},
data() {
return {
recordInfo: PublicUsers,
lockedFilters: [
{
field: 'isPublic',
operator: 'eq',
value: true,
},
{
field: 'isFeatured',
operator: 'eq',
value: true,
},
],
// override
head: {
title: 'Featured Users',
},
title: 'Featured Users',
}
},
}
</script>
10 changes: 10 additions & 0 deletions frontend/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type FilterByField<T> = {
| 'user_delete'
| 'personalBest_create'
| 'product_create'
| 'userUserFollowLink_get'
/**Enum stored as is*/ scoreMethod: 'STANDARD' | 'FMC' | 'MBLD'
userSortByKey: 'id' | 'createdAt' | 'updatedAt'
userGroupByKey: undefined
Expand Down Expand Up @@ -127,6 +128,7 @@ export type FilterByField<T> = {
'userFilterByField/role': FilterByField<Scalars['userRole']>
'userFilterByField/name': FilterByField<Scalars['string']>
'userFilterByField/isPublic': FilterByField<Scalars['boolean']>
'userFilterByField/isFeatured': FilterByField<Scalars['boolean']>
'userFilterByField/userUserFollowLink/user.id': FilterByField<Scalars['id']>
'userFilterByField/userUserFollowLink/target.id': FilterByField<Scalars['id']>
userFilterByObject: {
Expand All @@ -136,6 +138,7 @@ export type FilterByField<T> = {
role?: InputTypes['userFilterByField/role']
name?: InputTypes['userFilterByField/name']
isPublic?: InputTypes['userFilterByField/isPublic']
isFeatured?: InputTypes['userFilterByField/isFeatured']
'userUserFollowLink/user.id'?: InputTypes['userFilterByField/userUserFollowLink/user.id']
'userUserFollowLink/target.id'?: InputTypes['userFilterByField/userUserFollowLink/target.id']
}
Expand All @@ -159,6 +162,7 @@ export type FilterByField<T> = {
avatar?: Scalars['string'] | null
country?: Scalars['string'] | null
isPublic?: Scalars['boolean']
isFeatured?: Scalars['boolean']
role?: Scalars['userRole']
permissions?: Scalars['userPermission'][] | null
}
Expand All @@ -168,6 +172,7 @@ export type FilterByField<T> = {
avatar?: Scalars['string'] | null
country?: Scalars['string'] | null
isPublic?: Scalars['boolean']
isFeatured?: Scalars['boolean']
role?: Scalars['userRole']
permissions?: Scalars['userPermission'][] | null
}
Expand Down Expand Up @@ -285,6 +290,9 @@ export type FilterByField<T> = {
>
'personalBestFilterByField/isCurrent': FilterByField<Scalars['boolean']>
'personalBestFilterByField/setSize': FilterByField<Scalars['number']>
'personalBestFilterByField/createdBy.userUserFollowLink/user.id': FilterByField<
Scalars['id']
>
personalBestFilterByObject: {
id?: InputTypes['personalBestFilterByField/id']
'createdBy.id'?: InputTypes['personalBestFilterByField/createdBy.id']
Expand All @@ -295,6 +303,7 @@ export type FilterByField<T> = {
happenedOn?: InputTypes['personalBestFilterByField/happenedOn']
isCurrent?: InputTypes['personalBestFilterByField/isCurrent']
setSize?: InputTypes['personalBestFilterByField/setSize']
'createdBy.userUserFollowLink/user.id'?: InputTypes['personalBestFilterByField/createdBy.userUserFollowLink/user.id']
}
personalBestPaginator: {
first?: Scalars['number']
Expand Down Expand Up @@ -510,6 +519,7 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>
avatar: { Type: Scalars['string'] | null; Args: undefined }
country: { Type: Scalars['string'] | null; Args: undefined }
isPublic: { Type: Scalars['boolean']; Args: undefined }
isFeatured: { Type: Scalars['boolean']; Args: undefined }
role: { Type: Scalars['userRole']; Args: undefined }
permissions: { Type: Scalars['userPermission'][] | null; Args: undefined }
allPermissions: { Type: Scalars['userPermission'][]; Args: undefined }
Expand Down

0 comments on commit 0c60b9b

Please sign in to comment.