mirror of
https://github.com/quepasaevents/qpa-client.git
synced 2023-12-14 05:33:02 +01:00
Set up testing environment
This commit is contained in:
parent
5dbc1888f8
commit
3e5f83a6fc
42 changed files with 2638 additions and 16227 deletions
12
jest-setup.js
Normal file
12
jest-setup.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
require("ts-node/register");
|
||||||
|
|
||||||
|
// If you want to reference other typescript modules, do it via require:
|
||||||
|
// const { setup } = require("./setup");
|
||||||
|
|
||||||
|
module.exports = async function() {
|
||||||
|
// Call your initialization methods here.
|
||||||
|
if (!process.env.TEST_HOST) {
|
||||||
|
// await setup();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -6,6 +6,9 @@
|
||||||
"repository": "git@gitlab.com:amiiit/gcf-auth.git",
|
"repository": "git@gitlab.com:amiiit/gcf-auth.git",
|
||||||
"author": "Amit Jakubowicz <a.jakubowicz@travelaudience.com>",
|
"author": "Amit Jakubowicz <a.jakubowicz@travelaudience.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"resolutions": {
|
||||||
|
"@types/node": "11.11.0"
|
||||||
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"webapp",
|
"webapp",
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import { ConnectionOptions } from 'typeorm'
|
import {ConnectionOptions} from 'typeorm'
|
||||||
|
import { User } from './src/Auth/User.entity'
|
||||||
|
|
||||||
const config: ConnectionOptions = {
|
const config: ConnectionOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: process.env.POSTGRES_HOST || 'localhost',
|
host: process.env.POSTGRES_HOST || 'localhost',
|
||||||
port: Number(process.env.POSTGRES_PORT || 5432),
|
port: Number(process.env.POSTGRES_PORT || 5432),
|
||||||
username: process.env.POSTGRES_USER || 'admin',
|
database: process.env.POSTGRES_DB || 'qpa',
|
||||||
password: process.env.POSTGRES_PASSWORD || 'admin',
|
entities: ["src/**/*.entity.ts"],
|
||||||
database: process.env.POSTGRES_DB || 'gpa',
|
|
||||||
entities: [
|
|
||||||
__dirname + '/src/**/*.entity{.ts,.js}',
|
|
||||||
],
|
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
|
logging: "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testConfig: ConnectionOptions = {
|
||||||
|
...config,
|
||||||
|
database: 'qpa-test',
|
||||||
|
dropSchema: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -4,20 +4,18 @@
|
||||||
"codegen": "gql2ts ./src/schema.graphql -o ./src/@types/index.d.ts",
|
"codegen": "gql2ts ./src/schema.graphql -o ./src/@types/index.d.ts",
|
||||||
"lint": "tslint --project tsconfig.json",
|
"lint": "tslint --project tsconfig.json",
|
||||||
"build": "tsc --version && tsc",
|
"build": "tsc --version && tsc",
|
||||||
"start": "ts-node src/index.ts"
|
"start": "nodemon --exec ts-node src/index.ts",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/express-graphql": "^0.6.1",
|
"@types/graphql": "^14.0.7",
|
||||||
"@types/graphql": "^14.0.1",
|
"apollo-server": "^2.4.8",
|
||||||
"@types/mongodb": "^3.1.10",
|
"apollo-server-testing": "^2.4.8",
|
||||||
"apollo-server": "^2.1.0",
|
"atob": "^2.1.2",
|
||||||
"atob": "^2.1.1",
|
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"cookie": "^0.3.1",
|
"cookie": "^0.3.1",
|
||||||
"cors": "^2.8.4",
|
"cors": "^2.8.4",
|
||||||
"express": "^4.16.3",
|
|
||||||
"express-graphql": "^0.6.12",
|
|
||||||
"google-auth-library": "^1.5.0",
|
"google-auth-library": "^1.5.0",
|
||||||
"googleapis": "^31.0.2",
|
"googleapis": "^31.0.2",
|
||||||
"graphql": "^14.0.2",
|
"graphql": "^14.0.2",
|
||||||
|
@ -29,10 +27,9 @@
|
||||||
"joi-phone-number": "^2.0.12",
|
"joi-phone-number": "^2.0.12",
|
||||||
"joi-timezone": "^2.0.0",
|
"joi-timezone": "^2.0.0",
|
||||||
"jsonwebtoken": "^8.2.1",
|
"jsonwebtoken": "^8.2.1",
|
||||||
"mailgun-js": "^0.16.0",
|
"mailgun-js": "^0.22.0",
|
||||||
"mongodb": "^3.1.6",
|
"node-pre-gyp": "^0.12.0",
|
||||||
"node-fetch": "^2.1.2",
|
"pg": "^7.8.2",
|
||||||
"node-pre-gyp": "^0.9.0",
|
|
||||||
"random-string": "^0.2.0",
|
"random-string": "^0.2.0",
|
||||||
"randomstring": "^1.1.5",
|
"randomstring": "^1.1.5",
|
||||||
"superagent": "^3.8.3",
|
"superagent": "^3.8.3",
|
||||||
|
@ -41,43 +38,36 @@
|
||||||
"webpack-node-externals": "^1.7.2"
|
"webpack-node-externals": "^1.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/es6-promise": "^3.3.0",
|
|
||||||
"@types/express": "^4.11.1",
|
|
||||||
"@types/gapi.client.calendar": "^3.0.0",
|
"@types/gapi.client.calendar": "^3.0.0",
|
||||||
"@types/google-cloud__datastore": "^1.3.2",
|
|
||||||
"@types/jest": "^24.0.9",
|
"@types/jest": "^24.0.9",
|
||||||
"@types/joi": "^13.0.8",
|
"@types/joi": "^13.0.8",
|
||||||
"@types/node": "^9.6.1",
|
"@types/node": "^11.11.0",
|
||||||
"@types/node-fetch": "^2.1.1",
|
|
||||||
"@types/uuid": "^3.4.4",
|
"@types/uuid": "^3.4.4",
|
||||||
"gql2ts": "^1.10.1",
|
"gql2ts": "^1.10.1",
|
||||||
|
"jest": "^23.6.0",
|
||||||
|
"nodemon": "^1.18.10",
|
||||||
"ts-jest": "23.10.3",
|
"ts-jest": "23.10.3",
|
||||||
"ts-node": "^7.0.1",
|
"ts-node": "^8.0.3",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"typescript": "^3.2.4"
|
"typescript": "^3.3.3333"
|
||||||
},
|
},
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"proxy": "https://staging.quepasaalpujarra.com",
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"globalSetup": "../jest-setup.js",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.tsx?$": "ts-jest"
|
||||||
|
},
|
||||||
|
"testRegex": "(.*\\.(test|spec))\\.(jsx?|tsx?)$",
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"ts",
|
"ts",
|
||||||
"tsx",
|
"tsx",
|
||||||
"js",
|
"js",
|
||||||
"jsx",
|
"jsx",
|
||||||
"json"
|
"json",
|
||||||
],
|
"node"
|
||||||
"transform": {
|
]
|
||||||
"^.+\\.tsx?$": "ts-jest"
|
}
|
||||||
},
|
|
||||||
"modulePathIgnorePatterns": [
|
|
||||||
".png",
|
|
||||||
".scss"
|
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"testRegex": "/__tests__/.*spec.tsx?$"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"proxy": "https://staging.quepasaalpujarra.com"
|
|
||||||
}
|
}
|
||||||
|
|
8
server/src/@types/index.d.ts
vendored
8
server/src/@types/index.d.ts
vendored
|
@ -1,7 +1,7 @@
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
// graphql typescript definitions
|
// graphql typescript definitions
|
||||||
|
|
||||||
declare namespace GQL {
|
export namespace GQL {
|
||||||
interface IGraphQLResponseRoot {
|
interface IGraphQLResponseRoot {
|
||||||
data?: IQuery | IMutation;
|
data?: IQuery | IMutation;
|
||||||
errors?: Array<IGraphQLResponseError>;
|
errors?: Array<IGraphQLResponseError>;
|
||||||
|
@ -32,8 +32,7 @@ declare namespace GQL {
|
||||||
|
|
||||||
interface IUser {
|
interface IUser {
|
||||||
__typename: 'User';
|
__typename: 'User';
|
||||||
firstName: string;
|
name: string;
|
||||||
lastName: string | null;
|
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -124,8 +123,7 @@ declare namespace GQL {
|
||||||
interface ISignupInput {
|
interface ISignupInput {
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
firstName: string;
|
name: string;
|
||||||
lastName?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISigninInput {
|
interface ISigninInput {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import * as uuid4 from 'uuid/v4'
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Session extends BaseEntity {
|
export class Session extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: string
|
||||||
@ManyToOne(type => User, (user: User) => user.sessionInvites)
|
@ManyToOne(type => User, (user: User) => user.sessionInvites)
|
||||||
user: User
|
user: User
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ export class Session extends BaseEntity {
|
||||||
hash: string
|
hash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class SessionInvite extends BaseEntity {
|
export class SessionInvite extends BaseEntity {
|
||||||
|
|
||||||
|
@ -30,4 +33,3 @@ export class SessionInvite extends BaseEntity {
|
||||||
timeValidated?: Date
|
timeValidated?: Date
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import {User} from "./User.entity"
|
import {User} from "./User.entity"
|
||||||
import * as uuid from 'uuid/v4'
|
import * as uuid from 'uuid/v4'
|
||||||
const randomstring = require('random-string')
|
const randomstring = require('random-string')
|
||||||
import {sendEmail} from '../post_office'
|
import {PostOffice} from '../post_office'
|
||||||
import {domain} from '../config'
|
import {domain} from '../config'
|
||||||
import {Session, SessionInvite} from "./Session.entity"
|
import {Session, SessionInvite} from "./Session.entity"
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@ const generateHash = () => randomstring({
|
||||||
special: false
|
special: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const generateUniqueInviteHash = () => {
|
const generateUniqueInviteHash = async () => {
|
||||||
const hash = generateHash()
|
const hash = generateHash()
|
||||||
const existingSession = SessionInvite.findOne({hash: hash})
|
const existingSession = await SessionInvite.findOne({hash: hash})
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
return generateUniqueInviteHash()
|
return generateUniqueInviteHash()
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,8 +34,16 @@ const generateUniqueSessionHash = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
sendEmail: PostOffice
|
||||||
|
}
|
||||||
|
|
||||||
export default class SessionManager {
|
export default class SessionManager {
|
||||||
|
sendEmail: PostOffice
|
||||||
|
|
||||||
|
constructor(deps: Dependencies) {
|
||||||
|
this.sendEmail = deps.sendEmail
|
||||||
|
}
|
||||||
inviteUser = async (user: User): Promise<SessionInvite> => {
|
inviteUser = async (user: User): Promise<SessionInvite> => {
|
||||||
const invite = new SessionInvite()
|
const invite = new SessionInvite()
|
||||||
invite.user = user
|
invite.user = user
|
||||||
|
@ -44,7 +52,7 @@ export default class SessionManager {
|
||||||
|
|
||||||
return new Promise(async (resolve: (SessionInvite) => void, reject)=>{
|
return new Promise(async (resolve: (SessionInvite) => void, reject)=>{
|
||||||
try {
|
try {
|
||||||
await sendEmail({
|
await this.sendEmail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: `signin@${domain}`,
|
from: `signin@${domain}`,
|
||||||
text: `Follow this link to start a session: https://${domain}/login/${invite.hash}`,
|
text: `Follow this link to start a session: https://${domain}/login/${invite.hash}`,
|
||||||
|
|
|
@ -8,16 +8,13 @@ import {
|
||||||
import { Event } from "../Calendar/Event.entity"
|
import { Event } from "../Calendar/Event.entity"
|
||||||
import {SessionInvite} from "./Session.entity"
|
import {SessionInvite} from "./Session.entity"
|
||||||
|
|
||||||
@Entity()
|
@Entity("app_user")
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
firstName: string
|
name: string
|
||||||
|
|
||||||
@Column()
|
|
||||||
lastName: string
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
username: string
|
username: string
|
||||||
|
@ -25,9 +22,6 @@ export class User extends BaseEntity {
|
||||||
@Column()
|
@Column()
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
@Column()
|
|
||||||
age: number
|
|
||||||
|
|
||||||
@OneToMany(type => Event, event => event.owner)
|
@OneToMany(type => Event, event => event.owner)
|
||||||
events: Event[]
|
events: Event[]
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
import SessionManager from "./SessionManager"
|
import SessionManager from "./SessionManager"
|
||||||
import {User} from "./User.entity"
|
import {User} from "./User.entity"
|
||||||
|
import {GQL} from "../@types"
|
||||||
|
import {PostOffice} from "../post_office";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
sendEmail: PostOffice
|
||||||
|
}
|
||||||
export default class AuthResolvers {
|
export default class AuthResolvers {
|
||||||
sessionManager: SessionManager
|
sessionManager: SessionManager
|
||||||
|
sendEmail: PostOffice
|
||||||
|
|
||||||
constructor({sessionManager}) {
|
constructor(deps: Dependencies) {
|
||||||
this.sessionManager = sessionManager
|
this.sessionManager = new SessionManager({
|
||||||
|
sendEmail: deps.sendEmail
|
||||||
|
})
|
||||||
|
this.sendEmail = deps.sendEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
Query = {
|
Query = {
|
||||||
|
@ -16,7 +24,9 @@ export default class AuthResolvers {
|
||||||
Mutation = {
|
Mutation = {
|
||||||
signup: async (_, args: GQL.ISignupOnMutationArguments, context, info) => {
|
signup: async (_, args: GQL.ISignupOnMutationArguments, context, info) => {
|
||||||
const errors = []
|
const errors = []
|
||||||
if (await User.findOne({email: args.input.email})) {
|
const userExists = await User.findOne({email: args.input.email})
|
||||||
|
console.log('userExists', userExists)
|
||||||
|
if (userExists) {
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "email",
|
path: "email",
|
||||||
message: "Email taken"
|
message: "Email taken"
|
||||||
|
@ -34,8 +44,7 @@ export default class AuthResolvers {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = new User()
|
const newUser = new User()
|
||||||
newUser.firstName = args.input.firstName
|
newUser.name = args.input.name
|
||||||
newUser.lastName = args.input.lastName
|
|
||||||
newUser.email = args.input.email
|
newUser.email = args.input.email
|
||||||
newUser.username = args.input.username
|
newUser.username = args.input.username
|
||||||
await newUser.save()
|
await newUser.save()
|
||||||
|
@ -47,9 +56,9 @@ export default class AuthResolvers {
|
||||||
try {
|
try {
|
||||||
await this.sessionManager.inviteUser(newUser)
|
await this.sessionManager.inviteUser(newUser)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error sending invitation', e)
|
return false
|
||||||
}
|
}
|
||||||
return null
|
return true
|
||||||
},
|
},
|
||||||
signin: async (_, req, context, info) => {
|
signin: async (_, req, context, info) => {
|
||||||
const session = await this.sessionManager.initiateSession(req.input.hash)
|
const session = await this.sessionManager.initiateSession(req.input.hash)
|
||||||
|
|
|
@ -23,7 +23,7 @@ class EventContactPerson extends BaseEntity {
|
||||||
export class EventTime extends BaseEntity {
|
export class EventTime extends BaseEntity {
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
timeZone: string
|
timeZone: string
|
||||||
@Column({type: "datetime"})
|
@Column({type: "time with time zone"})
|
||||||
start: Date
|
start: Date
|
||||||
@Column()
|
@Column()
|
||||||
end: Date
|
end: Date
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {Event} from "../Calendar/Event.entity"
|
import {Event} from "../Calendar/Event.entity"
|
||||||
import {ResolverMap} from "../@types/graphql-utils"
|
import {ResolverMap} from "../@types/graphql-utils"
|
||||||
|
import {GQL} from "../@types"
|
||||||
|
|
||||||
const resolvers: ResolverMap = {
|
const resolvers: ResolverMap = {
|
||||||
|
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import {auth} from 'google-auth-library';
|
|
||||||
import { atob } from 'atob';
|
|
||||||
import Calendar from '../calendar'
|
|
||||||
import { gcal as gcalConfig } from '../config'
|
|
||||||
import Repository from "../repository";
|
|
||||||
import testEvent from "./testEvent";
|
|
||||||
|
|
||||||
let calendar
|
|
||||||
describe('cal access', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
calendar = new Calendar({
|
|
||||||
repository: new Repository('testProject'),
|
|
||||||
gcalConfig: gcalConfig,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
xit('atob test', () => {
|
|
||||||
expect(atob('aGVsbG8=')).toEqual('hello')
|
|
||||||
});
|
|
||||||
|
|
||||||
it('read events', async (done) => {
|
|
||||||
const events = await calendar.listEvents()
|
|
||||||
console.log('events', events)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
|
|
||||||
xit('try to access', async (done) => {
|
|
||||||
const cals = await calendar.listCalendars()
|
|
||||||
console.log('cals', cals)
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
|
|
||||||
xit('create no recurrence event', async (done) => {
|
|
||||||
const oneTimeEvent = {
|
|
||||||
...testEvent,
|
|
||||||
timing: {
|
|
||||||
...testEvent.timing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete oneTimeEvent.timing.recurrence;
|
|
||||||
|
|
||||||
const result = await calendar.createEvent({
|
|
||||||
...testEvent,
|
|
||||||
id: 1234,
|
|
||||||
})
|
|
||||||
console.log('result', result)
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
|
|
||||||
xit('create recurrence event', async (done) => {
|
|
||||||
const result = await calendar.createEvent({
|
|
||||||
...testEvent,
|
|
||||||
id: 1234,
|
|
||||||
})
|
|
||||||
console.log('result', result)
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,10 +0,0 @@
|
||||||
import {UserEventSchema} from "../event";
|
|
||||||
|
|
||||||
describe('event', () => {
|
|
||||||
it('basic validation', () => {
|
|
||||||
const yesError = UserEventSchema.validate({
|
|
||||||
timeZone: 'what'
|
|
||||||
}).error
|
|
||||||
expect(yesError).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { SessionInvite } from '../session'
|
|
||||||
|
|
||||||
it('test one', () => {
|
|
||||||
const si = new SessionInvite({id: 'testId'})
|
|
||||||
expect(si.hash).toHaveLength(48)
|
|
||||||
})
|
|
|
@ -5,54 +5,44 @@ import {makeExecutableSchema} from "graphql-tools"
|
||||||
import EventsResolvers from './Events/eventsResolvers'
|
import EventsResolvers from './Events/eventsResolvers'
|
||||||
import {importSchema} from 'graphql-import'
|
import {importSchema} from 'graphql-import'
|
||||||
import AuthResolvers from "./Auth/authResolvers"
|
import AuthResolvers from "./Auth/authResolvers"
|
||||||
|
import {Connection} from "typeorm"
|
||||||
|
import {PostOffice, sendEmail} from "./post_office";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
sessionManager: SessionManager,
|
typeormConnection: Connection
|
||||||
|
sendEmail: PostOffice
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GraphQLInterface {
|
const resolvers = {
|
||||||
sessionManager: SessionManager
|
Query: {},
|
||||||
|
Mutation: {}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(dependencies: Dependencies) {
|
export const createServer = async (dependencies: Dependencies) => {
|
||||||
this.sessionManager = dependencies.sessionManager
|
|
||||||
}
|
|
||||||
|
|
||||||
start = () => {
|
const authResolvers = new AuthResolvers({
|
||||||
|
sendEmail: dependencies.sendEmail
|
||||||
|
})
|
||||||
|
|
||||||
const authResolvers = new AuthResolvers({
|
const typeDefs = importSchema(__dirname + '/schema.graphql')
|
||||||
sessionManager: this.sessionManager,
|
|
||||||
})
|
|
||||||
|
|
||||||
const typeDefs = importSchema(__dirname + '/schema.graphql')
|
const schema = makeExecutableSchema({
|
||||||
|
typeDefs: [
|
||||||
const schema = makeExecutableSchema({
|
typeDefs
|
||||||
typeDefs: [
|
],
|
||||||
typeDefs
|
resolvers: {
|
||||||
],
|
Query: {
|
||||||
resolvers: {
|
...resolvers.Query,
|
||||||
Query: {
|
...EventsResolvers.Query,
|
||||||
...this.resolvers.Query,
|
...authResolvers.Query
|
||||||
...EventsResolvers.Query,
|
|
||||||
...authResolvers.Query
|
|
||||||
},
|
|
||||||
Mutation: {
|
|
||||||
...this.resolvers.Mutation,
|
|
||||||
...EventsResolvers.Mutation,
|
|
||||||
...authResolvers.Mutation
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
Mutation: {
|
||||||
|
...resolvers.Mutation,
|
||||||
|
...EventsResolvers.Mutation,
|
||||||
|
...authResolvers.Mutation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const server = new ApolloServer({schema})
|
return new ApolloServer({schema})
|
||||||
server.listen().then(({url}) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvers = {
|
|
||||||
Query: {},
|
|
||||||
Mutation: {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import GraphQLInterface from "./graphql"
|
import { createServer } from "./graphql"
|
||||||
import SessionManager from "./Auth/SessionManager"
|
import typeormConfig from '../ormconfig'
|
||||||
|
import {createConnection} from "typeorm"
|
||||||
|
import { sendEmail} from "./post_office"
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
const gql = new GraphQLInterface({
|
const server = await createServer({
|
||||||
sessionManager: new SessionManager()
|
typeormConnection: await createConnection(typeormConfig),
|
||||||
|
sendEmail,
|
||||||
})
|
})
|
||||||
gql.start()
|
server.listen().then(({url}) => {
|
||||||
|
console.log(`🚀 Server ready at ${url}`)
|
||||||
|
})
|
||||||
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
start()
|
start()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { mailgun as mailgunConfig } from './config';
|
import { mailgun as mailgunConfig } from './config'
|
||||||
const Mailgun = require('mailgun-js')
|
const Mailgun = require('mailgun-js')
|
||||||
|
|
||||||
interface Email {
|
interface Email {
|
||||||
|
@ -8,22 +8,24 @@ interface Email {
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendEmail = async (email: Email) => {
|
export type PostOffice = (email: Email) => Promise<boolean>
|
||||||
|
|
||||||
|
export const sendEmail: PostOffice = async (email: Email) => {
|
||||||
console.log('Will try to send following email', JSON.stringify(email))
|
console.log('Will try to send following email', JSON.stringify(email))
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const client = Mailgun(mailgunConfig);
|
const client = Mailgun(mailgunConfig)
|
||||||
client.messages().send(email, function (error, body) {
|
client.messages().send(email, function (error, body) {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(body)
|
resolve(body)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to send mail', e)
|
console.error('Failed to send mail', e)
|
||||||
reject(e)
|
reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
type User {
|
type User {
|
||||||
firstName: String!
|
name: String!
|
||||||
lastName: String
|
|
||||||
username: String!
|
username: String!
|
||||||
email: String!
|
email: String!
|
||||||
id: ID!
|
id: ID!
|
||||||
|
@ -18,8 +17,7 @@ type UserSession {
|
||||||
input SignupInput {
|
input SignupInput {
|
||||||
email: String!
|
email: String!
|
||||||
username: String!
|
username: String!
|
||||||
firstName: String!
|
name: String!
|
||||||
lastName: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input SigninInput {
|
input SigninInput {
|
||||||
|
@ -147,9 +145,13 @@ input CreateEventInput {
|
||||||
contact: [EventContactPersonInput!]!
|
contact: [EventContactPersonInput!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Error {
|
||||||
|
path: String!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
type Mutation {
|
type Mutation {
|
||||||
# Auth
|
# Auth
|
||||||
signup(input: SignupInput): Boolean!
|
signup(input: SignupInput): Boolean
|
||||||
signin(input: SigninInput): UserSession!
|
signin(input: SigninInput): UserSession!
|
||||||
requestInvite(input: RequestInviteInput): Boolean!
|
requestInvite(input: RequestInviteInput): Boolean!
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,15 @@
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"types": ["node", "jest"]
|
||||||
},
|
},
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"./src/@types/index.d.ts"
|
"src/@types/index.d.ts", "../node_modules/@types/node/ts3.2/index.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/*.spec.ts", "node_modules", "__tests__"
|
"**/*.spec.ts", "node_modules", "__tests__"
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"development": {
|
|
||||||
"plugins": ["react-hot-loader/babel", "babel-plugin-styled-components"]
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"plugins": ["transform-es2015-modules-commonjs"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
21
webapp/.gitignore
vendored
21
webapp/.gitignore
vendored
|
@ -1,21 +0,0 @@
|
||||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
2448
webapp/README.md
2448
webapp/README.md
File diff suppressed because it is too large
Load diff
3
webapp/images.d.ts
vendored
3
webapp/images.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
declare module '*.svg'
|
|
||||||
declare module '*.png'
|
|
||||||
declare module '*.jpg'
|
|
|
@ -1,50 +0,0 @@
|
||||||
{
|
|
||||||
"name": "webapp",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@material-ui/core": "^3.1.2",
|
|
||||||
"axios": "^0.18.0",
|
|
||||||
"cc-components": "1.0.0",
|
|
||||||
"date-fns": "^1.29.0",
|
|
||||||
"formik": "^1.3.0",
|
|
||||||
"react": "^16.5.2",
|
|
||||||
"react-dom": "^16.5.2",
|
|
||||||
"react-router-dom": "^4.3.1",
|
|
||||||
"styled-components": "^3.4.5",
|
|
||||||
"webpack": "^4.20.2",
|
|
||||||
"webpack-cli": "^3.1.2",
|
|
||||||
"webpack-dev-server": "^3.1.9"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "webpack-dev-server --hot --progress",
|
|
||||||
"build": "NODE_ENV=production webpack --progress"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.1.2",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
|
||||||
"@babel/preset-env": "^7.1.0",
|
|
||||||
"@babel/preset-react": "^7.0.0",
|
|
||||||
"@babel/preset-typescript": "^7.1.0",
|
|
||||||
"@types/html-webpack-plugin": "^3.2.0",
|
|
||||||
"@types/jest": "^23.3.1",
|
|
||||||
"@types/joi": "^13.6.0",
|
|
||||||
"@types/node": "^10.7.1",
|
|
||||||
"@types/react": "^16.0.8",
|
|
||||||
"@types/react-dom": "^16.0.7",
|
|
||||||
"@types/react-router-dom": "^4.3.1",
|
|
||||||
"@types/styled-components": "^3.0.1",
|
|
||||||
"@types/webpack": "^4.4.14",
|
|
||||||
"@types/webpack-dev-server": "^3.1.1",
|
|
||||||
"babel-loader": "^8.0.4",
|
|
||||||
"babel-plugin-react-hot-loader": "^3.0.0-beta.6",
|
|
||||||
"babel-plugin-styled-components": "^1.8.0",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
|
||||||
"react-hot-loader": "^4.3.11",
|
|
||||||
"ts-node": "^7.0.1",
|
|
||||||
"tslint-react": "^3.6.0",
|
|
||||||
"typescript": "^3.1.1"
|
|
||||||
},
|
|
||||||
"proxy": "https://staging.quepasaalpujarra.com"
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import * as ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
ReactDOM.render(<App />, div);
|
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
|
||||||
});
|
|
|
@ -1,25 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';
|
|
||||||
import CreateEvent from './CreateEvent';
|
|
||||||
import Events from './events/Events'
|
|
||||||
import InitiateSession from './InitiateSession'
|
|
||||||
import RequestMagicLink from './RequestMagicLink'
|
|
||||||
import Root from './Root'
|
|
||||||
|
|
||||||
class App extends React.Component {
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<Router>
|
|
||||||
<Switch>
|
|
||||||
<Route path="/login/:hash" component={InitiateSession}/>
|
|
||||||
<Route path="/login" component={RequestMagicLink}/>
|
|
||||||
<Route path="/events/create" component={CreateEvent}/>
|
|
||||||
<Route path="/events" component={Events}/>
|
|
||||||
<Route path="/" component={Root}/>
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,149 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import Input from 'cc-components/Input';
|
|
||||||
import * as addHours from 'date-fns/add_hours';
|
|
||||||
import * as fnsFormat from 'date-fns/format';
|
|
||||||
import * as startOfTomorrow from 'date-fns/start_of_tomorrow';
|
|
||||||
import {Field, FieldProps, Form, Formik, FormikProps} from 'formik';
|
|
||||||
import * as React from 'react';
|
|
||||||
import {ChangeEvent} from "react";
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import {CalendarEventRequest} from "../../../functions/src/types";
|
|
||||||
|
|
||||||
const nextWeekNoon = addHours(startOfTomorrow(), 24 * 7 + 12)
|
|
||||||
const format = (date: Date) => fnsFormat(date, 'YYYY-MM-DD')
|
|
||||||
const InitialValues: CalendarEventRequest = {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
tags: [],
|
|
||||||
timing: {
|
|
||||||
end: {
|
|
||||||
date: format(addHours(nextWeekNoon, 2)),
|
|
||||||
dateTime: '',
|
|
||||||
},
|
|
||||||
start: {
|
|
||||||
date: format(nextWeekNoon),
|
|
||||||
dateTime: '',
|
|
||||||
},
|
|
||||||
recurrence: [],
|
|
||||||
status: 'tentative',
|
|
||||||
},
|
|
||||||
timeZone: '',
|
|
||||||
contactPhone: '',
|
|
||||||
contactEmail: '',
|
|
||||||
locationAddress: '',
|
|
||||||
location: '',
|
|
||||||
locationCoordinate: [0, 0],
|
|
||||||
imageUrl: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
wholeDayEvent: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CreateEvent extends React.Component<Props, State> {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
wholeDayEvent: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
axios.get('/api/events').then(events => console.log('boludo', events))
|
|
||||||
}
|
|
||||||
|
|
||||||
submitEvent(event: CalendarEventRequest) {
|
|
||||||
event.timeZone = 'Europe/Madrid'
|
|
||||||
axios.post('/api/events', event, {
|
|
||||||
headers: {
|
|
||||||
Accept: '*/*'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWholeDayEventChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({wholeDayEvent: e.target.checked})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Root>
|
|
||||||
<Title>Post your own event</Title>
|
|
||||||
<Formik onSubmit={this.submitEvent} initialValues={InitialValues}>
|
|
||||||
{
|
|
||||||
({values}: FormikProps<CalendarEventRequest>) => (<Form>
|
|
||||||
<label>Title
|
|
||||||
<Field name="title">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
<label>Location
|
|
||||||
<Field name="location">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
<label>Contact phone number
|
|
||||||
<Field name="contactPhone">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input type="phone" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
<label>Contact email
|
|
||||||
<Field name="contactEmail">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input type="email" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Start date
|
|
||||||
<Field name="timing.start.date">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input type="date" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
This is a whole day event
|
|
||||||
<Input type="checkbox" checked={this.state.wholeDayEvent} onChange={this.handleWholeDayEventChange}/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Start date
|
|
||||||
<Field name="timing.start.date">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input type="date" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Start time
|
|
||||||
<Field name="timing.start.date">
|
|
||||||
{
|
|
||||||
({field}: FieldProps) => <Input disabled={this.state.wholeDayEvent} type="time" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
</label>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</Form>)
|
|
||||||
}
|
|
||||||
</Formik>
|
|
||||||
</Root>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Root = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`
|
|
||||||
const Title = styled.div`
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
`
|
|
|
@ -1 +0,0 @@
|
||||||
export {default as default} from './CreateEvent'
|
|
|
@ -1,64 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import * as React from 'react';
|
|
||||||
import {Link, match} from "react-router-dom";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
match: match<{hash: string, email: string}>
|
|
||||||
}
|
|
||||||
type LoginStatus = 'success' | 'error' | 'failure' | 'loading'
|
|
||||||
interface State {
|
|
||||||
loginStatus: LoginStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
class InitiateSession extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {loginStatus: 'loading'}
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const { hash } = this.props.match.params
|
|
||||||
let loginStatus: LoginStatus = 'loading';
|
|
||||||
this.setState({loginStatus})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/signin', {
|
|
||||||
hash,
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
loginStatus = 'success'
|
|
||||||
} else if (response.status === 403) {
|
|
||||||
loginStatus = 'failure'
|
|
||||||
} else if (response.status === 401) {
|
|
||||||
loginStatus = 'failure';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
const err = e
|
|
||||||
console.log('caught error', err)
|
|
||||||
loginStatus = 'error';
|
|
||||||
}
|
|
||||||
if (loginStatus === undefined) {
|
|
||||||
throw new Error('Could not determine login statue')
|
|
||||||
}
|
|
||||||
this.setState({loginStatus})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div>
|
|
||||||
<h1>Thanks for coming back, we will log you in now.</h1>
|
|
||||||
{
|
|
||||||
this.state.loginStatus === 'loading' && <div>Please wait ...</div>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.loginStatus === 'success' && <div>You are now logged in. <Link to="/events/create">Create an event</Link></div>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.loginStatus === 'failure' && <div>Could not log you in</div>
|
|
||||||
}
|
|
||||||
<h1>Thanks for coming back, we will log you in now.</h1>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default InitiateSession
|
|
|
@ -1,95 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
import {Field, FieldProps, Form, Formik, FormikErrors, FormikProps} from 'formik';
|
|
||||||
import * as React from 'react';
|
|
||||||
import {RouteComponentProps, withRouter} from "react-router";
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const submitRequestToken = (loginRequest: SessionRequest) => {
|
|
||||||
axios.post('/api/session', loginRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionRequest {
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps {
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestMagicLink extends React.Component<Props, {}> {
|
|
||||||
|
|
||||||
handleValidate(values: SessionRequest): FormikErrors<SessionRequest> {
|
|
||||||
const errors: FormikErrors<SessionRequest> = {};
|
|
||||||
// get a proper validator for this
|
|
||||||
if (!(values && values.email && values.email.includes('@'))){
|
|
||||||
errors.email = 'Must include valid email address'
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit = async (values: SessionRequest) => {
|
|
||||||
this.setState({
|
|
||||||
loading: true,
|
|
||||||
})
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await axios.post('/api/signin', values)
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({
|
|
||||||
error: e
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} finally {
|
|
||||||
this.setState({
|
|
||||||
loading: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
this.setState({
|
|
||||||
error: null,
|
|
||||||
invitationSent: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Login</h1>
|
|
||||||
<MessageContainer>
|
|
||||||
In order to just view the calendar you don't need to log in, you can simply go back to the calendar and browse
|
|
||||||
the events. You only need a login if you have an event you would like to publish or manage. If you already have
|
|
||||||
registered, simply enter your email below and you will get a magic link per email. Following the magic link will
|
|
||||||
log you into this page. No password necessary.
|
|
||||||
</MessageContainer>
|
|
||||||
<Formik onSubmit={submitRequestToken} initialValues={{email: ''}} validateOnBlur={true} validate={this.handleValidate}>
|
|
||||||
{
|
|
||||||
(formikProps: FormikProps<SessionRequest>) => (
|
|
||||||
<Form>
|
|
||||||
<Field name="email">
|
|
||||||
{
|
|
||||||
({ field }: FieldProps<string>) => <input type="email" {...field} />
|
|
||||||
}
|
|
||||||
</Field>
|
|
||||||
<Button disabled={!formikProps.isValid} type="submit">Request magic link</Button>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageContainer = styled.div`
|
|
||||||
max-width: 600px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
`
|
|
||||||
const Button = styled.button`
|
|
||||||
&[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default withRouter(RequestMagicLink)
|
|
|
@ -1,5 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import {hot} from 'react-hot-loader';
|
|
||||||
|
|
||||||
const Root = () => <h1>root</h1>
|
|
||||||
export default hot(module)(Root)
|
|
|
@ -1,7 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
export default class Signup extends React.Component {
|
|
||||||
render() {
|
|
||||||
return <div>signup</div>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import {CalendarEvent} from "../../../functions/src/types";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
event: CalendarEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class EventItem extends React.Component<IProps> {
|
|
||||||
public render(){
|
|
||||||
return <div>
|
|
||||||
event item
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import * as React from 'react';
|
|
||||||
import {CalendarEvent} from "../../../functions/src/types";
|
|
||||||
import EventItem from './EventItem'
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
events: CalendarEvent[],
|
|
||||||
loadingState: 'loading' | 'error' | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Events extends React.Component<IProps, IState> {
|
|
||||||
public state = {
|
|
||||||
events: [],
|
|
||||||
loadingState: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
public async componentDidMount() {
|
|
||||||
try {
|
|
||||||
this.setState({
|
|
||||||
loadingState: 'loading'
|
|
||||||
})
|
|
||||||
const eventsResponse = await axios.get('/api/events');
|
|
||||||
this.setState({
|
|
||||||
events: (eventsResponse.data || []) as CalendarEvent[],
|
|
||||||
loadingState: null,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({
|
|
||||||
loadingState: 'error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return <div className={this.props.className}>
|
|
||||||
{
|
|
||||||
this.state.loadingState === 'loading' && <h4>loading ...</h4>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.loadingState === 'error' && <h4>Error occured while loading</h4>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.events && this.state.events.map((event: CalendarEvent) => <EventItem key={event.id} event={event}/>)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Events from './Events'
|
|
||||||
|
|
||||||
export { Events }
|
|
|
@ -1,10 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import * as ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
const appDiv = document.createElement('div')
|
|
||||||
document.body.appendChild(appDiv)
|
|
||||||
ReactDOM.render(
|
|
||||||
<App />,
|
|
||||||
appDiv as HTMLElement
|
|
||||||
);
|
|
|
@ -1,7 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
|
||||||
<g fill="#61DAFB">
|
|
||||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
|
||||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
|
||||||
<path d="M520.5 78.1z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,123 +0,0 @@
|
||||||
// tslint:disable:no-console
|
|
||||||
// In production, we register a service worker to serve assets from local cache.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on the 'N+1' visit to a page, since previously
|
|
||||||
// cached resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
|
||||||
// This link also includes instructions on opting out of this behavior.
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(
|
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function register() {
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(
|
|
||||||
process.env.PUBLIC_URL!,
|
|
||||||
window.location.toString()
|
|
||||||
);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl);
|
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Is not local host. Just register service worker
|
|
||||||
registerValidSW(swUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl: string) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then(registration => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker) {
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the old content will have been purged and
|
|
||||||
// the fresh content will have been added to the cache.
|
|
||||||
// It's the perfect time to display a 'New content is
|
|
||||||
// available; please refresh.' message in your web app.
|
|
||||||
console.log('New content is available; please refresh.');
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// 'Content is cached for offline use.' message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl)
|
|
||||||
.then(response => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
response.headers.get('content-type')!.indexOf('javascript') === -1
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "build/dist",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es6", "dom"],
|
|
||||||
"sourceMap": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"jsx": "react",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"rootDir": "src",
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
|
||||||
"noUnusedLocals": true
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"build"
|
|
||||||
],
|
|
||||||
"indlude": [
|
|
||||||
"node_modules/@types",
|
|
||||||
"./src/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
import * as HTMLWebpackPlugin from 'html-webpack-plugin'
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as webpack from 'webpack';
|
|
||||||
import * as wds from 'webpack-dev-server';
|
|
||||||
|
|
||||||
const Config: webpack.Configuration & {devServer: wds.Configuration} = {
|
|
||||||
entry: './src/index.tsx',
|
|
||||||
output: {
|
|
||||||
filename: "[name].bundle.[hash].js",
|
|
||||||
path: path.resolve(__dirname, "dist"),
|
|
||||||
publicPath: "/"
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: [".js", ".jsx", ".ts", ".tsx"]
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
exclude: path.resolve(__dirname, "node_modules"),
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: {
|
|
||||||
loader: "babel-loader",
|
|
||||||
options: {
|
|
||||||
presets: [
|
|
||||||
["@babel/preset-env", {
|
|
||||||
"exclude": ["transform-regenerator"]
|
|
||||||
}],
|
|
||||||
"@babel/typescript",
|
|
||||||
"@babel/react"
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/plugin-syntax-dynamic-import"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HTMLWebpackPlugin(),
|
|
||||||
],
|
|
||||||
devtool: 'cheap-module-source-map',
|
|
||||||
devServer: {
|
|
||||||
historyApiFallback: true,
|
|
||||||
https: true,
|
|
||||||
proxy: {
|
|
||||||
'/api': 'https://staging.quepasaalpujarra.com'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Config
|
|
9129
webapp/yarn.lock
9129
webapp/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue