Set up testing environment

This commit is contained in:
Amit Jakubowicz 2019-03-11 13:39:16 +01:00
parent 5dbc1888f8
commit 3e5f83a6fc
42 changed files with 2638 additions and 16227 deletions

12
jest-setup.js Normal file
View 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;
};

View File

@ -6,6 +6,9 @@
"repository": "git@gitlab.com:amiiit/gcf-auth.git",
"author": "Amit Jakubowicz <a.jakubowicz@travelaudience.com>",
"license": "MIT",
"resolutions": {
"@types/node": "11.11.0"
},
"workspaces": [
"packages/*",
"webapp",

View File

@ -1,16 +1,20 @@
import { ConnectionOptions } from 'typeorm'
import {ConnectionOptions} from 'typeorm'
import { User } from './src/Auth/User.entity'
const config: ConnectionOptions = {
type: 'postgres',
host: process.env.POSTGRES_HOST || 'localhost',
port: Number(process.env.POSTGRES_PORT || 5432),
username: process.env.POSTGRES_USER || 'admin',
password: process.env.POSTGRES_PASSWORD || 'admin',
database: process.env.POSTGRES_DB || 'gpa',
entities: [
__dirname + '/src/**/*.entity{.ts,.js}',
],
database: process.env.POSTGRES_DB || 'qpa',
entities: ["src/**/*.entity.ts"],
synchronize: true,
logging: "all"
}
export const testConfig: ConnectionOptions = {
...config,
database: 'qpa-test',
dropSchema: true
}
export default config

View File

@ -4,20 +4,18 @@
"codegen": "gql2ts ./src/schema.graphql -o ./src/@types/index.d.ts",
"lint": "tslint --project tsconfig.json",
"build": "tsc --version && tsc",
"start": "ts-node src/index.ts"
"start": "nodemon --exec ts-node src/index.ts",
"test": "jest"
},
"main": "lib/index.js",
"dependencies": {
"@types/express-graphql": "^0.6.1",
"@types/graphql": "^14.0.1",
"@types/mongodb": "^3.1.10",
"apollo-server": "^2.1.0",
"atob": "^2.1.1",
"@types/graphql": "^14.0.7",
"apollo-server": "^2.4.8",
"apollo-server-testing": "^2.4.8",
"atob": "^2.1.2",
"axios": "^0.18.0",
"cookie": "^0.3.1",
"cors": "^2.8.4",
"express": "^4.16.3",
"express-graphql": "^0.6.12",
"google-auth-library": "^1.5.0",
"googleapis": "^31.0.2",
"graphql": "^14.0.2",
@ -29,10 +27,9 @@
"joi-phone-number": "^2.0.12",
"joi-timezone": "^2.0.0",
"jsonwebtoken": "^8.2.1",
"mailgun-js": "^0.16.0",
"mongodb": "^3.1.6",
"node-fetch": "^2.1.2",
"node-pre-gyp": "^0.9.0",
"mailgun-js": "^0.22.0",
"node-pre-gyp": "^0.12.0",
"pg": "^7.8.2",
"random-string": "^0.2.0",
"randomstring": "^1.1.5",
"superagent": "^3.8.3",
@ -41,43 +38,36 @@
"webpack-node-externals": "^1.7.2"
},
"devDependencies": {
"@types/es6-promise": "^3.3.0",
"@types/express": "^4.11.1",
"@types/gapi.client.calendar": "^3.0.0",
"@types/google-cloud__datastore": "^1.3.2",
"@types/jest": "^24.0.9",
"@types/joi": "^13.0.8",
"@types/node": "^9.6.1",
"@types/node-fetch": "^2.1.1",
"@types/node": "^11.11.0",
"@types/uuid": "^3.4.4",
"gql2ts": "^1.10.1",
"jest": "^23.6.0",
"nodemon": "^1.18.10",
"ts-jest": "23.10.3",
"ts-node": "^7.0.1",
"ts-node": "^8.0.3",
"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": {
"globalSetup": "../jest-setup.js",
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(.*\\.(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"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"
"json",
"node"
]
}
}

View File

@ -1,7 +1,7 @@
// tslint:disable
// graphql typescript definitions
declare namespace GQL {
export namespace GQL {
interface IGraphQLResponseRoot {
data?: IQuery | IMutation;
errors?: Array<IGraphQLResponseError>;
@ -32,8 +32,7 @@ declare namespace GQL {
interface IUser {
__typename: 'User';
firstName: string;
lastName: string | null;
name: string;
username: string;
email: string;
id: string;
@ -124,8 +123,7 @@ declare namespace GQL {
interface ISignupInput {
email: string;
username: string;
firstName: string;
lastName?: string | null;
name: string;
}
interface ISigninInput {

View File

@ -4,6 +4,8 @@ import * as uuid4 from 'uuid/v4'
@Entity()
export class Session extends BaseEntity {
@PrimaryGeneratedColumn()
id: string
@ManyToOne(type => User, (user: User) => user.sessionInvites)
user: User
@ -14,6 +16,7 @@ export class Session extends BaseEntity {
hash: string
}
@Entity()
export class SessionInvite extends BaseEntity {
@ -30,4 +33,3 @@ export class SessionInvite extends BaseEntity {
timeValidated?: Date
}

View File

@ -3,7 +3,7 @@
import {User} from "./User.entity"
import * as uuid from 'uuid/v4'
const randomstring = require('random-string')
import {sendEmail} from '../post_office'
import {PostOffice} from '../post_office'
import {domain} from '../config'
import {Session, SessionInvite} from "./Session.entity"
@ -15,9 +15,9 @@ const generateHash = () => randomstring({
special: false
})
const generateUniqueInviteHash = () => {
const generateUniqueInviteHash = async () => {
const hash = generateHash()
const existingSession = SessionInvite.findOne({hash: hash})
const existingSession = await SessionInvite.findOne({hash: hash})
if (existingSession) {
return generateUniqueInviteHash()
} else {
@ -34,8 +34,16 @@ const generateUniqueSessionHash = () => {
}
}
interface Dependencies {
sendEmail: PostOffice
}
export default class SessionManager {
sendEmail: PostOffice
constructor(deps: Dependencies) {
this.sendEmail = deps.sendEmail
}
inviteUser = async (user: User): Promise<SessionInvite> => {
const invite = new SessionInvite()
invite.user = user
@ -44,7 +52,7 @@ export default class SessionManager {
return new Promise(async (resolve: (SessionInvite) => void, reject)=>{
try {
await sendEmail({
await this.sendEmail({
to: user.email,
from: `signin@${domain}`,
text: `Follow this link to start a session: https://${domain}/login/${invite.hash}`,

View File

@ -8,16 +8,13 @@ import {
import { Event } from "../Calendar/Event.entity"
import {SessionInvite} from "./Session.entity"
@Entity()
@Entity("app_user")
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: string
@Column()
firstName: string
@Column()
lastName: string
name: string
@Column()
username: string
@ -25,9 +22,6 @@ export class User extends BaseEntity {
@Column()
email: string
@Column()
age: number
@OneToMany(type => Event, event => event.owner)
events: Event[]

View File

@ -1,12 +1,20 @@
import SessionManager from "./SessionManager"
import {User} from "./User.entity"
import {GQL} from "../@types"
import {PostOffice} from "../post_office";
interface Dependencies {
sendEmail: PostOffice
}
export default class AuthResolvers {
sessionManager: SessionManager
sendEmail: PostOffice
constructor({sessionManager}) {
this.sessionManager = sessionManager
constructor(deps: Dependencies) {
this.sessionManager = new SessionManager({
sendEmail: deps.sendEmail
})
this.sendEmail = deps.sendEmail
}
Query = {
@ -16,7 +24,9 @@ export default class AuthResolvers {
Mutation = {
signup: async (_, args: GQL.ISignupOnMutationArguments, context, info) => {
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({
path: "email",
message: "Email taken"
@ -34,8 +44,7 @@ export default class AuthResolvers {
}
const newUser = new User()
newUser.firstName = args.input.firstName
newUser.lastName = args.input.lastName
newUser.name = args.input.name
newUser.email = args.input.email
newUser.username = args.input.username
await newUser.save()
@ -47,9 +56,9 @@ export default class AuthResolvers {
try {
await this.sessionManager.inviteUser(newUser)
} catch (e) {
console.error('Error sending invitation', e)
return false
}
return null
return true
},
signin: async (_, req, context, info) => {
const session = await this.sessionManager.initiateSession(req.input.hash)

View File

@ -23,7 +23,7 @@ class EventContactPerson extends BaseEntity {
export class EventTime extends BaseEntity {
@CreateDateColumn()
timeZone: string
@Column({type: "datetime"})
@Column({type: "time with time zone"})
start: Date
@Column()
end: Date

View File

@ -1,5 +1,6 @@
import {Event} from "../Calendar/Event.entity"
import {ResolverMap} from "../@types/graphql-utils"
import {GQL} from "../@types"
const resolvers: ResolverMap = {

View File

@ -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();
})
})

View File

@ -1,10 +0,0 @@
import {UserEventSchema} from "../event";
describe('event', () => {
it('basic validation', () => {
const yesError = UserEventSchema.validate({
timeZone: 'what'
}).error
expect(yesError).toBeDefined()
})
})

View File

@ -1,6 +0,0 @@
import { SessionInvite } from '../session'
it('test one', () => {
const si = new SessionInvite({id: 'testId'})
expect(si.hash).toHaveLength(48)
})

View File

@ -5,54 +5,44 @@ import {makeExecutableSchema} from "graphql-tools"
import EventsResolvers from './Events/eventsResolvers'
import {importSchema} from 'graphql-import'
import AuthResolvers from "./Auth/authResolvers"
import {Connection} from "typeorm"
import {PostOffice, sendEmail} from "./post_office";
interface Dependencies {
sessionManager: SessionManager,
typeormConnection: Connection
sendEmail: PostOffice
}
export default class GraphQLInterface {
sessionManager: SessionManager
const resolvers = {
Query: {},
Mutation: {}
}
constructor(dependencies: Dependencies) {
this.sessionManager = dependencies.sessionManager
}
export const createServer = async (dependencies: Dependencies) => {
start = () => {
const authResolvers = new AuthResolvers({
sendEmail: dependencies.sendEmail
})
const authResolvers = new AuthResolvers({
sessionManager: this.sessionManager,
})
const typeDefs = importSchema(__dirname + '/schema.graphql')
const typeDefs = importSchema(__dirname + '/schema.graphql')
const schema = makeExecutableSchema({
typeDefs: [
typeDefs
],
resolvers: {
Query: {
...this.resolvers.Query,
...EventsResolvers.Query,
...authResolvers.Query
},
Mutation: {
...this.resolvers.Mutation,
...EventsResolvers.Mutation,
...authResolvers.Mutation
}
const schema = makeExecutableSchema({
typeDefs: [
typeDefs
],
resolvers: {
Query: {
...resolvers.Query,
...EventsResolvers.Query,
...authResolvers.Query
},
})
Mutation: {
...resolvers.Mutation,
...EventsResolvers.Mutation,
...authResolvers.Mutation
}
},
})
const server = new ApolloServer({schema})
server.listen().then(({url}) => {
console.log(`🚀 Server ready at ${url}`)
})
}
resolvers = {
Query: {},
Mutation: {}
}
return new ApolloServer({schema})
}

View File

@ -1,12 +1,17 @@
import GraphQLInterface from "./graphql"
import SessionManager from "./Auth/SessionManager"
import { createServer } from "./graphql"
import typeormConfig from '../ormconfig'
import {createConnection} from "typeorm"
import { sendEmail} from "./post_office"
async function start() {
const gql = new GraphQLInterface({
sessionManager: new SessionManager()
const server = await createServer({
typeormConnection: await createConnection(typeormConfig),
sendEmail,
})
gql.start()
server.listen().then(({url}) => {
console.log(`🚀 Server ready at ${url}`)
})
return server
}
start()

View File

@ -1,4 +1,4 @@
import { mailgun as mailgunConfig } from './config';
import { mailgun as mailgunConfig } from './config'
const Mailgun = require('mailgun-js')
interface Email {
@ -8,22 +8,24 @@ interface Email {
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))
return new Promise((resolve, reject) => {
try {
const client = Mailgun(mailgunConfig);
const client = Mailgun(mailgunConfig)
client.messages().send(email, function (error, body) {
if (error) {
reject(error)
} else {
resolve(body)
}
});
})
} catch (e) {
console.error('Failed to send mail', e)
reject(e)
}
})
}
}

View File

@ -1,6 +1,5 @@
type User {
firstName: String!
lastName: String
name: String!
username: String!
email: String!
id: ID!
@ -18,8 +17,7 @@ type UserSession {
input SignupInput {
email: String!
username: String!
firstName: String!
lastName: String
name: String!
}
input SigninInput {
@ -147,9 +145,13 @@ input CreateEventInput {
contact: [EventContactPersonInput!]!
}
type Error {
path: String!
message: String!
}
type Mutation {
# Auth
signup(input: SignupInput): Boolean!
signup(input: SignupInput): Boolean
signin(input: SigninInput): UserSession!
requestInvite(input: RequestInviteInput): Boolean!

View File

@ -10,14 +10,15 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"types": ["node", "jest"]
},
"compileOnSave": true,
"include": [
"src"
],
"files": [
"./src/@types/index.d.ts"
"src/@types/index.d.ts", "../node_modules/@types/node/ts3.2/index.d.ts"
],
"exclude": [
"**/*.spec.ts", "node_modules", "__tests__"

View File

@ -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
View File

@ -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*

File diff suppressed because it is too large Load Diff

3
webapp/images.d.ts vendored
View File

@ -1,3 +0,0 @@
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'

View File

@ -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"
}

View File

@ -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);
});

View File

@ -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;

View File

@ -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;
`

View File

@ -1 +0,0 @@
export {default as default} from './CreateEvent'

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -1,7 +0,0 @@
import * as React from 'react';
export default class Signup extends React.Component {
render() {
return <div>signup</div>
}
}

View File

@ -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>
}
}

View File

@ -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>
}
}

View File

@ -1,3 +0,0 @@
import Events from './Events'
export { Events }

View File

@ -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
);

View File

@ -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

View File

@ -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();
});
}
}

View File

@ -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/**/*"
]
}

View File

@ -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

File diff suppressed because it is too large Load Diff

6204
yarn.lock

File diff suppressed because it is too large Load Diff