Skip to content
This repository was archived by the owner on Nov 19, 2021. It is now read-only.

Local auth and user data #18

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ SENTRY_DSN=https://[email protected]/544
JWT_SECRET=

DB_URL=postgresql://postgres:postgres@meetup-db/meetup

MAILSERVER_ADDRESS=
MAILSERVER_PORT=
MAILSERVER_USERNAME=
MAILSERVER_PASSWORD=

139 changes: 139 additions & 0 deletions api/graphql/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* eslint-disable camelcase */
export interface ImageVariant {
url: string;
width: number;
height: number;
}

export interface Image {
original: ImageVariant;
large: ImageVariant;
medium: ImageVariant;
small: ImageVariant;
}

export interface Industry {
id: string;
name: string;
}

export interface Company {
id: string;
name: string;
brand_name: string;
short_description: string;
homepage_url: string;
address: string;
logo: Image | null;
cover: Image | null;
industry: Industry;
}

export interface BasicUser {
id: string;
email: string;
name: string;
role: string;
}

export interface User {
id: string;
email: string;
first_name: string;
last_name: string;
name: string;
role: string;
companies: Company[];
resume: {
uid: string;
}
}

export interface Presentation {
id: string;
description: string;
occures_at: string;
location: string | null;
title: string;
topic: string | null;
presenter_bio: string;
presenterPhoto: Image | null;
}

export interface Workshop {
id: string;
approved: boolean;
name: string;
description: string;
occures_at: string;
location: string | null;
}

export interface ResumeAward {
title: string;
year: string;
}

export interface ResumeEducation {
name: string;
year: string;
module: string;
awarded_title: string;
}

export interface ResumeLanguage {
name: string;
skill_level:
"A1" |
"A2" |
"B1" |
"B2" |
"C1" |
"C2";
}

export interface ResumeSkill {
name: string;
}

export interface ResumeComputerSkill {
name: string;
}

export interface ResumeWorkExperience {
company: string;
years: string;
description: string;
current_employer: boolean;
}

export interface BasicResume {
id: string;
user_id: string;
uid: string;
first_name: string;
last_name: string;
email: string;
created_at: string;
updated_at: string;
}

export interface ResumeSections {
educations: ResumeEducation[];
work_experiences: ResumeWorkExperience[];
computer_skills: ResumeComputerSkill[];
skills: ResumeSkill[];
languages: ResumeLanguage[];
awards: ResumeAward[];
}

export interface ResumeInfo extends BasicResume {
city: string;
birthday: string;
phone: string;
github_url: string;
linkedin_url: string;
resume_file_data: string;
}

export type Resume = ResumeInfo & Partial<ResumeSections>;
60 changes: 43 additions & 17 deletions api/helpers/fetchCache.js → api/helpers/fetchCache.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,81 @@
import {
Opaque,
} from "type-fest";
import {
AtomicBool,
} from "./atomic";

export const cachedFetcher = (timeoutMs, fetchFn, cacheKeyFn = () => "default") => {
const getBaseData = () => ({
export type CacheKey = Opaque<string, "CacheKey">;

type Promised<T> = Promise<T> | T;
export type FetchFn<T> = (...args: unknown[]) => Promised<T>;
export type KeyFn = (...args: unknown[]) => CacheKey;

interface ICache<T> {
time: number;
data: T | null;
fetching: Readonly<AtomicBool>;
}

export const cachedFetcher = <T>(
timeoutMs: number,
fetchFn: FetchFn<T>,
cacheKeyFn: KeyFn = () => "default" as CacheKey,
): (...args: unknown[]) => Promise<T> => {
type Cache = ICache<T>;

const getBaseData = (): Cache => ({
time: 0,
data: null,
fetching: new AtomicBool(),
});

const cache = {};
const cache: Record<CacheKey, Cache> = {};

const cacheSet = (cacheKey, key, value) => {
function cacheSet(cacheKey: CacheKey, key: "time", value: number): void;
function cacheSet(cacheKey: CacheKey, key: "data", value: T): void;
function cacheSet(cacheKey: CacheKey, key: keyof Cache, value): void {
if (!(cacheKey in cache)) {
cache[cacheKey] = getBaseData();
}

cache[cacheKey][key] = value;
};
}

const cacheGet = (cacheKey, key) => {
function cacheGet(cacheKey: CacheKey, key: "time"): number;
function cacheGet(cacheKey: CacheKey, key: "data"): T;
function cacheGet(cacheKey: CacheKey, key: "fetching"): AtomicBool;
function cacheGet(cacheKey: CacheKey, key: keyof Cache) {
if (!(cacheKey in cache)) {
cache[cacheKey] = getBaseData();
}

return cache[cacheKey][key];
};
}

const timeMs = () => {
const timeMs = (): number => {
const hrTime = process.hrtime();

return hrTime[0] * 1000 + hrTime[1] / 1000000;
};

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const setData = (key, data) => {
const setData = (key: CacheKey, data: T): void => {
cacheSet(key, "data", data);
cacheSet(key, "time", timeMs());
};

const setFetching = (key, fetching) => {
const setFetching = (key: CacheKey, fetching: boolean): void => {
cacheGet(key, "fetching").value = fetching;
};

const isFetching =
(key) =>
(key: CacheKey): boolean =>
cacheGet(key, "fetching").value
;

const testAndSetFetching = (key, ifItIs, thenSetTo) => {
const testAndSetFetching = (key: CacheKey, ifItIs: boolean, thenSetTo: boolean): boolean => {
const oldValue = cacheGet(key, "fetching").testAndSet(ifItIs, thenSetTo);
const newValue = isFetching(key);

Expand All @@ -62,7 +88,7 @@ export const cachedFetcher = (timeoutMs, fetchFn, cacheKeyFn = () => "default")
}
};

const waitForFetchingToBe = async (key, fetching) => {
const waitForFetchingToBe = async (key: CacheKey, fetching: boolean): Promise<boolean> => {
while (isFetching(key) !== fetching) {
await sleep(10);
}
Expand All @@ -71,22 +97,22 @@ export const cachedFetcher = (timeoutMs, fetchFn, cacheKeyFn = () => "default")
};

const hasData =
(key) =>
(key: CacheKey): boolean =>
Boolean(getData(key))
;

const getData =
(key) =>
(key: CacheKey): T =>
cacheGet(key, "data")
;

const hasFreshCache =
(key) =>
(key: CacheKey): boolean =>
hasData(key) &&
(timeMs() - timeoutMs) <= cacheGet(key, "time")
;

return async (...args) => {
return async (...args: unknown[]): Promise<T> => {
const key = cacheKeyFn(...args);
// console.log("CACHE GET", key);

Expand Down
2 changes: 1 addition & 1 deletion api/helpers/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ export class HttpStatus {


export const internalRequest = async (method, url, ...rest) => {
const baseUrl = `http://localhost:${ "development" === process.env.NODE_ENV ? "3000" : process.env.PORT }/api`;
const baseUrl = `http://localhost:${ process.env.PORT }/api`;
const fetchUrl = `${ baseUrl }${ url }`;

try {
Expand Down
4 changes: 2 additions & 2 deletions api/helpers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "./token";
import {
hasPermission,
RoleNames,
Role,
} from "./permissions";
import {
error,
Expand Down Expand Up @@ -49,7 +49,7 @@ export const injectAuthData =
export const requireAuth =
({
fullUserData = false,
role = RoleNames.BASE,
role = Role.base,
} = {}) =>
async (req, res, next) => {
await injectAuthData({ fullUserData })(req);
Expand Down
32 changes: 0 additions & 32 deletions api/helpers/permissions.js

This file was deleted.

45 changes: 45 additions & 0 deletions api/helpers/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import flow from "lodash/fp/flow";
import values from "lodash/fp/values";
import fromPairs from "lodash/fp/fromPairs";
import map from "lodash/fp/map";

const mapWithIndex = map.convert({ cap: false });

// This is just a noop function to force the TS compiler to typecheck the enum
const ensureEnumKeysSameAsValues = <T>(_kv: { [K in keyof T]: K }) => null;

export enum Role {
base = "base",
company = "company",
student = "student",
moderator = "moderator",
admin = "admin",
}

ensureEnumKeysSameAsValues(Role);

const roleNameToPriority: Record<Role, number> =
flow(
values,
mapWithIndex((value: Role, i: number) => [ value, i ]),
fromPairs,
)(Role)
;

export const hasPermission =
(
minimumRoleName: Role,
currentRoleName: Role,
): boolean =>
roleNameToPriority[minimumRoleName] <= roleNameToPriority[currentRoleName]
;

const isAtLeast =
(role: Role) =>
(roleName: Role) =>
hasPermission(role, roleName)
;

export const isAdmin = isAtLeast(Role.admin);
export const isModerator = isAtLeast(Role.moderator);
export const isStudent = isAtLeast(Role.student);
Loading