Repackage Team Module

This commit is contained in:
Barry Low committed 2018-09-01 11:04:04 -07:00
commit a1b4bbe18c
203 files changed
+29564

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/.cache
.vscode
docs/notes
+27
View File
@@ -0,0 +1,27 @@
# Advanced Algos Team Module
## Introduction
The general purpose of the team module is
This module explores crafting a strategic development pattern that upholds the Advanced Algos principles of rapid-evolution and automation built upon the values of collaboration, permissionless innovation, and accessibility.
#### This is a monorepo containing the Team GraphQL API server, Team Client, and Team Prisma Schema
## Quick Start
Requirements:
- NodeJS 8+
- NPM (comes installed with NodeJS)
- [Github authorization via SSH](https://help.github.com/articles/connecting-to-github-with-ssh/): *optional* - removes need to authenticate every push or pull from GitHub especially as this is a private repo.
1. Clone this repository `git clone https://github.com/Algoteers/aa-team-module.git`
2. `cd aa-team-module`
5. Navigate to desired package in /packages and look at its README.md for further instructions.
## Getting Started
_This repo is currently under heavy initial-phase development and is not representative of the more mature product. On that note, feel free to look around and keep an eye out as the development unfolds!_
1. Where we set sail — [to the docs!](./docs/README.md)
+162
View File
@@ -0,0 +1,162 @@
# AA Team Module Documentation
## Architecture
Single-Page App (SPA) Modules are NodeJS applications that consist of a client-side application that loads and runs within a users browsers. While a SPA is a fully functional application and doesn't need backend support, it easily meets its limitations when relegated to solely the resources of a web browser.
Single-page apps are able to expand their capabilities by connecting to external datasources or other services, usually through a web API(application programming interface). These APIs allow the web app to pull in curated data from other sources like trade exchanges or movie information, connect to functionality that allows the app to authenticate, create, manage and save information in databases and much more.
### Overview
![AA Team Module Demo Architecture](./assets/AATeamDemoArchiteture.svg)
1. Postgres Database: relational data store
2. Prisma GraphQL service: wrap Postgres as a Relational Database Manager (RDBM)
3. GraphQL API server: Provides backend support with connections to data storage and other datasources
4. NGinx Web Server: Serves the front-end web applications. HTTPS provided by LetsEncrypt.
5. https://api.algocommunity.org: The API endpoint
6. https://aaweb.algocommunity.org: The aaweb demo endpoint
7. https://algocommunity.org: the team module front-end demo endpoint
8. Auth0: Third-party service providing authentication to front-end clients and Graphql API server.
9. User Web Browser: Location where front-end web apps are loaded to
### GraphQL
This module uses a GraphQL API server as its backend support providing it with the ability to query stored data on AA teams and members as well as create and manage that data.
[GraphQL](https://graphql.org/) is a specification and query language for APIs and is implemented by using the [Apollo Server](https://www.apollographql.com/) platform. Behind the GraphQL API server is a Prisma service that connects to a Postgres relational database. The Prisma service acts as a lower-level GraphQL relational-database-manager wrapping the Postgres DB.
A simple comparison between GraphQL and REST can be found at [howtographql.com](https://www.howtographql.com/basics/1-graphql-is-the-better-rest/)
In short, this combination allows
- rapid database development by simply creating a GraphQL schema
- connecting multiple datasource endpoints and wrapping them into a single endpoint (Postgres DB, Azure storage API, other micro-services and apis (like Sendgrid or even other AA modules)
- a single endpoint simplifies caching and authentication
### Auth0 Integration
[Auth0](https://auth0.com/) is a third-party service that simplifies integrating authentication into a web application.
The main use of Auth0 will be for [Oauth 2.0](https://auth0.com/docs/protocols/oauth2) authentication requesting an [*implicit grant*](https://tools.ietf.org/html/rfc6749#section-1.3.2) flow. Auth0 also hosts the login/oauth authentication forms.
While Auth0 provides a depth of user-security services such as authorization (scopes/roles), multi-factor authentication, currently only basic authentication will be used.
#### Auth0 Function Flow
![Auth0 Function Flow](./assets/AATeamAuth0FunctionFlow.svg)
Following a likely User Experience flow and revealing the Auth0 API functionality below:
1. **User loads AAWeb app and clicks login**. App should initially check session/local storage on-load to see if user is already logged-in. For Single-sign on, can use [Auth0.checkSession](https://auth0.github.io/auth0.js/global.html#checkSession). Otherwise, a first action could be for user to click on the login button to initiate the authentication flow.
2. **AAWeb app sends authorize call to Auth0 and display universal login screen** The login screen is hosted by Auth0. A user can choose to login or signup using email/password or Oauth2 login through strategy provider like Github. After user submits and Auth0 authenticates user, it returns a callback address to the web app.
3. **AAWeb receives callback address and replies to that callback address.**
4. **Auth0 now sends back a JSON Web Token (JWT) back to AAWeb** AAWeb parses JWT and extracts id_token for user information. Saves access_token, id_token and token expiration for future authentication checks. AAWeb can make an `authenticate` mutation query to the API to create an initial account store in the database.
5. **After login, AAWeb checks for user's team information by making request to GraphQL API.** Sends a POST request with an `Authorization: Bearing <auth0_access_token>`header, AAWeb queries API for `teamByOwner` using Auth0 user_info *idTokenPayload.sub* as the owner_id.
6. **GraphQL API receives query request** API parses authentication token and verifies user, compares user_id to existing user or creates a new one. Searches for any teams owned by user. Returns data in JSON format.
7. **User does not have a team and clicks on `Create Team` button**
8. User is redirected to team module client. Session is checked and if user previously logged-in, then user is automatically signed in, otherwise user has to sign-in again. *Note 1: Single-sign-on (SSO) is just a convenience function that shows a single button in login form as opposed to some "auto-login" function.* *Note 2: If user signs in with different method (ie email/pass instead of social[github]) is treated as a new user.*
9. **Auth0 sends new access_token, id_token, expiration, etc** Web app should store values for future api calls.
10. **Make authenticated API class - same as step 5** Once logged in, user can create teams, edit profile, invite others to their team, etc.
11. **Data or errors returned** Both data and errors are returned in JSON format. Data usually returned with HTTP status 204 and errors returned with status 400.
## Working with GraphQL
If you're unfamiliar with working with GraphQL, the best way to start is learning the [Core Concepts](https://www.howtographql.com/basics/2-core-concepts/) and reading through some of the introduction on [HowToGraphql](https://www.howtographql.com/).
The next step is to start playing with queries in a GraphQL playground. Browse around this one: [https://api.algocommunity.org/playground](https://api.algocommunity.org/playground)
![GraphQL Playground](./assets/AAGraphQLPlayground.png)
1. The main query builder area. GraphQL schemas can be introspected, so auto-suggestion and linting is built in.
2. The response area: where data and errors will show up.
3. The schema documentation: The format of a GraphQL schema allows it to be "Self-documenting" showing the various queries and mutations along with their expected parameters and response forms.
4. Query Variables or Headers can be set. Especially the Authorization Header for queries/mutations that require authentication. The Bearer token is the Auth0 access_token.
### GraphQL tutorial
1. In the playground, let's make a first query. Right now, there's really only one call that doesn't require authenticated: getting teams list.
2. A GraphQL query is written in a JSON-like format of nested layers between brackets. The main searching function is called a "query" and is started by typing **`query {}`** Go ahead an enter that in the query builder section.
3. You should notice some syntax highlighting where the last bracket is marked red and if you hover your mouse over the red, you should see a message like `Syntax Error: Expected Name.` This is useful for letting you know that your query is malformed and it this case it's just telling you that it needs a Name to query — or to be more specific, a Name of a Type to query.
4. To know what our options are, click the Schema tab on the right-hand side. We are going to query the teams and you should see a **Query called teams:TeamConnection**.
5. Go ahead and **click on the query**. What's being show is that the `query teams` returns the type TeamConnection. There are no parameters. But we can somewhat choose what shape of response we get.
6. You'll notice this query returns three main TeamConnection items: pageInfo, edges, and aggregate. **Click on Edges** You should see a type of Edges with item `node: Team` Go ahead and explore the Team item down to see our query options. [Edges/node?](https://blog.apollographql.com/explaining-graphql-connections-c48b7c3d6976)
7. Back in the query builder, let's start building our query. Since we are querying `teams` add it to your query like so: ``` query { teams }``` Notice an autocomplete will pop up to help you figure out what to type.
8. `teams` will be red, and the syntax highlighting should give you a hint. That's right, we need to tell the query what we want returned. After `teams` add `{ }`
9. The cool thing about GraphQL is we can shape our response. We could return all three main query items, but if we really only need one, then we can tell it to return one item. In this case, start typing `edges` Autocomplete will help you again, and you can hit the [Tab] key to take advantage of the suggestions.
11. You probably clicked through a fair amount of pages when exploring the teams schema. Each slideout could be thought of as another layer of brackets and you working your way down the graph until you hit the end of a connection that ends in some type of scalar like String, Int, Boolean or custom ones you defined like Date or Role.
12. So using the syntax highlighting and schema, finish creating a query. No red is usually a sign that the query form is correct. Here's an example:
```
query {
teams {
edges {
node {
name
owner {
nickname
}
profile {
avatar
}
}
}
}
}
```
Go ahead and hit the large triangle button and see what you get!
Congrats, you've created your first GraphQL query.
### GraqphQL schema
The Teams Module graphql schema:
```
enum Role {
OWNER
MANAGER
MEMBER
}
type Team {
id: ID! @unique
createdAt: DateTime!
name: String!
slug: String! @unique
owner: Member! @relation (name: "TeamOwner", onDelete: CASCADE)
profile: TeamProfile
members: [TeamMembers!]! @relation (name: "TeamMembers", onDelete: SET_NULL)
anonymous: Boolean! @default(value: true)
}
type TeamMembers {
id: ID! @unique
member: Member @relation (name: "MemberOnTeam", onDelete: SET_NULL)
team: Team @relation (name: "TeamMembers", onDelete: SET_NULL)
role: Role @default(value: MEMBER)
}
type TeamProfile {
id: ID! @unique
createdAt: DateTime!
description: String
motto: String
avatar: String
}
type Member {
id: ID! @unique
nickname: String
teams: [TeamMembers!]! @relation (name: "MemberOnTeam", onDelete: SET_NULL)
profile: MemberProfile
auth0id: String! @unique
visible: Boolean! @default(value: false)
anonymous: Boolean! @default(value: true)
}
type MemberProfile {
id: ID! @unique
createdAt: DateTime!
bio: String
status: String
avatar: String
repo: String
email: String
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 366"><defs><style>.cls-1{font-size:14.28px;font-family:Saira-SemiBold, Saira;letter-spacing:0.1em;}.cls-1,.cls-23,.cls-28,.cls-46,.cls-65,.cls-70,.cls-76,.cls-88{fill:#666;}.cls-1,.cls-89,.cls-90,.cls-91,.cls-92{font-weight:700;}.cls-2{letter-spacing:0.05em;}.cls-3{letter-spacing:-0.03em;}.cls-4{letter-spacing:0.05em;}.cls-5{letter-spacing:0.04em;}.cls-6{letter-spacing:0.05em;}.cls-7{letter-spacing:0.05em;}.cls-8{letter-spacing:0.04em;}.cls-9{letter-spacing:0.05em;}.cls-10{letter-spacing:0.05em;}.cls-11{letter-spacing:0.05em;}.cls-12{letter-spacing:0.06em;}.cls-13{letter-spacing:0.04em;}.cls-14{letter-spacing:0.03em;}.cls-15{letter-spacing:0.04em;}.cls-16{letter-spacing:0.04em;}.cls-17{letter-spacing:0.07em;}.cls-18{letter-spacing:0.04em;}.cls-19{letter-spacing:0.05em;}.cls-20{letter-spacing:0.05em;}.cls-21{letter-spacing:0.05em;}.cls-22,.cls-93,.cls-94,.cls-95,.cls-96,.cls-97,.cls-98{fill:none;stroke:#666;stroke-miterlimit:10;}.cls-23,.cls-28,.cls-46,.cls-65,.cls-70,.cls-76,.cls-89,.cls-90,.cls-91,.cls-92{font-size:12px;}.cls-23,.cls-76{font-family:SairaExtraCondensed-Light, Saira ExtraCondensed;}.cls-23{letter-spacing:0.05em;}.cls-24{letter-spacing:0.06em;}.cls-25{letter-spacing:0.06em;}.cls-26{letter-spacing:0.05em;}.cls-27,.cls-76,.cls-89{letter-spacing:0.05em;}.cls-28,.cls-46,.cls-65,.cls-70{font-family:SairaExtraCondensed-Medium, Saira ExtraCondensed;}.cls-28,.cls-55{letter-spacing:0.05em;}.cls-29{letter-spacing:0.05em;}.cls-30{letter-spacing:0.04em;}.cls-31{letter-spacing:0.05em;}.cls-32,.cls-65{letter-spacing:0.05em;}.cls-33{letter-spacing:0.05em;}.cls-34,.cls-90{letter-spacing:0.05em;}.cls-35,.cls-91{letter-spacing:0.05em;}.cls-36{letter-spacing:0.05em;}.cls-37{letter-spacing:0.05em;}.cls-38{letter-spacing:0.04em;}.cls-39{letter-spacing:0.04em;}.cls-40{letter-spacing:0.05em;}.cls-41{letter-spacing:0.06em;}.cls-42{letter-spacing:0.04em;}.cls-43{letter-spacing:0.04em;}.cls-44{letter-spacing:0.05em;}.cls-45{letter-spacing:0.06em;}.cls-46{letter-spacing:0.05em;}.cls-47{letter-spacing:0.05em;}.cls-48{letter-spacing:0.06em;}.cls-49{letter-spacing:0.06em;}.cls-50{letter-spacing:0.04em;}.cls-51{letter-spacing:0.04em;}.cls-52{letter-spacing:0.05em;}.cls-53{letter-spacing:0.06em;}.cls-54{letter-spacing:0.06em;}.cls-56{letter-spacing:0.05em;}.cls-57{letter-spacing:0.05em;}.cls-58{letter-spacing:0.04em;}.cls-59{letter-spacing:0.04em;}.cls-60{letter-spacing:0.05em;}.cls-61{letter-spacing:0.05em;}.cls-62{letter-spacing:0.05em;}.cls-63{letter-spacing:0.04em;}.cls-64{letter-spacing:0.05em;}.cls-66{letter-spacing:0.05em;}.cls-67{letter-spacing:0.04em;}.cls-68{letter-spacing:0.04em;}.cls-69{letter-spacing:0.04em;}.cls-70{letter-spacing:0.08em;}.cls-71{letter-spacing:0.03em;}.cls-72{letter-spacing:0.04em;}.cls-73{letter-spacing:0.04em;}.cls-74{letter-spacing:0.04em;}.cls-75{letter-spacing:0.04em;}.cls-77{letter-spacing:0.04em;}.cls-78{letter-spacing:0.07em;}.cls-79{letter-spacing:0.06em;}.cls-80{letter-spacing:0.05em;}.cls-81{letter-spacing:0.05em;}.cls-82{letter-spacing:0.05em;}.cls-83{letter-spacing:0.05em;}.cls-84{letter-spacing:0.05em;}.cls-85{letter-spacing:0.06em;}.cls-86{letter-spacing:0.06em;}.cls-87{letter-spacing:0.05em;}.cls-89,.cls-90,.cls-91,.cls-92{fill:#fff;font-family:SairaExtraCondensed-Bold, Saira ExtraCondensed;}.cls-92{letter-spacing:0.05em;}.cls-93{stroke-dasharray:4.21 6.32;}.cls-94{stroke-dasharray:4.02 6.03;}.cls-95{stroke-dasharray:4.34 6.51;}.cls-96{stroke-dasharray:3.85 5.78;}.cls-97{stroke-dasharray:4.16 6.24;}.cls-98{stroke-dasharray:3.79 5.69;}</style></defs><title>AATeamAuth0FunctionFlow</title><text class="cls-1" transform="translate(14.07 29.34)">A<tspan class="cls-2" x="11.19" y="0">A </tspan><tspan class="cls-3" x="25.94" y="0">T</tspan><tspan class="cls-4" x="33.83" y="0">e</tspan><tspan class="cls-5" x="42.49" y="0">a</tspan><tspan class="cls-2" x="51.11" y="0">m </tspan><tspan class="cls-6" x="68.47" y="0">M</tspan><tspan class="cls-7" x="81.93" y="0">o</tspan><tspan class="cls-8" x="90.8" y="0">du</tspan><tspan class="cls-9" x="108.72" y="0">l</tspan><tspan class="cls-2" x="113.29" y="0">e </tspan><tspan class="cls-10" x="126.17" y="0">A</tspan><tspan class="cls-11" x="136.67" y="0">u</tspan><tspan class="cls-12" x="145.91" y="0">t</tspan><tspan class="cls-13" x="152.27" y="0">h</tspan><tspan class="cls-2" x="161.35" y="0">0 </tspan><tspan class="cls-14" x="175.78" y="0">F</tspan><tspan class="cls-15" x="184.44" y="0">u</tspan><tspan class="cls-16" x="193.46" y="0">n</tspan><tspan class="cls-17" x="202.52" y="0">c</tspan><tspan class="cls-12" x="210.05" y="0">t</tspan><tspan class="cls-18" x="216.41" y="0">i</tspan><tspan class="cls-9" x="220.97" y="0">o</tspan><tspan class="cls-2" x="229.78" y="0">n </tspan><tspan class="cls-19" x="243.18" y="0">F</tspan><tspan class="cls-20" x="252.11" y="0">l</tspan><tspan class="cls-21" x="256.69" y="0">o</tspan><tspan class="cls-2" x="265.54" y="0">w<Line truncated

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 226"><defs><style>.cls-1,.cls-44{fill:none;stroke:#666;stroke-miterlimit:10;}.cls-2{font-size:14.28px;font-family:Saira-SemiBold, Saira;letter-spacing:0.1em;}.cls-102,.cls-118,.cls-2,.cls-26,.cls-45,.cls-52,.cls-58,.cls-61,.cls-70,.cls-88,.cls-93,.cls-98,.cls-99{fill:#666;}.cls-119,.cls-120,.cls-2{font-weight:700;}.cls-3{letter-spacing:0.05em;}.cls-4{letter-spacing:-0.03em;}.cls-5{letter-spacing:0.05em;}.cls-6{letter-spacing:0.04em;}.cls-7{letter-spacing:0.05em;}.cls-8{letter-spacing:0.05em;}.cls-9{letter-spacing:0.04em;}.cls-10{letter-spacing:0.05em;}.cls-11{letter-spacing:0.05em;}.cls-12{letter-spacing:0.04em;}.cls-13{letter-spacing:0.04em;}.cls-14{letter-spacing:0.05em;}.cls-15{letter-spacing:0.06em;}.cls-16{letter-spacing:0.06em;}.cls-17{letter-spacing:0.04em;}.cls-18{letter-spacing:0.04em;}.cls-19{letter-spacing:0.06em;}.cls-20{letter-spacing:0.06em;}.cls-21{letter-spacing:0.07em;}.cls-22{letter-spacing:0.06em;}.cls-23{letter-spacing:0.04em;}.cls-24{letter-spacing:0.06em;}.cls-25{letter-spacing:0.05em;}.cls-102,.cls-119,.cls-120,.cls-26,.cls-45,.cls-52,.cls-58,.cls-61,.cls-70,.cls-88,.cls-93,.cls-98,.cls-99{font-size:12px;}.cls-26,.cls-45,.cls-52,.cls-58,.cls-99{font-family:SairaExtraCondensed-Light, Saira ExtraCondensed;}.cls-26,.cls-83{letter-spacing:0.05em;}.cls-27{letter-spacing:0.05em;}.cls-28{letter-spacing:0.05em;}.cls-29{letter-spacing:0.05em;}.cls-30{letter-spacing:0.06em;}.cls-102,.cls-120,.cls-31{letter-spacing:0.05em;}.cls-119,.cls-32,.cls-99{letter-spacing:0.05em;}.cls-33{letter-spacing:0.04em;}.cls-34{letter-spacing:0.07em;}.cls-35{letter-spacing:0.06em;}.cls-36{letter-spacing:0.05em;}.cls-37{letter-spacing:0.05em;}.cls-38{letter-spacing:0.05em;}.cls-39{letter-spacing:0.05em;}.cls-40{letter-spacing:0.06em;}.cls-41{letter-spacing:0.06em;}.cls-42{letter-spacing:0.04em;}.cls-43{letter-spacing:0.05em;}.cls-44{stroke-width:0.25px;}.cls-45{letter-spacing:0.06em;}.cls-46{letter-spacing:0.06em;}.cls-47{letter-spacing:0.06em;}.cls-48{letter-spacing:0.05em;}.cls-49{letter-spacing:0.05em;}.cls-50{letter-spacing:0.06em;}.cls-51{letter-spacing:0.04em;}.cls-52{letter-spacing:0.05em;}.cls-53{letter-spacing:0.04em;}.cls-54{letter-spacing:0.04em;}.cls-55{letter-spacing:0.06em;}.cls-56{letter-spacing:0.05em;}.cls-57{letter-spacing:0.06em;}.cls-58{letter-spacing:0.05em;}.cls-59{letter-spacing:0.06em;}.cls-60{letter-spacing:0.05em;}.cls-102,.cls-61,.cls-70,.cls-88,.cls-93,.cls-98{font-family:SairaExtraCondensed-Medium, Saira ExtraCondensed;}.cls-61{letter-spacing:0.06em;}.cls-62{letter-spacing:0.04em;}.cls-63{letter-spacing:0.07em;}.cls-64{letter-spacing:0.06em;}.cls-65,.cls-88{letter-spacing:0.05em;}.cls-66{letter-spacing:0.05em;}.cls-67{letter-spacing:0.05em;}.cls-68{letter-spacing:0.04em;}.cls-69{letter-spacing:0.05em;}.cls-70{letter-spacing:0.05em;}.cls-71{letter-spacing:0.05em;}.cls-72{letter-spacing:0.04em;}.cls-73{letter-spacing:0.04em;}.cls-74{letter-spacing:0.05em;}.cls-75{letter-spacing:0.05em;}.cls-76{letter-spacing:0.05em;}.cls-77,.cls-98{letter-spacing:0.05em;}.cls-78{letter-spacing:0.05em;}.cls-79{letter-spacing:0.05em;}.cls-80{letter-spacing:0.05em;}.cls-81{letter-spacing:0.05em;}.cls-82{letter-spacing:0.04em;}.cls-84{letter-spacing:0.06em;}.cls-85{letter-spacing:0.04em;}.cls-86{letter-spacing:0.04em;}.cls-87{letter-spacing:0.06em;}.cls-89{letter-spacing:0.04em;}.cls-90{letter-spacing:0.05em;}.cls-91{letter-spacing:0.04em;}.cls-92{letter-spacing:0.06em;}.cls-93{letter-spacing:0.08em;}.cls-94{letter-spacing:0.03em;}.cls-95{letter-spacing:0.04em;}.cls-96{letter-spacing:0.04em;}.cls-97{letter-spacing:0.05em;}.cls-100{letter-spacing:0.06em;}.cls-101{letter-spacing:0.06em;}.cls-103{letter-spacing:0.04em;}.cls-104{letter-spacing:0.05em;}.cls-105{letter-spacing:0.06em;}.cls-106{letter-spacing:0.05em;}.cls-107{letter-spacing:0.05em;}.cls-108{letter-spacing:0.04em;}.cls-109{letter-spacing:0.04em;}.cls-110{letter-spacing:0.05em;}.cls-111{letter-spacing:0.05em;}.cls-112{letter-spacing:0.04em;}.cls-113{letter-spacing:0.06em;}.cls-114{letter-spacing:0.05em;}.cls-115{letter-spacing:0.04em;}.cls-116{letter-spacing:0.04em;}.cls-117{letter-spacing:0.04em;}.cls-119,.cls-120{fill:#fff;font-family:SairaExtraCondensed-Bold, Saira ExtraCondensed;}</style></defs><title>AATeamDemoArchiteture</title><rect class="cls-1" x="25.49" y="76.58" width="202.21" height="68.84"/><text class="cls-2" transform="translate(21.82 29.34)">A<tspan class="cls-3" x="11.19" y="0">A </tspan><tspan class="cls-4" x="25.94" y="0">T</tspan><tspan class="cls-5" x="33.83" y="0">e</tspan><tspan class="cls-6" x="42.49" y="0">a</tspan><tspan class="cls-3" x="51.11" y="0">m </tspan><tspan class="cls-7" x="68.47" y="0">M</tspan><tspan class="cls-8" x="81.93" y="0">o</tspan><tspan class="cls-9" x="90.8" y="0">du</tspan><tspan class="cls-10" x="108.72" y="0">l</tspan><tspan class="cls-3" x="113.29" y="0">e </tspan><tspan class="cls-11" x="126.17" y="0">D</tspan>Line truncated

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "AATeamModule",
"version": "0.0.1",
"description": "Advanced Algos Team Module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/AdvancedAlgos/AATeamModule.git.git"
},
"keywords": [
"Advanced Algos"
],
"author": "Barry Low <bearcanru@gmail.com>",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/AdvancedAlgos/AATeamModule.git/issues"
},
"homepage": "https://github.com/AdvancedAlgos/AATeamModule.git#readme"
}
+5
View File
@@ -0,0 +1,5 @@
PRISMA_SECRET="prisma-secret"
PRISMA_ENDPOINT="http://localhost:4466"
AUTH0_DOMAIN="auth0-domain"
AUTH0_AUDIENCE="auth0-audience"
AUTH0_ISSUER="auth0-issuer"
+9
View File
@@ -0,0 +1,9 @@
/.env
/database/.env
/database/docker-compose.prisma.prod.yml
/docker-compose.prisma.prod.yml
dist
node_modules
.idea
.vscode
*.log
+12
View File
@@ -0,0 +1,12 @@
projects:
app:
schemaPath: src/schema.graphql
extensions:
endpoints:
dev1:
url: http://localhost:4000/graphql
database:
schemaPath: src/generated/prisma.graphql
extensions:
prisma: database/prisma.yml
+25
View File
@@ -0,0 +1,25 @@
FROM mhart/alpine-node:8
ARG PORT=4000
ARG APP_PATH=/usr/src/app
RUN mkdir -p /usr && \
mkdir -p $APP_PATH && \
mkdir -p $APP_PATH /node_modules
# Use app directory as current workdir
WORKDIR $APP_PATH
# Copy package.json to workdir
COPY package.json .
# Install dependencies
RUN npm install
# Copy application code
COPY . .
RUN NODE_ENV=production npm run start
# Expose Port
EXPOSE $PORT
+114
View File
@@ -0,0 +1,114 @@
# Advance Algos Team GraphQL API Server
This is the Advanced Algos Team Module server-side GraphQL API server. We use Webpack 4 + Docker to develop locally, then build to a distribution bundle that can then be uploaded via FTP, git or used with a continuous integration workflow.
This modules client-side component is the [aateam-client package](../aateam-client#README).
For more information on the general architecture and related technology of this module, please visit this repos [main documenation](../../docs#README).
## Getting Started
Run
```
npm install
```
then
```
npm run dev
```
### Using Docker
Get latest Docker and Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/)
#### Using Docker container for Development
To run starter kit in development mode with hot code reload execute:
Run docker container in background:
```
docker-compose up -d
```
If image issues, try running:
```
docker-compose up --build
```
then run
```
docker-compose up -d
```
To view logs
```
docker-compose logs -f -t
```
Hit Ctrl + c to exit logs without stopping service
To stop service
```
docker-compose down
```
## Overview
The development architecture of the Algobots Community site is to allow both rapid initial development and ongoing rapid-evolution. As with any opinionated workflow, there's a learning curve, but the libraries and architecture of this application have been chosen to maximize collaboration, flexibility, and extensibility.
### Main Libraries
- [*Webpack 4*](https://webpack.js.org/):web application bundler. Allows us to us latest newer JS ECMA features that increase productivity and code clarity. Also allows additional libraries such as modular and programmable CSS via SASS/SCSS/LESS. Provides webserver for local development.
- [*Bulma CSS Framework*](https://bulma.io/documentation/): Bulma is a highly-customizable CSS-only framework allowing us to quickly prototype the layout of a webapp without being complicated by opinionated functionality represented in libraries such as Bootstrap or Material UI. Grants us the flexibility to create a custom look at top speed whilst minimizing baggage.
- [*HyperApp*](https://github.com/hyperapp/hyperapp): Allows a controlled Flux-styled approach to state-management and data-flow through components like React minus the excess boilerplate and learning curve.
- [*Docker*](https://www.docker.com/what-docker)
### File organization
```
. # Top level directory located at your choice
├── src
│ ├── actions # Actions are functions that change the app state
│ ├── assets # images, logos and other static assets
│ ├── state # Schema of state — the single source of truth concerning app state.
│ ├── styles # SASS/SCSS styles
│ ├── utils # Local storage and other utils
│ └── views # Main folder for editing site content
│ ├─ nav # Nav components — header, footer, etc.
│ ├─ pages # Nav components — header, footer, etc.
│ │ ├─ landing # Homepage
│ │ │ ├─ sections # Homepage sections. Collated in its own index.js
│ │ │ └─ index.js # Main homepage component. Pulls in sections
│ │ └─ ... # Other page/component folders
│ └─ index.js # Main view container. Main router
├── html
│ └── index.html # index.html template pulled in by Webpack to attach transpiled bundles
├── index.js # JS entry point to website
│
├── tools # utility scripts, mostly for webpack configuration
│
├── node_modules # created by running `npm install` in this packages root dir
├── dist # created by `npm run develop` - a cache of bundled files served by Webpack for local development
└── build # created by `npm run build` - location of deployable production bundle
```
## Deployment
The simplest deployment is compiling/transpiling the site to a distribution bundle and then uploading the files in the bundle to a web-accessible folder or storage.
For this application we use Webpack to transpile the application which combines and minimizes all code and assets into an optimized bundle without and of the development code and resources.
To build, run:
```
npm run build
```
Looking in this packages directory, there should be a ./build directory with the final built files that can be uploaded.
### Create Docker image
```
docker build -t aateamapi/prod .
```
### Deployment instructions
Coming soon...
@@ -0,0 +1,51 @@
enum Role {
OWNER
MANAGER
MEMBER
}
type Team {
id: ID! @unique
createdAt: DateTime!
name: String! @unique
slug: String! @unique
owner: Member! @relation (name: "TeamOwner", onDelete: CASCADE)
profile: TeamProfile
members: [TeamMembers!]! @relation (name: "TeamMembers", onDelete: SET_NULL)
anonymous: Boolean! @default(value: true)
}
type TeamMembers {
id: ID! @unique
member: Member @relation (name: "MemberOnTeam", onDelete: SET_NULL)
team: Team @relation (name: "TeamMembers", onDelete: SET_NULL)
role: Role @default(value: MEMBER)
}
type TeamProfile {
id: ID! @unique
createdAt: DateTime!
description: String
motto: String
avatar: String
}
type Member {
id: ID! @unique
nickname: String
teams: [TeamMembers!]! @relation (name: "MemberOnTeam", onDelete: SET_NULL)
profile: MemberProfile
auth0id: String! @unique
visible: Boolean! @default(value: false)
anonymous: Boolean! @default(value: true)
}
type MemberProfile {
id: ID! @unique
createdAt: DateTime!
bio: String
status: String
avatar: String
repo: String
email: String
}
+8
View File
@@ -0,0 +1,8 @@
secret: ${env:PRISMA_SECRET}
endpoint: ${env:PRISMA_ENDPOINT}
datamodel: datamodel.graphql
hooks:
post-deploy:
- graphql get-schema --project database
- graphql prepare
@@ -0,0 +1,29 @@
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.15.1
restart: always
ports:
- "4466:4466"
environment:
PRISMA_CONFIG: |
managementApiSecret: PRISMA_SECRET
port: 4466
databases:
default:
connector: postgres
host: postgres
port: 5432
user: prisma
password: prisma
migrations: true
postgres:
image: postgres
restart: always
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
@@ -0,0 +1,31 @@
version: '3'
services:
node:
image: aateamapi/prod
environment:
- NODE_ENV=production
- GENERATE_SOURCEMAP=false
- APP_PATH=/usr/src/app
- COMPOSE_HTTP_TIMEOUT=2000
- VIRTUAL_HOST=api.algocommunity.org
- LETSENCRYPT_HOST=api.algocommunity.org
- LETSENCRYPT_EMAIL=barry@algocommunity.org
- VIRTUAL_PORT=4000
container_name: aateamapi
tty: true
stdin_open: true
volumes:
- ./:${APP_PATH}
- ${APP_PATH}/build
- ${APP_PATH}/node_modules
working_dir: ${APP_PATH}
restart: always
command: npm run start
expose:
- 4000
- 80
network_mode: webproxy
networks:
default:
external:
name: webproxy
+16
View File
@@ -0,0 +1,16 @@
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: development
ports:
- "4000:4000"
- "3000:3000"
volumes:
- .:/var/www/application
- /var/www/application/node_modules
container_name: aateam-api
+26
View File
@@ -0,0 +1,26 @@
{
"name": "aa-team-api",
"scripts": {
"start": "nodemon -e js,graphql -x node -r dotenv/config src/index.js",
"debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js",
"playground": "graphql playground --dotenv dotenv/config",
"dev": "npm-run-all --parallel debug playground"
},
"dependencies": {
"cors": "^2.8.4",
"express-jwt": "^5.3.1",
"graphql-import": "^0.6.0",
"graphql-tools": "3.1.1",
"graphql-yoga": "^1.16.1",
"jsonwebtoken": "^8.3.0",
"jwks-rsa": "^1.3.0",
"lodash.get": "^4.4.2",
"nodemon": "^1.18.3",
"prisma-binding": "2.1.4"
},
"devDependencies": {
"graphql-cli": "^2.16.5",
"npm-run-all": "4.1.3",
"prisma": "^1.14.0"
}
}
+116
View File
@@ -0,0 +1,116 @@
const _get = require('lodash.get')
const validateAndParseIdToken = require('./helpers/validateAndParseIdToken')
const createPrismaMember = require('./index')
const memberLocationOnContext = 'request.member'
const bearerAccessToken = 'request.headers.authorization'
const ctxMember = ctx => _get(ctx, memberLocationOnContext)
const ctxToken = ctx => _get(ctx, bearerAccessToken)
const isLoggedIn = async ctx => {
let member = ctxMember(ctx, memberLocationOnContext)
let token = ctxToken(ctx, bearerAccessToken)
let memberToken
console.log('authenticate.member0: ', ctx.request.headers, member, token)
if (!member && token) {
let scheme, credentials
const tokenParts = token.split(' ')
if (tokenParts.length === 2) {
scheme = tokenParts[0]
credentials = tokenParts[1]
}
if (/^Bearer$/i.test(scheme)) {
token = credentials
//verify token
try {
memberToken = await validateAndParseIdToken(token)
const auth0id = memberToken.sub
console.log('authenticate.member1: ', memberToken, member)
member = await ctx.db.query.member({ where: { auth0id } })
console.log('authenticate.member2: ', memberToken, member)
if (!member) {
member = await createPrismaMember(ctx, memberToken)
}
} catch (err) {
console.log('authenticate.validateAndParseIdToken err: ', err)
memberToken = false
}
}
console.log('isLoggedIn token: ', await memberToken)
}
console.log('member: ', await member)
if (!token && !member) {
throw new Error(`Not logged in`)
}
return member
}
const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, typeId }) =>
ctx.db.exists[type]({ id: typeId, member: { id: memberId } })
const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ id: memberId })
const directiveResolvers = {
isAuthenticated: async (next, source, args, ctx) => {
let result = await isLoggedIn(ctx)
console.log('isAuthenticated: ', result)
return next()
},
hasRole: (next, source, { roles }, ctx) => {
const { role } = isLoggedIn(ctx)
if (roles.includes(role)) {
return next()
}
throw new Error(`Unauthorized, incorrect role`)
},
isOwner: async (next, source, { type }, ctx) => {
const { id: typeId } =
source && source.id
? source
: ctx.request.body.variables ? ctx.request.body.variables : { id: null }
const { id: memberId } = isLoggedIn(ctx)
const isOwner =
type === `Member`
? memberId === typeId
: await isRequestingMemberAlsoOwner({ ctx, memberId, type, typeId })
if (isOwner) {
return next()
}
throw new Error(`Unauthorized, must be owner`)
},
isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => {
const { id: memberId, role } = isLoggedIn(ctx)
if (roles.includes(role)) {
return next()
}
const { id: typeId } = ctx.request.body.variables
const isOwner = await isRequestingMemberAlsoOwner({
ctx,
memberId,
type,
typeId
})
if (isOwner) {
return next()
}
const hasRole = await isRequestingMember({
ctx,
memberId,
type,
typeId
})
if(hasRole){
return next()
}
throw new Error(`Unauthorized, not owner or incorrect role`)
}
}
module.exports = { directiveResolvers }
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,29 @@
const jwksClient = require('jwks-rsa')
const jwt = require('jsonwebtoken')
const jwks = jwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 1,
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
})
const validateAndParseIdToken = idToken =>
new Promise((resolve, reject) => {
const { header, payload } = jwt.decode(idToken, { complete: true })
if (!header || !header.kid || !payload) reject(new Error('Invalid Token'))
jwks.getSigningKey(header.kid, (err, key) => {
if (err) reject(new Error('Error getting signing key: ' + err.message))
jwt.verify(
idToken,
key.publicKey,
{ algorithms: ['RS256'] },
(err, decoded) => {
if (err) reject('jwt verify error: ' + err.message)
console.log('jwks-verify: ', decoded)
resolve(decoded)
}
)
})
})
module.exports = validateAndParseIdToken
+237
View File
@@ -0,0 +1,237 @@
const { GraphQLServer } = require('graphql-yoga')
const {
Prisma,
extractFragmentReplacements,
forwardTo
} = require('prisma-binding')
const { makeExecutableSchema } = require('graphql-tools')
const { importSchema } = require('graphql-import')
const { checkJwt } = require('./middleware/jwt')
const { getMember } = require('./middleware/getMember')
const validateAndParseIdToken = require('./helpers/validateAndParseIdToken')
const { directiveResolvers } = require('./directives')
const createPrismaMember = async function (ctx, info, idToken) {
console.log('createPrismaMember', idToken)
const member = await ctx.db.mutation.upsertMember({
where: {
auth0id: idToken.sub,
},
create: {
auth0id: idToken.sub,
nickname: idToken.nickname,
profile: {
create: {
email: idToken.email,
avatar: idToken.picture
}
}
},
update: {
auth0id: idToken.sub,
nickname: idToken.nickname,
profile: {
update: {
email: idToken.email,
avatar: idToken.picture
}
}
}
}, info)
return member
}
const ctxMember = ctx => ctx.request.member
const resolvers = {
Query: {
async member(parent, { auth0id }, ctx, info) {
return ctx.db.query.member({ where: { auth0id } }, info)
},
teams(parent, args, ctx, info) {
return ctx.db.query.teamsConnection({},
`{
edges {
node {
id
name
slug
profile {
avatar
description
motto
}
owner {
nickname
visible
profile {
avatar
}
}
members {
role
member {
nickname
visible
profile {
avatar
}
}
}
}
}
}`)
},
async teamById(parent, { id }, ctx, info) {
return ctx.db.query.team({ where: { id } }, info)
},
async teamByName(parent, { name }, ctx, info) {
return ctx.db.query.team({ where: { name: name } }, `{ name }`)
},
async teamByOwner(parent, { ownerId }, ctx, info) {
return ctx.db.query.teamsConnection({where: { owner: { auth0id: ownerId }}},
`{
edges {
node {
id
name
slug
profile {
avatar
description
motto
}
owner {
auth0id
nickname
visible
profile {
avatar
}
}
members {
role
member {
nickname
visible
profile {
avatar
}
}
}
}
}
}`)
},
async owner(parent, args, ctx, info) {
console.log('resolver.query.owner ctx: ', ctxMember(ctx))
return ctx.db.query.member({ where: { id: ctxMember(ctx).auth0id } }, info)
},
members: forwardTo('db')
},
Mutation: {
async authenticate(parent, { idToken }, ctx, info) {
let memberToken = null
try {
memberToken = await validateAndParseIdToken(idToken)
console.log('authenticate.memberToken: ', await memberToken)
} catch (err) {
console.log('authenticat.validateAndParseIdToken err: ', err)
throw new Error(err.message)
}
const auth0id = memberToken.sub
let member = await ctx.db.query.member({ where: { auth0id } }, info)
if (!member) {
try {
return createPrismaMember(ctx, info, memberToken)
} catch (error) {
try {
return createPrismaMember(ctx, info, memberToken)
} catch (error) {
throw new Error(error.message)
}
}
}
return member
},
async createTeam(parent, { name, slug, owner }, ctx, info) {
ctx.db.mutation.createTeam({
data: {
name: name,
slug: slug,
owner: {connect:{auth0id: owner}},
members: {create: {role: 'OWNER', member: {connect: {auth0id: owner}}}}
}
}, info)
},
async deleteTeam(parent, { id, owner }, ctx, info) {
ctx.db.mutation.deleteTeam({ where: { id, member: {where: { auth0id: owner } } } }, owner)
}
}
}
const db = new Prisma({
fragmentReplacements: extractFragmentReplacements(resolvers),
typeDefs: 'src/generated/prisma.graphql',
endpoint: process.env.PRISMA_ENDPOINT,
secret: process.env.PRISMA_SECRET,
debug: true
})
console.log(process.env.PRISMA_ENDPOINT)
const schema = makeExecutableSchema({
typeDefs: importSchema('src/schema.graphql'),
resolvers,
resolverValidationOptions :{ requireResolversForResolveType: false },
directiveResolvers
})
const server = new GraphQLServer({
schema,
context: req => ({
...req,
db
})
})
server.express.post(
server.options.endpoint,
checkJwt,
(err, req, res, next) => {
console.log('server.express.post:',err, req, res, next, req.token)
if (err) return res.status(401).send(err.message)
next()
}
)
server.express.post(server.options.endpoint, (req, res, done) =>
getMember(req, res, done, db)
)
const whitelist = [
'http://localhost:8000',
'http://localhost:1337',
'http://localhost:3000',
'http://localhost:4000',
'http://0.0.0.0:5000'
]
const corsOptionsDelegate = (req, callback) => {
var corsOptions
if (whitelist.indexOf(req.header('Origin')) !== -1) {
corsOptions = { origin: true, credentials: true } // reflect (enable) the requested origin in the CORS response
} else {
corsOptions = { origin: false, credentials: true } // disable CORS for this request
}
callback(null, corsOptions) // callback expects two parameters: error and options
}
const options = {
cors: corsOptionsDelegate,
endpoint: '/graphql',
subscriptions: '/graphql',
playground: '/playground'
}
server.start(options, () => console.log(`Server is running on http://localhost:4000${server.options.endpoint}`))
module.exports= { createPrismaMember }
@@ -0,0 +1,10 @@
const getMember = async (req, res, next, db) => {
if (!req.user) return next()
const member = await db.query.member({
where: { auth0id: req.user.sub }
})
req.user = { token: req.member, ...member }
next()
}
module.exports = { getMember }
+24
View File
@@ -0,0 +1,24 @@
const jwt = require('express-jwt')
const jwksRsa = require('jwks-rsa')
// Authentication middleware. When used, the
// if the access token exists, it be verified against
// the Auth0 JSON Web Key Set
const checkJwt = jwt({
// Dynamically provide a signing key
// based on the kid in the header and
// the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 1,
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
// Validate the audience and the issuer.
credentialsRequired: false,
audience: process.env.AUTH0_AUDIENCE,
issuer: process.env.AUTH0_ISSUER,
algorithms: [`RS256`]
})
module.exports = { checkJwt }
+35
View File
@@ -0,0 +1,35 @@
# import Query.members, Query.TeamConnection, Team from "./generated/prisma.graphql"
directive @isAuthenticated on QUERY | FIELD | MUTATION
directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION
directive @isOwner(type: String) on QUERY | MUTATION
directive @isOwnerOrHasRole(type: String, roles: [String]) on QUERY | MUTATION
type Query {
teams: TeamConnection
teamDetails: [Team!]! @isAuthenticated
teamByName(name: String!): Team
teamById(teamId: String!): Team
teamByOwner(ownerId: String!): TeamConnection
owner: Member @isAuthenticated
member(auth0id: String!): Member @isAuthenticated
}
type Mutation {
authenticate(idToken: String!): Member
createTeam(name: String!, slug: String, owner: String): Team @isAuthenticated
deleteTeam(id: ID!, owner: String!): Team @isOwnerOrHasRole(type: "Member", roles: ["OWNER"])
updateProfile(data: MemberProfileInput): Member @isAuthenticated
}
input MemberProfileInput {
auth0id: String!
nickname: String!
anonymous: Boolean
visible: Boolean
avatar: String
email: String
repo: String
bio: String
status: String
}
+11
View File
@@ -0,0 +1,11 @@
{
"presets": [
["@babel/env", {
"modules": false
}]
],
"plugins": [
"syntax-dynamic-import",
["transform-react-jsx", { "pragma": "h" }]
]
}
+7
View File
@@ -0,0 +1,7 @@
.git
.cache
dist
build
.DS_Store
node_modules
npm-debug.log
+11
View File
@@ -0,0 +1,11 @@
PORT=5000
NODE_ENV=development
DEV_GRAPHQL=http://localhost:9090/graphql
PROD_GRAPHQL=https://production.com/graphql
AUTH0_DOMAIN=<auth0-domain>
AUTH0_AUDIENCE=<auth0-audience>
AUTH0_ISSUER=<auth0-issuer>
AUTH0_CLIENTID=<auth0-clientid>
AUTH0_CALLBACKURL=<auth0-callbackurl>
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "standard"
}
+71
View File
@@ -0,0 +1,71 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# Build folder js files
build/
dist/
# Visual Studio Code
.vscode
# JetBrains ide
.idea
#Mac specific files
.DS_Store
+29
View File
@@ -0,0 +1,29 @@
FROM mhart/alpine-node
ENV PORT=5000
ENV NODE_ENV=development
ENV APP_PATH=/var/www/application
RUN apk update && apk upgrade && \
apk add --no-cache bash git
# Create app directory
RUN mkdir -p $APP_PATH
# Use app directory as current workdir
WORKDIR $APP_PATH
# Copy package.json to workdir
COPY package.json .
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Expose Port
EXPOSE $PORT
# Run command
CMD npm run develop
+108
View File
@@ -0,0 +1,108 @@
# Advance Algos Team Client-side Application
This is the Advanced Algos Team Module client-side web app a.k.a. single-page-application (SPA). We use Webpack 4 + Docker to develop locally, then build to a distribution bundle that can then be uploaded via FTP, git or used with a continuous integration workflow.
This webapp's server-side component is the [aateam-api package](../aateam-api#README).
For more information on the general architecture and related technology of this module, please visit this repos [main documenation](../../docs#README).
## Getting Started
Run
```
npm install
```
then
```
npm run develop
```
### Using Docker
Get latest Docker and Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/)
#### Using Docker container for Development
To run starter kit in development mode with hot code reload execute:
Run docker container in background:
```
docker-compose up -d
```
If image issues, try running:
```
docker-compose up --build
```
then run
```
docker-compose up -d
```
To view logs
```
docker-compose logs -f -t
```
Hit Ctrl + c to exit logs without stopping service
To stop service
```
docker-compose down
```
## Overview
The development architecture of the Algobots Community site is to allow both rapid initial development and ongoing rapid-evolution. As with any opinionated workflow, there's a learning curve, but the libraries and architecture of this application have been chosen to maximize collaboration, flexibility, and extensibility.
### Main Libraries
- [*Webpack 4*](https://webpack.js.org/):web application bundler. Allows us to us latest newer JS ECMA features that increase productivity and code clarity. Also allows additional libraries such as modular and programmable CSS via SASS/SCSS/LESS. Provides webserver for local development.
- [*Bulma CSS Framework*](https://bulma.io/documentation/): Bulma is a highly-customizable CSS-only framework allowing us to quickly prototype the layout of a webapp without being complicated by opinionated functionality represented in libraries such as Bootstrap or Material UI. Grants us the flexibility to create a custom look at top speed whilst minimizing baggage.
- [*HyperApp*](https://github.com/hyperapp/hyperapp): Allows a controlled Flux-styled approach to state-management and data-flow through components like React minus the excess boilerplate and learning curve.
- [*Docker*](https://www.docker.com/what-docker)
### File organization
```
. # Top level directory located at your choice
├── src
│ ├── actions # Actions are functions that change the app state
│ ├── assets # images, logos and other static assets
│ ├── state # Schema of state — the single source of truth concerning app state.
│ ├── styles # SASS/SCSS styles
│ ├── utils # Local storage and other utils
│ └── views # Main folder for editing site content
│ ├─ nav # Nav components — header, footer, etc.
│ ├─ pages # Nav components — header, footer, etc.
│ │ ├─ landing # Homepage
│ │ │ ├─ sections # Homepage sections. Collated in its own index.js
│ │ │ └─ index.js # Main homepage component. Pulls in sections
│ │ └─ ... # Other page/component folders
│ └─ index.js # Main view container. Main router
├── html
│ └── index.html # index.html template pulled in by Webpack to attach transpiled bundles
├── index.js # JS entry point to website
│
├── tools # utility scripts, mostly for webpack configuration
│
├── node_modules # created by running `npm install` in this packages root dir
├── dist # created by `npm run develop` - a cache of bundled files served by Webpack for local development
└── build # created by `npm run build` - location of deployable production bundle
```
## Deployment
The simplest deployment is compiling/transpiling the site to a distribution bundle and then uploading the files in the bundle to a web-accessible folder or storage.
For this application we use Webpack to transpile the application which combines and minimizes all code and assets into an optimized bundle without and of the development code and resources.
To build, run:
```
npm run build
```
Looking in this packages directory, there should be a ./build directory with the final built files that can be uploaded.
### Deployment instructions
Coming soon...
+19
View File
@@ -0,0 +1,19 @@
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: development
ports:
- "5000:5000"
- "4536:4536"
expose:
- "5000"
- "4536"
volumes:
- .:/var/www/application
- /var/www/application/node_modules
container_name: aateam-client
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AlgoCommunity</title>
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="The AlgoCommunity is a collaborative setting for developing, competing and evolving Advanced Algos financial beings">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#e3493c">
<meta name="apple-mobile-web-app-title" content="Algobots Community">
<meta name="application-name" content="Algobots Community">
<meta name="msapplication-TileColor" content="#e3493c">
<meta name="theme-color" content="#ffffff">
<link href="https://fonts.googleapis.com/css?family=Saira+Condensed:300,600|Saira:300,600&amp;subset=latin-ext,vietnamese" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous">
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,19 @@
{
"name": "Advanced Algos",
"short_name": "Advanced Algos",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1365.333" height="1365.333" viewBox="0 0 1024.000000 1024.000000"><path d="M685.9 27.4c-.2.2-3 .7-6.1 1-12.6 1.5-34.6 8.5-45.2 14.5-2 1.2-5.3 3.1-7.3 4.2-14.3 8.1-30.2 22.3-43.5 38.8-5.9 7.4-16.2 22.1-15.5 22.1.2 0 3.6-1.7 7.5-3.9 17.4-9.6 28.8-12.1 52.2-11.7 13.4.3 17.7.8 23.1 2.5 25.1 8 45.2 36.6 58.9 83.7 1.8 6 3.3 11.6 3.5 12.4 2.3 10.8 3.5 17.1 4 20 .3 1.9 1 6 1.5 9 1.2 7.3 1.8 11.3 2.5 18.5.4 3.3.8 7.6 1.1 9.5 1.6 14.3 2.2 51.4 1 69.5-1.4 21.3-1.5 26-.5 31 .5 2.7 1.2 6.3 1.5 8 2.3 12.9 20.6 69 33.4 102.5 1.6 4.1 4.8 12.4 7.1 18.5 17.1 45.2 47.3 118.6 49.1 119.3.4.2.8.9.8 1.5.1 3.8 18.3 22 29.2 29.1 4.7 3.1 8.8 5.6 9 5.6.3 0-3.2-10.9-7.7-24.3-4.5-13.3-8.4-25.3-8.7-26.7-.4-1.4-2-7.2-3.7-13-4.3-14.5-12.2-45.3-13.7-54-.3-1.4-.7-3.3-1-4.2-.3-1-.7-3-1-4.5-.2-1.6-.9-4.8-1.4-7.3-.5-2.5-1.4-7.2-2-10.5-.5-3.3-1.2-7.1-1.5-8.5-.3-1.4-1-5.7-1.5-9.5-.6-3.9-1.2-8.1-1.5-9.5-.3-2.3-.7-5.4-2-16.3-3.1-25.8-3.8-58.6-1.5-73.2.7-4.4 2.2-10.7 3.5-14 2.3-6.1 9-28.9 9.9-33.5.3-1.4 1.2-5.2 2-8.5s1.7-7.4 1.9-9c.3-1.7 1.4-8.2 2.6-14.6 1.1-6.3 2.3-14.7 2.6-18.5.4-3.8.9-8.3 1.1-9.9 2.2-16.2 2.2-69.4 0-83.5-.2-1.4-.7-5-1.1-8-6.7-56.6-26.4-102.4-53.1-123.1-11.1-8.5-31-16.4-49.9-19.7-9.6-1.7-38.4-2.9-39.6-1.8zM465 215.4c-2.3 3-9.7 11.7-14.5 17-1.1 1.2-5.6 6.2-10 11.1-8.3 9.3-43.9 45.2-53.5 54-51.1 46.9-108.8 86-132.5 90-2.2.4-4.4.8-5 .9-.5.2-2.8.6-5 1.1-24.5 4.7-70 20.4-91 31.3-2.7 1.4-5.2 2.6-5.5 2.7-.7.1-12.3 6.2-16.7 8.7-2.1 1.2-5.4 3.1-7.5 4.2C62.4 471.4 16.3 519 4.3 559.8.9 571.3.5 590 3.4 603.5c2.6 12.1 7.2 25.6 11.8 35 19.6 39.1 52.7 65.8 95.3 76.8 5.5 1.4 12 2.9 14.5 3.2 2.5.4 5.4.9 6.5 1.1 2.8.5 11.7 1.4 19.5 1.9l6.4.4-3.4-2.1c-20.8-12.5-29.7-21.3-39-38.3-1.8-3.3-4.3-7.8-5.5-10-6.1-11.2-8.2-22.8-6.4-35.1 7.4-48 72.3-111.2 159.4-155 10.5-5.3 21-11.2 23.5-13.1 8.7-6.7 26.9-25.3 45.4-46.3 1.7-2 4.9-5.6 7.1-8.1 2.2-2.4 6-6.9 8.5-9.8 2.5-3 5-5.9 5.5-6.5.6-.6 3-3.6 5.5-6.6 6.5-8 7.1-8.6 15.2-18.5 12.7-15.4 51.5-64.5 61.3-77.6 1.1-1.5 5.9-7.8 10.6-14 11.5-15.2 13.3-18.1 16.8-26.9 3-7.7 3.5-9.3 5.1-19 1.2-7.1 1.9-21.9 1.1-22.4-.3-.2-1.7 1.1-3.1 2.8zM563.9 315c-21.5 5.4-43.9 28.6-59.4 61.3-5.2 11-10.1 24.9-12.5 35.7-2 8.8-6.1 16.2-21 37.4-1.8 2.6-17.8 23.2-20.7 26.6-14.7 17.6-22.4 26.2-31.2 35.1-8.7 8.6-12.2 11.3-19.6 15.2-16.7 8.7-25.6 14.5-37 23.8-22.2 18.1-36.8 38.2-41 56.4-1.6 6.6-2.2 17.6-1.1 19.3.3.5.7 2.1 1 3.7.8 4.3 5.3 12.7 9.1 16.8 10.1 11.1 27.7 18.5 50.5 21.3 12.2 1.5 36.1.8 49-1.6 11.6-2 12.6-2.3 20.5-4.5s31.7-2.2 43.5 0c.8.2 4.4.6 8 1 3.6.4 7.4.9 8.4 1.1 1.1.2 4.2.6 6.9.9 2.8.3 5.3.8 5.6.9.3.2 2.9.7 5.6 1 9.1 1.3 39 7.5 47.9 10.1 8.9 2.6 18.5 7 20.6 9.6 1.9 2.3 30.1 16.5 39.5 20 9.1 3.3 26.6 7.7 33.7 8.6 9.2 1.1 23.9.7 30-.7 19.5-4.6 33.3-18.5 36.3-36.7.7-4.2.8-19 .1-20.1-.2-.4-.7-2.4-1-4.6-1-6.1-5.5-18.3-10.3-28.1-4.2-8.3-15.4-26.1-20.1-31.6-5.8-7-14.5-17-18.6-21.4-2.6-2.8-6.5-8-8.7-11.7-6.3-10.8-17.6-35.1-25.4-55.3-1.5-3.9-3.3-8.4-4-10-1.4-3.5-4.9-12.8-5-13.5 0-.3-1.2-3.7-2.5-7.5-1.3-3.9-2.5-7.5-2.6-8-.1-.6-.9-3.5-1.8-6.6-2.6-8.9-2.8-10-2.6-17.4.7-25.5.3-39.3-1.3-49.5-.3-1.4-.8-4.5-1.2-7-.3-2.4-.8-4.7-1-5.1-.2-.3-.7-2.1-1-4-.9-5.4-6.3-20.6-9.7-27.3-7.5-14.9-16.6-24.9-27.5-30.3-6.3-3.1-8.3-3.6-16-3.9-4.8-.2-10.4.1-12.4.6zM340 752.1c-12.3 2.1-18.9 4.4-31.5 10.7-10.5 5.3-11.8 4.4 13 9.2 69 13.5 135.5 34.9 186.8 60.2 24.5 12.1 41 22.8 49.7 32.3 24.3 26.6 37.9 39.4 60 56.6 50.3 39.2 116.2 69.8 162.5 75.5 2.8.3 6.8.9 9 1.3 4.8.9 28.3.6 34-.3 23.2-4 37.1-11.8 56.6-31.7 33-33.8 46.8-81.2 37.4-128.4-3.2-16-8-29.9-16.2-46.5l-4.2-8.5-.1 8c-.1 4.4-.3 8.7-.4 9.5-.2.8-.6 3.8-1 6.6-1.8 14.4-15.1 40.8-26.4 52.7-17.8 18.7-53.6 23.1-99.2 12.2-5.8-1.4-11.5-2.7-12.6-3-1.2-.3-7.7-2.3-14.5-4.6-36.3-12-78-32.8-111.4-55.7-6-4.1-13-8.4-15.5-9.4-2.5-1.1-5.4-2.3-6.5-2.8-5.6-2.4-39.7-11.2-52.5-13.5-1.9-.3-4.2-.7-5-.9-.8-.1-4-.8-7-1.4-9.8-2-26.4-5.1-30-5.6-3-.5-29.1-4.8-37-6.1-1.4-.2-4.3-.7-6.5-1-6.9-1.1-9.8-1.5-15.5-2.5-3-.5-8.2-1.2-11.5-1.6-3.3-.3-6.2-.7-6.5-.9-.3-.2-4.4-.9-9-1.5-4.7-.6-9.6-1.3-11-1.5-1.4-.2-6.3-.9-11-1.5s-9.6-1.3-11-1.5c-5.9-.8-10.8-1.5-15-2-5.1-.6-9.4-1.2-16-2.1-7.1-.9-20.1-1.1-25-.3z"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
{
"name": "aateam-client",
"version": "0.0.1",
"description": "Advanced Algos Team client-side web app",
"main": "src/index.js",
"scripts": {
"develop": "NODE_ENV=development PORT=5000 webpack-serve --config ./webpack.config.js",
"build": "NODE_ENV=production webpack --mode production --config ./webpack.config.js",
"lint": "node_modules/.bin/eslint src",
"format": "prettier-standard 'src/**/*.js'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"hyperapp",
"bulma"
],
"repository": "",
"author": "Barry Low <bearcanrun@gmail.com>",
"license": "AGPL-3.0",
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"autoprefixer": "^8.6.5",
"babel-eslint": "8.2.2",
"babel-loader": "^8.0.0",
"babel-minify-webpack-plugin": "^0.3.1",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-spread": "^6.22.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"bulma-o-steps": "^1.0.8",
"connect-history-api-fallback": "^1.5.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"file-loader": "^1.1.11",
"html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^3.2.0",
"koa-connect": "^2.0.1",
"mini-css-extract-plugin": "^0.4.2",
"minilog": "^3.1.0",
"node-sass": "^4.9.3",
"optimize-css-assets-webpack-plugin": "^4.0.3",
"postcss-loader": "^2.1.6",
"prettier-standard": "^8.0.1",
"sass-loader": "^6.0.7",
"script-ext-html-webpack-plugin": "^2.0.1",
"standard": "^12.0.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^4.17.1",
"webpack-cli": "^2.1.5",
"webpack-hot-client": "^4.1.1",
"webpack-merge": "^4.1.4",
"webpack-serve": "^1.0.4",
"workbox-build": "^3.4.1",
"workbox-cli": "^3.4.1"
},
"dependencies": {
"@hyperapp/fx": "^0.7.0",
"@hyperapp/router": "^0.7.0",
"apollo-cache-inmemory": "^1.2.9",
"apollo-client": "^2.4.1",
"apollo-link": "^1.2.2",
"apollo-link-context": "^1.0.8",
"apollo-link-error": "^1.1.0",
"apollo-link-http": "^1.5.4",
"apollo-link-ws": "^1.0.8",
"apollo-utilities": "^1.0.20",
"auth0-lock": "^11.9.0",
"babel-polyfill": "^6.26.0",
"bulma": "^0.7.1",
"bulma-timeline": "^2.0.3",
"cross-fetch": "^2.2.2",
"dotenv-webpack": "^1.5.7",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"hyperapp": "^1.2.8",
"subscriptions-transport-ws": "^0.9.14"
},
"standard": {
"parser": "babel-eslint",
"ignore": [
"dist/",
"build/"
]
},
"prettier": {
"semi": false
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
@@ -0,0 +1,58 @@
import { action, log } from '@hyperapp/fx'
import { location } from '@hyperapp/router'
import { nav } from '../modules/nav/actions'
import { team } from '../modules/team/actions'
import { algobot } from '../modules/algobots/actions'
import { community } from '../modules/community/actions'
export const actions = {
location: location.actions,
isLoggedIn: authStatus => async (state, actions) => {
authStatus.then((result) => {
if (result) {
actions.setLoggedIn(result)
actions.redirect('dashboard')
} else {
actions.loggedIn(result)
actions.redirect('/')
}
})
},
loggedIn: loggedIn => ({ loggedIn }),
redirect: redirect => ({ redirect }),
setLoggedIn: profile => [
log('setLoggedIn', profile, window.location.pathname),
action('loggedIn', true),
action('setUserProfile', profile),
action('redirect', window.location.pathname)
],
setLoggedOut: () => [
log('setLoggedOut', true),
action('loggedIn', false),
action('setUserProfileEmpty')
],
setUserProfile: profile => {
const user = {
id: profile.sub,
name: profile.name,
username: profile.nickname,
avatar: profile.picture,
email: profile.email
}
return { user }
},
setUserProfileEmpty: () => ({ user: '' }),
toggleModal: form => (state, actions) => {
return {
modal: {
content: form,
isActive: !state.modal.isActive
}
}
},
nav,
team,
algobot,
community
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 98 140"><defs><style>.cls-1,.cls-3{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{stroke:#2a96ff;stroke-miterlimit:10;stroke-width:0.5px;}.cls-4{fill:#4d4d4d;}.cls-5{fill:#e3493c;}</style><clipPath id="clip-path"><rect class="cls-1" x="144.54" y="59.59" width="600" height="600"/></clipPath></defs><title>aa-logo-vert</title><g id="Layer_1" data-name="Layer 1"><g class="cls-2"><circle class="cls-3" cx="210.14" cy="243.28" r="277.97"/></g></g><g id="Layer_4" data-name="Layer 4"><path class="cls-4" d="M11.27,108.29l-.38-3H8.62l-.38,3h-3L7.49,93.1h4.57l2.28,15.19ZM9.83,96.08H9.68L8.9,102.6h1.71Z"/><path class="cls-4" d="M18,93.1h3.69a4.53,4.53,0,0,1,2.79.67,3.22,3.22,0,0,1,1,2.11,41.13,41.13,0,0,1,.19,4.82,41,41,0,0,1-.19,4.81,3.19,3.19,0,0,1-1,2.11,4.46,4.46,0,0,1-2.79.67H18Zm3.69,12.43a.89.89,0,0,0,.71-.22,2.31,2.31,0,0,0,.25-1.15c0-.61,0-1.77,0-3.46s0-2.85,0-3.47a2.31,2.31,0,0,0-.25-1.15.89.89,0,0,0-.71-.22H21v9.67Z"/><path class="cls-4" d="M31.16,108.29,29.11,93.1h3l1.25,12.06h.23L34.8,93.1h3l-2.05,15.19Z"/><path class="cls-4" d="M46.35,108.29l-.38-3H43.7l-.38,3h-3L42.57,93.1h4.57l2.28,15.19ZM44.91,96.08h-.15L44,102.6h1.7Z"/><path class="cls-4" d="M58.44,108.29l-2.36-7.62H56v7.62H53.12V93.1h2.8L58.09,100h.11V93.1h2.89v15.19Z"/><path class="cls-4" d="M70.19,108.36a6.43,6.43,0,0,1-1.12.11,4,4,0,0,1-2.61-.71,3.72,3.72,0,0,1-1.1-2.28,31.28,31.28,0,0,1-.26-4.78,29.46,29.46,0,0,1,.28-4.77,3.82,3.82,0,0,1,1.12-2.3,3.9,3.9,0,0,1,2.57-.71,7.72,7.72,0,0,1,1.18.1,3.21,3.21,0,0,1,.85.24v2.53a11.75,11.75,0,0,0-1.48-.11,1.48,1.48,0,0,0-1.05.29,2.16,2.16,0,0,0-.35,1.23c0,.62-.08,1.79-.08,3.5s0,2.87.08,3.5a2.15,2.15,0,0,0,.35,1.22,1.48,1.48,0,0,0,1.05.29,9.89,9.89,0,0,0,1.53-.11v2.47A4.23,4.23,0,0,1,70.19,108.36Z"/><path class="cls-4" d="M75,108.29V93.1h6.09v2.76H78v3.38h2.7V102H78v3.53h3.07v2.76Z"/><path class="cls-4" d="M85.08,93.1h3.69a4.55,4.55,0,0,1,2.79.67,3.16,3.16,0,0,1,1,2.11,38.45,38.45,0,0,1,.2,4.82,38.37,38.37,0,0,1-.2,4.81,3.14,3.14,0,0,1-1,2.11,4.48,4.48,0,0,1-2.79.67H85.08Zm3.69,12.43a.89.89,0,0,0,.71-.22,2.31,2.31,0,0,0,.25-1.15c0-.61,0-1.77,0-3.46s0-2.85,0-3.47a2.31,2.31,0,0,0-.25-1.15.89.89,0,0,0-.71-.22h-.69v9.67Z"/><path class="cls-5" d="M19.79,134.79l-2.1-5.3H9.08L7,134.79H5.24l7.15-18.23h2l7.16,18.23Zm-6.33-16.24h-.14L9.64,127.9h7.5Z"/><path class="cls-5" d="M26.41,134.79V116.56h1.7v16.58H37v1.65Z"/><path class="cls-5" d="M48.1,126.76V125.2h6.2v9.3c-.87.14-1.84.26-2.9.35s-2,.15-2.8.15a12.88,12.88,0,0,1-4.9-.69,3.89,3.89,0,0,1-2.15-2.6,22.5,22.5,0,0,1-.58-6,22.11,22.11,0,0,1,.6-6A3.87,3.87,0,0,1,43.72,117a13.15,13.15,0,0,1,4.88-.67c.88,0,1.85.05,2.92.14s2,.22,2.7.36v1.51a33.49,33.49,0,0,0-4.66-.42,18.69,18.69,0,0,0-4.55.37,2.7,2.7,0,0,0-1.81,1.93,18.81,18.81,0,0,0-.53,5.43,23.5,23.5,0,0,0,.39,5.13,3,3,0,0,0,1.59,2.09,10.15,10.15,0,0,0,4,.55c.56,0,1.25,0,2.07-.08s1.47-.12,2-.19v-6.41Z"/><path class="cls-5" d="M61.91,134.32A3.33,3.33,0,0,1,60,131.86a31.94,31.94,0,0,1-.41-6.19,31.89,31.89,0,0,1,.41-6.18A3.33,3.33,0,0,1,61.91,117a13.45,13.45,0,0,1,5.07-.67A13.42,13.42,0,0,1,72,117a3.33,3.33,0,0,1,1.89,2.47,31.89,31.89,0,0,1,.41,6.18,31.94,31.94,0,0,1-.41,6.19A3.33,3.33,0,0,1,72,134.32,13.38,13.38,0,0,1,67,135,13.4,13.4,0,0,1,61.91,134.32Zm9-1.29a2.34,2.34,0,0,0,1.37-1.86,29.55,29.55,0,0,0,.34-5.5,29.7,29.7,0,0,0-.34-5.5,2.32,2.32,0,0,0-1.37-1.85,13.69,13.69,0,0,0-4-.38,13.73,13.73,0,0,0-4,.38,2.32,2.32,0,0,0-1.37,1.85,28.82,28.82,0,0,0-.34,5.5,28.68,28.68,0,0,0,.34,5.5A2.34,2.34,0,0,0,63,133a13.73,13.73,0,0,0,4,.38A13.69,13.69,0,0,0,70.94,133Z"/><path class="cls-5" d="M82.89,134.91c-1.17-.07-2.18-.15-3-.25v-1.59c.91.08,1.93.16,3,.23s2,.11,2.76.11a17.72,17.72,0,0,0,2.32-.12,4,4,0,0,0,1.49-.49,1.67,1.67,0,0,0,.81-1.07,7.68,7.68,0,0,0,.2-1.92,9.6,9.6,0,0,0-.15-2.07,1.14,1.14,0,0,0-.71-.83,7.63,7.63,0,0,0-2-.42L83,126a4.91,4.91,0,0,1-2.42-.74,2.61,2.61,0,0,1-1-1.47,10.4,10.4,0,0,1-.23-2.45,5.81,5.81,0,0,1,.71-3.17,3.47,3.47,0,0,1,2-1.44,14.24,14.24,0,0,1,3.66-.37c1,0,2,0,3.09.1a19.49,19.49,0,0,1,2.44.29v1.51c-.7-.08-1.55-.16-2.54-.22s-1.9-.09-2.73-.09a15.54,15.54,0,0,0-2.52.16,4.18,4.18,0,0,0-1.51.5,1.74,1.74,0,0,0-.76,1,6,6,0,0,0-.19,1.72,6.83,6.83,0,0,0,.17,1.76,1.32,1.32,0,0,0,.67.82,4.9,4.9,0,0,0,1.57.39l4.51.47a10.3,10.3,0,0,1,2.69.56,2.23,2.23,0,0,1,1.25,1.43,9.9,9.9,0,0,1,.3,2.91,6.84,6.84,0,0,1-.72,3.54,3.17,3.17,0,0,1-2,1.47,14.17,14.17,0,0,1-3.38.32C85.14,135,84.05,135,82.89,134.91Z"/><path class="cls-5" d="M47.08,49.62c1.17-.36,8.61.83,9.61,1.53,3.13,2.21,7.71,3.41,9.55,1.15s-.08-6.13-3-9.1c-.88-.88-3.7-7.75-3.56-9,.42-3.87-.85-8.5-3.73-9s-5.61,3.66-6.3,7.16c-.24,1.22-5,7.11-6.14,7.62-3.48,1.62-6.77,5-5.73,7.69S43.7,50.65,47.08,49.62Z"/><path class="cls-5" d="M60,9.61c4.68.2,6.65,9.72,5.89,17.18-.24,2.37,5.27,15.71,6.24,17.91a7.29,7.29,0,0,0,3,3C71.63,38.07,71.32,30.32,72,28.51c3-8.4,1.71-19.69-2.85-22.32-3.07-1.77-9.79-2.48-14.07,4.56A6.91,6.91,0,0,1,60,9.61Z"/><path class="cls-5" d="M22.61,50.2C20.Line truncated

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,9 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#e3493c">
<meta name="apple-mobile-web-app-title" content="Advanced Algos">
<meta name="application-name" content="Advanced Algos">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1365.333" height="1365.333" viewBox="0 0 1024.000000 1024.000000"><path d="M685.9 27.4c-.2.2-3 .7-6.1 1-12.6 1.5-34.6 8.5-45.2 14.5-2 1.2-5.3 3.1-7.3 4.2-14.3 8.1-30.2 22.3-43.5 38.8-5.9 7.4-16.2 22.1-15.5 22.1.2 0 3.6-1.7 7.5-3.9 17.4-9.6 28.8-12.1 52.2-11.7 13.4.3 17.7.8 23.1 2.5 25.1 8 45.2 36.6 58.9 83.7 1.8 6 3.3 11.6 3.5 12.4 2.3 10.8 3.5 17.1 4 20 .3 1.9 1 6 1.5 9 1.2 7.3 1.8 11.3 2.5 18.5.4 3.3.8 7.6 1.1 9.5 1.6 14.3 2.2 51.4 1 69.5-1.4 21.3-1.5 26-.5 31 .5 2.7 1.2 6.3 1.5 8 2.3 12.9 20.6 69 33.4 102.5 1.6 4.1 4.8 12.4 7.1 18.5 17.1 45.2 47.3 118.6 49.1 119.3.4.2.8.9.8 1.5.1 3.8 18.3 22 29.2 29.1 4.7 3.1 8.8 5.6 9 5.6.3 0-3.2-10.9-7.7-24.3-4.5-13.3-8.4-25.3-8.7-26.7-.4-1.4-2-7.2-3.7-13-4.3-14.5-12.2-45.3-13.7-54-.3-1.4-.7-3.3-1-4.2-.3-1-.7-3-1-4.5-.2-1.6-.9-4.8-1.4-7.3-.5-2.5-1.4-7.2-2-10.5-.5-3.3-1.2-7.1-1.5-8.5-.3-1.4-1-5.7-1.5-9.5-.6-3.9-1.2-8.1-1.5-9.5-.3-2.3-.7-5.4-2-16.3-3.1-25.8-3.8-58.6-1.5-73.2.7-4.4 2.2-10.7 3.5-14 2.3-6.1 9-28.9 9.9-33.5.3-1.4 1.2-5.2 2-8.5s1.7-7.4 1.9-9c.3-1.7 1.4-8.2 2.6-14.6 1.1-6.3 2.3-14.7 2.6-18.5.4-3.8.9-8.3 1.1-9.9 2.2-16.2 2.2-69.4 0-83.5-.2-1.4-.7-5-1.1-8-6.7-56.6-26.4-102.4-53.1-123.1-11.1-8.5-31-16.4-49.9-19.7-9.6-1.7-38.4-2.9-39.6-1.8zM465 215.4c-2.3 3-9.7 11.7-14.5 17-1.1 1.2-5.6 6.2-10 11.1-8.3 9.3-43.9 45.2-53.5 54-51.1 46.9-108.8 86-132.5 90-2.2.4-4.4.8-5 .9-.5.2-2.8.6-5 1.1-24.5 4.7-70 20.4-91 31.3-2.7 1.4-5.2 2.6-5.5 2.7-.7.1-12.3 6.2-16.7 8.7-2.1 1.2-5.4 3.1-7.5 4.2C62.4 471.4 16.3 519 4.3 559.8.9 571.3.5 590 3.4 603.5c2.6 12.1 7.2 25.6 11.8 35 19.6 39.1 52.7 65.8 95.3 76.8 5.5 1.4 12 2.9 14.5 3.2 2.5.4 5.4.9 6.5 1.1 2.8.5 11.7 1.4 19.5 1.9l6.4.4-3.4-2.1c-20.8-12.5-29.7-21.3-39-38.3-1.8-3.3-4.3-7.8-5.5-10-6.1-11.2-8.2-22.8-6.4-35.1 7.4-48 72.3-111.2 159.4-155 10.5-5.3 21-11.2 23.5-13.1 8.7-6.7 26.9-25.3 45.4-46.3 1.7-2 4.9-5.6 7.1-8.1 2.2-2.4 6-6.9 8.5-9.8 2.5-3 5-5.9 5.5-6.5.6-.6 3-3.6 5.5-6.6 6.5-8 7.1-8.6 15.2-18.5 12.7-15.4 51.5-64.5 61.3-77.6 1.1-1.5 5.9-7.8 10.6-14 11.5-15.2 13.3-18.1 16.8-26.9 3-7.7 3.5-9.3 5.1-19 1.2-7.1 1.9-21.9 1.1-22.4-.3-.2-1.7 1.1-3.1 2.8zM563.9 315c-21.5 5.4-43.9 28.6-59.4 61.3-5.2 11-10.1 24.9-12.5 35.7-2 8.8-6.1 16.2-21 37.4-1.8 2.6-17.8 23.2-20.7 26.6-14.7 17.6-22.4 26.2-31.2 35.1-8.7 8.6-12.2 11.3-19.6 15.2-16.7 8.7-25.6 14.5-37 23.8-22.2 18.1-36.8 38.2-41 56.4-1.6 6.6-2.2 17.6-1.1 19.3.3.5.7 2.1 1 3.7.8 4.3 5.3 12.7 9.1 16.8 10.1 11.1 27.7 18.5 50.5 21.3 12.2 1.5 36.1.8 49-1.6 11.6-2 12.6-2.3 20.5-4.5s31.7-2.2 43.5 0c.8.2 4.4.6 8 1 3.6.4 7.4.9 8.4 1.1 1.1.2 4.2.6 6.9.9 2.8.3 5.3.8 5.6.9.3.2 2.9.7 5.6 1 9.1 1.3 39 7.5 47.9 10.1 8.9 2.6 18.5 7 20.6 9.6 1.9 2.3 30.1 16.5 39.5 20 9.1 3.3 26.6 7.7 33.7 8.6 9.2 1.1 23.9.7 30-.7 19.5-4.6 33.3-18.5 36.3-36.7.7-4.2.8-19 .1-20.1-.2-.4-.7-2.4-1-4.6-1-6.1-5.5-18.3-10.3-28.1-4.2-8.3-15.4-26.1-20.1-31.6-5.8-7-14.5-17-18.6-21.4-2.6-2.8-6.5-8-8.7-11.7-6.3-10.8-17.6-35.1-25.4-55.3-1.5-3.9-3.3-8.4-4-10-1.4-3.5-4.9-12.8-5-13.5 0-.3-1.2-3.7-2.5-7.5-1.3-3.9-2.5-7.5-2.6-8-.1-.6-.9-3.5-1.8-6.6-2.6-8.9-2.8-10-2.6-17.4.7-25.5.3-39.3-1.3-49.5-.3-1.4-.8-4.5-1.2-7-.3-2.4-.8-4.7-1-5.1-.2-.3-.7-2.1-1-4-.9-5.4-6.3-20.6-9.7-27.3-7.5-14.9-16.6-24.9-27.5-30.3-6.3-3.1-8.3-3.6-16-3.9-4.8-.2-10.4.1-12.4.6zM340 752.1c-12.3 2.1-18.9 4.4-31.5 10.7-10.5 5.3-11.8 4.4 13 9.2 69 13.5 135.5 34.9 186.8 60.2 24.5 12.1 41 22.8 49.7 32.3 24.3 26.6 37.9 39.4 60 56.6 50.3 39.2 116.2 69.8 162.5 75.5 2.8.3 6.8.9 9 1.3 4.8.9 28.3.6 34-.3 23.2-4 37.1-11.8 56.6-31.7 33-33.8 46.8-81.2 37.4-128.4-3.2-16-8-29.9-16.2-46.5l-4.2-8.5-.1 8c-.1 4.4-.3 8.7-.4 9.5-.2.8-.6 3.8-1 6.6-1.8 14.4-15.1 40.8-26.4 52.7-17.8 18.7-53.6 23.1-99.2 12.2-5.8-1.4-11.5-2.7-12.6-3-1.2-.3-7.7-2.3-14.5-4.6-36.3-12-78-32.8-111.4-55.7-6-4.1-13-8.4-15.5-9.4-2.5-1.1-5.4-2.3-6.5-2.8-5.6-2.4-39.7-11.2-52.5-13.5-1.9-.3-4.2-.7-5-.9-.8-.1-4-.8-7-1.4-9.8-2-26.4-5.1-30-5.6-3-.5-29.1-4.8-37-6.1-1.4-.2-4.3-.7-6.5-1-6.9-1.1-9.8-1.5-15.5-2.5-3-.5-8.2-1.2-11.5-1.6-3.3-.3-6.2-.7-6.5-.9-.3-.2-4.4-.9-9-1.5-4.7-.6-9.6-1.3-11-1.5-1.4-.2-6.3-.9-11-1.5s-9.6-1.3-11-1.5c-5.9-.8-10.8-1.5-15-2-5.1-.6-9.4-1.2-16-2.1-7.1-.9-20.1-1.1-25-.3z"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,19 @@
{
"name": "Advanced Algos",
"short_name": "Advanced Algos",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+217
View File
@@ -0,0 +1,217 @@
import Auth0Lock from 'auth0-lock'
import gql from 'graphql-tag'
import { getItem, setItem, removeItem } from '../utils/local-storage'
import { validObject, deleteCookie } from '../utils/js-helpers'
import { actions } from '../actions'
import { client } from '../index'
export const AUTH_CONFIG = {
api_audience: process.env.AUTH0_AUDIENCE,
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENTID,
callbackUrl: process.env.AUTH0_CALLBACK_URL
}
console.log(process.env.AUTH0_CLIENTID)
const AUTHENTICATE = gql`
mutation authenticate($idToken: String!) {
authenticate(idToken: $idToken) {
id
nickname
auth0id
profile {
email
avatar
}
}
}
`
const lock = new Auth0Lock(AUTH_CONFIG.clientId, AUTH_CONFIG.domain, {
oidcConformant: true,
autoclose: true,
auth: {
sso: true,
redirectUrl: window.location.origin + '/callback',
responseType: 'token id_token',
audience: `${AUTH_CONFIG.api_audience}`,
params: {
scope: `openid profile email user_metadata app_metadata picture`
}
}
})
class Auth {
constructor (cb, apolloClient) {
this.handleAuthentication()
// binds functions to keep this context
this.apolloClient = apolloClient
this.cb = cb.bind(this)
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
this.isAuthenticated = this.isAuthenticated.bind(this)
this.checkSession = this.checkSession.bind(this)
}
login () {
// Call the show method to display the widget.
lock.show()
}
handleAuthentication () {
// Add a callback for Lock's `authenticated` event
lock.on('authenticated', this.setSession.bind(this))
// Add a callback for Lock's `authorization_error` event
lock.on('authorization_error', err => {
console.log(err)
const data = { status: `error`, errMessage: err.error }
this.cb(data)
})
}
checkSession () {
return new Promise((resolve, reject) => {
// Add a callback for Lock's `authenticated` event
lock.checkSession(
{
responseType: 'token id_token',
audience: AUTH_CONFIG.api_audience,
scope:
'openid email profile read:teams write:teams read:member write:member',
connection: 'github',
prompt: 'none'
},
function (err, authResult) {
if (!err) {
resolve(authResult)
} else {
reject(err)
}
}
)
})
}
setSession (authResult) {
console.log('setSession')
if (authResult && authResult.accessToken && authResult.idToken) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
)
setItem('access_token', authResult.accessToken)
setItem('id_token', authResult.idToken)
setItem('expires_at', expiresAt)
const data = {
status: `success`,
accessToken: authResult.accessToken,
idToken: authResult.idToken,
expiresAt
}
console.log('setSession idTokenPayload: ', authResult.idTokenPayload)
this.signinOrCreateAccount({ ...data })
this.cb(data)
console.log(authResult.idTokenPayload)
setItem('user', JSON.stringify(authResult.idTokenPayload))
if (window.location.href.includes(`callback`)) {
window.location.href = '/dashboard'
}
return true
}
}
async signinOrCreateAccount ({ accessToken, idToken, expiresAt }) {
console.log(
'signinOrCreateAccount: ',
client,
this.apolloClient,
accessToken,
idToken,
expiresAt
)
try {
const data = await client.mutate({
mutation: AUTHENTICATE,
variables: { idToken }
})
console.log('signinOrCreateAccount auth: ', await data)
setItem('user', data.data.authenticate.auth0id)
if (window.location.href.includes(`callback`)) {
window.location.href = '/'
} else {
window.location.reload()
}
return data
} catch (err) {
return console.log('Sign in or create account error: ', err)
}
}
logout () {
// Clear access token and ID token from local storage
removeItem('access_token')
removeItem('id_token')
removeItem('expires_at')
removeItem('user')
removeItem('auth0.ssodata')
window.localStorage.clear()
deleteCookie('ajs_anonymous_id')
deleteCookie('ajs_user_id')
deleteCookie('current_tenant')
actions.setLoggedOut()
lock.logout({ returnTo: 'http://0.0.0.0:5000' })
console.log('logout')
}
async isAuthenticated () {
// check session and run Auth0 SS0
const getUser = await getItem('user')
let user = JSON.parse(getUser)
if (validObject(user, 'exp') && new Date().getTime() < (user.exp * 1000)) {
console.log('handleAuth.user exp: ', user, window.location.href)
actions.setLoggedIn(user)
return user
}
console.log('handleAuth.user: ', user, actions, window.location.href)
const checkSSO = await this.checkSession()
.then(result => {
console.log('handleAuth.checksessions: ', result)
setItem('user', result.idTokenPayload.sub)
// user confirmed, log into client
this.setSession(result)
return result.idTokenPayload
})
.catch(err => {
console.log('handleAuth.checksessions err: ', err)
if (err.error === 'login_required') {
// return this.login()
}
return err
})
if (
(window.location.href.includes(`manage`) ||
window.location.href.includes(`profile`)) &&
!user
) {
window.location.href = '/'
}
if (/manage|profile|create|dashboard/.test(window.location.href) && !user) {
this.login()
}
console.log('handleAuth.isAuthenticated 2: ', checkSSO, user)
if (validObject(checkSSO, 'error')) {
return false
} else {
return checkSSO
}
}
}
export default Auth
+143
View File
@@ -0,0 +1,143 @@
/* eslint-disable no-unused-vars */
import { app } from 'hyperapp'
import { location } from '@hyperapp/router'
import { withFx } from '@hyperapp/fx'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink, concat, split, Observable } from 'apollo-link'
import { onError } from 'apollo-link-error'
import { WebSocketLink } from 'apollo-link-ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { getMainDefinition } from 'apollo-utilities'
import Auth from './auth'
import log from '../tools/log'
import { getItem, setItem, removeItem } from './utils/local-storage'
import { actions } from './actions/'
import { state } from './state/'
import { view } from './views/'
import css from './styles/app.scss'
const graphqlEndpoint =
process.env.NODE_ENV === 'production'
? process.env.PROD_GRAPHQL
: process.env.DEV_GRAPHQL
const wsUri = graphqlEndpoint.replace(/^http/, 'ws')
const globalVar =
typeof global !== 'undefined'
? global
: typeof window !== 'undefined'
? window
: {}
const webSocketImpl = globalVar.WebSocket || globalVar.MozWebSocket
const wsClient = new SubscriptionClient(
wsUri,
{
reconnect: true
},
webSocketImpl
)
wsClient.onDisconnected(() => {
// console.log('onDisconnected')
})
wsClient.onReconnected(() => {
// console.log('onReconnected')
})
const httpLink = new HttpLink({ uri: graphqlEndpoint })
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
new WebSocketLink(wsClient),
httpLink
)
const authLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
// User access token has expired
console.log('authLink: ', graphQLErrors)
if (graphQLErrors[0].message === 'Not logged in') {
// We assume we have both tokens needed to run the async request
// Let's refresh token through async request
return new Observable(observer => {
getItem('access_token')
.then(accessToken => {
operation.setContext(({ headers = {} }) => ({
headers: {
// Re-add old headers
...headers,
// Switch out old access token for new one
Authorization: `Bearer ${accessToken}` || null
}
}))
})
.then(() => {
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
}
// Retry last failed request
forward(operation).subscribe(subscriber)
})
.catch(error => {
// No refresh or client token available, we force user to login
observer.error(error)
})
})
}
}
})
const linkWithAuth = concat(authLink, link)
let apolloClient
const cache = new InMemoryCache().restore(window.__APOLLO_STATE__)
export const client = new ApolloClient({
link: linkWithAuth,
cache,
connectToDevTools: true
})
export const auth = new Auth(result => console.log('auth result', result), client)
let frontendReloadCount = 0
export const main = withFx(app)(
state,
actions,
view,
document.getElementById('root')
)
location.subscribe(main.location)
if (process.env.NODE_ENV === 'development') {
if (module.hot) {
module.hot.accept()
module.hot.accept('./index', () => {
try {
log.debug('Updating front-end')
frontendReloadCount = (frontendReloadCount || 0) + 1
} catch (err) {
log(err.stack)
}
})
}
}
@@ -0,0 +1,233 @@
import { action, throttle } from '@hyperapp/fx'
import { UniqueAlgobotnameQuery } from '../graphql/UniqueAlgobotnameQuery'
import { AddAlgobotMutation } from '../graphql/AddAlgobotMutation'
import { GetAlgobotQuery } from '../graphql/GetAlgobotQuery'
import { DeleteAlgobotMutation } from '../graphql/DeleteAlgobotMutation'
import { client } from '../../../index'
import { mergeDeep } from '../../../utils/js-helpers'
export const algobot = {
form: ({ form, message }) => state => {
return {
form: {
form,
message
}
}
},
clearForms: () => ({
form: {
form: 'login',
message: ''
},
createAlgobot: {
input: '',
unique: false,
placeholder: 'Your algobotname'
}
}),
checkAlgobotname: value => [
action('throttleAlgobotname', value),
action('createAlgobot.input', value)
],
throttleAlgobotname: value => throttle(750, 'uniqueAlgobotname', value),
uniqueAlgobotname: value => async (state, actions) => {
const result = await client.query({
query: UniqueAlgobotnameQuery,
variables: { algobotname: value }
})
return actions.createAlgobot.unique(result.data.uniqueAlgobotname)
},
createAlgobot: {
input: value => state => {
const algobotnameInput = {
input: value
}
return mergeDeep(algobotnameInput, state.createAlgobot)
},
unique: value => state => {
const algobotnameInput = {
unique: value
}
return mergeDeep(algobotnameInput, state.createAlgobot)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.createAlgobot)
}
},
profile: profile => (state, actions) => {
return {
profile
}
},
addAlgobot: result => [
action('form', {
form: 'addAlgobotNotice',
message: `Adding your algobot...`
}),
action('addAlgobotSubmit', result)
],
addAlgobotSubmit: algobotForm => async (state, actions) => {
algobotForm.preventDefault()
algobotForm.target.blur()
const algobotname = algobotForm.target.form[0].value
const slug = await algobotname.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()
const validateSlug = new RegExp('^[a-z0-9-]+$')
const valid = await validateSlug.test(slug)
await console.log('actions.algobot.addAlgobot: ', valid, slug)
if (!valid) {
return actions.form({
form: 'addAlgobotError',
message:
'Algobot name not valid. Please only letters, numbers and spaces'
})
}
const teamId = algobotForm.target.form[2].value
if (teamId === '') {
return actions.form({
form: 'addAlgobotError',
message: 'Please create a team first!'
})
}
const algobot = {
name: algobotname,
slug: slug,
owner: algobotForm.target.form[1].value,
team_id: teamId
}
const uniqueAlgobot = await client.query({
query: UniqueAlgobotnameQuery,
variables: { algobotname: algobotname }
})
await console.log(
'actions.algobot.addAlgobot: ',
algobot,
valid,
uniqueAlgobot,
uniqueAlgobot.data.uniqueAlgobotname
)
if (!uniqueAlgobot.data.uniqueAlgobotname) {
return actions.form({
form: 'addAlgobotError',
message: 'Sorry, this algobot name is taken. Please create a new one'
})
}
const result = await client.mutate({
mutation: AddAlgobotMutation,
variables: { input: algobot },
options: {
update: (proxy, { data: { getAlgobotByOwner } }) => {
const data = proxy.readQuery({ GetAlgobotQuery })
data.getAlgobotByOwner.push(getAlgobotByOwner)
proxy.writeQuery({ GetAlgobotQuery, data })
}
}
})
if (result.data.addAlgobot !== null) {
return actions.addAlgobotSuccess(result.data.addAlgobot)
} else {
return actions.form({
form: 'addAlgobotError',
message: `Unable to add Algobot: ${
result.data.addAlgobot.errors[0].message
}`
})
}
},
addAlgobotSuccess: result => [
action('form', { form: 'addAlgobotSuccess', message: 'Algobot Added!' }),
action('profile', result),
action('createAlgobot.input', ''),
action('toggleModal', '')
],
toggleModal: form => (state, actions) => {
return {
modal: {
content: form,
isActive: !state.modal.isActive
}
}
},
getAlgobots: id => [
action('form', {
form: 'getAlgobotLoad',
message: 'Getting your algobot...'
}),
action('getAlgobotbyOwner', id)
],
getAlgobotbyOwner: id => async (state, actions) => {
/*
const result = await client.query({
query: GetAlgobotQuery,
variables: { owner: id },
fetchPolicy: 'network-only'
})
if (result.data.getAlgobotByOwner !== null) {
return actions.getAlgobotSuccess(result.data.getAlgobotByOwner)
} else {
return actions.getAlgobotNoAlgobot()
} */
return actions.getAlgobotNoAlgobot()
},
getAlgobotSuccess: profile => [
action('form', { form: 'getAlgobotSuccess', message: 'Your algobot' }),
action('profile', profile)
],
getAlgobotNoAlgobot: profile => [
action('form', { form: 'getAlgobotNotice', message: 'No algobot found' }),
action('profile', { name: '' })
],
deleteAlgobot: id => [
action('form', {
form: 'deleteAlgobotLoad',
message: 'Removing your algobot...'
}),
action('deleteAlgobotSubmit', id)
],
deleteAlgobotSubmit: id => async (state, actions) => {
const result = await client.mutate({
mutation: DeleteAlgobotMutation,
variables: { id: Number(id) },
options: {
update: (proxy, { data: { getAlgobotByOwner } }) => {
const data = proxy.readQuery({ GetAlgobotQuery })
data.getAlgobotByOwner.push(getAlgobotByOwner)
proxy.writeQuery({ GetAlgobotQuery, data })
}
}
})
if (result.data.deleteAlgobot.id !== null) {
return actions.deleteAlgobotSuccess({ name: '' })
} else {
return actions.form({
form: 'deleteAlgobotError',
message: 'No algobot found'
})
}
},
deleteAlgobotSuccess: profile => [
action('form', {
form: 'deleteAlgobotSuccess',
message: 'Your algobot has been deleted'
}),
action('profile', profile)
]
}
export default algobot
@@ -0,0 +1,17 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const AddAlgobotMutation = gql`
mutation addAlgobot($input: AddAlgobotInput!) {
addAlgobot(input: $input) {
id
name
slug
owner
avatar
repo
team_id
type
}
}
`
@@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const DeleteAlgobotMutation = gql`
mutation deleteAlgobot($id: Int!) {
deleteAlgobot(id: $id) {
id
}
}
`
@@ -0,0 +1,19 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const GetAlgobotQuery = gql`
query getAlgobotByOwner($owner: Int!) {
getAlgobotByOwner(owner: $owner) {
id
name
slug
owner
avatar
repo
team_id
type
}
}
`
export default GetAlgobotQuery
@@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const UniqueAlgobotnameQuery = gql`
query uniqueAlgobotname($algobotname: String!) {
uniqueAlgobotname(algobotname: $algobotname)
}
`
export default UniqueAlgobotnameQuery
Whitespace-only changes.
@@ -0,0 +1,28 @@
export const algobot = {
form: {
type: 'login',
message: ''
},
createAlgobot: {
input: '',
unique: true,
placeholder: 'Your algobot name',
error: ''
},
profile: {
id: '',
name: '',
slug: '',
owner: '',
avatar: '',
repo: '',
team_id: ''
},
members: [],
modal: {
content: '',
isActive: false
}
}
export default algobot
@@ -0,0 +1,90 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
export const AddAlgobotForm = props => (state, actions) => (
<form name='addAlgobotForm' key='AddAlgobotForm'>
<h1 class='title'>Add An Algobot</h1>
<div class='field'>
<label class='label'>Enter a algobot name</label>
<div class='control has-icons-left has-icons-right'>
<input
class='input'
type='text'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.algobot.createAlgobot.placeholder}
oninput={({ target: { value } }) =>
actions.algobot.checkAlgobotname(value)
}
onblur={({ target: { value } }) =>
actions.algobot.checkAlgobotname(value)
}
value={state.algobot.createAlgobot.input}
placeholder={state.algobot.createAlgobot.placeholder}
/>
<span
class={`icon is-small is-right ${
state.algobot.createAlgobot.unique.input !== '' &&
state.algobot.createAlgobot.unique
? 'has-text-success'
: 'has-text-grey'
}`}
>
<i class='fas fa-check' />
</span>
</div>
</div>
<p
key='createAlgobotUniqueMessage'
class={`has-text-centered ${
!state.algobot.createAlgobot.unique ? 'has-text-danger' : 'is-invisible'
}`}
>
This algobotname is already taken. Please create a new one.
</p>
<p
key='addAlgobotMessage'
class={`help is-size-4 m-t-hlf m-b-hlf
${
state.algobot.form.form === 'addAlgobotSuccess'
? 'has-text-success'
: ''
}
${
state.algobot.form.form === 'addAlgobotNotice'
? 'has-text-primary'
: ''
}
${
state.algobot.form.form === 'addAlgobotError' ? 'has-text-danger' : ''
}
${
state.algobot.form.form === 'addAlgobotSuccess' ||
state.algobot.form.form === 'addAlgobotNotice' ||
state.algobot.form.form === 'addAlgobotError'
? ''
: 'is-invisible'
}`}
>
{state.algobot.form.message}
</p>
<div class='control buttons is-centered'>
<input type='hidden' name='owner' value={state.user.profile.id} />
<input type='hidden' name='team_id' value={state.team.profile.id} />
<button
onclick={e => actions.algobot.addAlgobot(e)}
class='button is-link'
>
Create Algobot
</button>
</div>
</form>
)
export default AddAlgobotForm
@@ -0,0 +1,164 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
import { validObject } from '../../../utils/js-helpers'
export const AlgobotList = props => (state, actions) => {
const algobot = state.algobot.profile
console.log(state.algobot.form.form, state.algobot.form.message, algobot.name)
if (
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'No algobot found' &&
algobot.name === '' &&
state.team.profile.name === ''
) {
return (
<div class='section' key='algobotList1'>
<p class='is-size-4'>
You don't have a team yet! Please create one before adding an algobot.
</p>
<div class='level m-t-2'>
<div class='level-item'>
<Link to='/team' class='button navbar-item'>
Go to Team Overview &rarr;
</Link>
</div>
</div>
</div>
)
}
if (
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'No algobot found' &&
algobot.name === ''
) {
return (
<div class='section' key='algobotList2'>
<p class='is-size-4'>Algobot creation currently disabled</p>
<div class='level m-t-2'>
<div class='level-item'>
<a
class='button is-primary is-medium'
disabled={true}
onclick={() => actions.algobot.toggleModal('AddAlgobot')}
>
Create an algobot
</a>
</div>
</div>
</div>
)
}
if (
(algobot.name !== '' && algobot.name !== null) ||
state.algobot.form.form === 'deleteAlgobotSuccess'
) {
const initial = algobot.name.substring(0, 1)
return (
<div class='section' key='algobotList3'>
<p
class={`is-size-5 card-footer-item
${
state.algobot.form.form === 'deleteAlgobotSuccess'
? 'has-text-success'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotNotice'
? 'has-text-primary'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotError'
? 'has-text-danger'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotSuccess' ||
state.algobot.form.form === 'deleteAlgobotNotice' ||
state.algobot.form.form === 'deleteAlgobotError'
? ''
: 'is-invisible'
}`}
>
{state.algobot.form.message}
</p>
<table class='table is-fullwidth is-striped'>
<thead>
<tr>
<th>&nbsp;</th>
<th>Algobot Name</th>
<th>Team</th>
<th>Type</th>
<th>Owner</th>
<th>Repo</th>
<th class='has-text-right'>Actions</th>
</tr>
</thead>
<tbody>
{algobot.name !== '' ? (
<tr>
<td>
{algobot.avatar !== null && algobot.avatar !== '' ? (
<div class='image is-16x16 avatar'>
<img src={algobot.avatar} alt={algobot.name} />
</div>
) : (
<div class='image is-16x16 avatar has-text-white is-uppercase is-green is-size-7 is-centered'>
{initial}
</div>
)}
</td>
<td>
<strong>{algobot.name}</strong>
</td>
<td>{state.team.profile.name}</td>
<td />
<td>{state.user.profile.username}</td>
<td />
<td class='has-text-right'>
<button
onclick={e =>
actions.algobot.deleteAlgobot(
state.algobot.profile.id,
state.algobot.profile.owner
)
}
class='button is-small is-red'
disabled={
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'Removing your algobot...'
}
>
<span class='icon'>
<i class='fas fa-trash-alt' />
</span>
</button>
</td>
</tr>
) : (
''
)}
</tbody>
</table>
</div>
)
}
return (
<div class='section' key='algobotList4'>
<p class='is-size-4'>{state.algobot.form.message}</p>
<div class='level m-t-2'>
<div class='level-item'>
<span class='icon has-text-primary is-large'>
<i class='fas fa-spinner fa-pulse' />
</span>
</div>
</div>
</div>
)
}
export default AlgobotList
@@ -0,0 +1,121 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
import { validObject } from '../../../utils/js-helpers'
export const AlgobotPanel = props => (state, actions) => {
const algobot = validObject(state.algobot.profile, 'edges') ? state.team.profile.edges : ''
if (algobot === '') {
return (
<div
class='section'
key='algobotList1'
class='is-marginless is-paddingless'
>
<h2 class='title is-size-4 m-t-1'>Algobots</h2>
<p class='is-size-5'>
Create a team first,<br />then setup your algobot!
</p>
</div>
)
}
if (
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'No algobot found' &&
algobot.name === ''
) {
return (
<div
class='section'
key='algobotList2'
class='is-marginless is-paddingless p-l-1 p-r-1'
>
<h2 class='title is-size-4 m-t-1'>Algobots</h2>
<p class='is-size-5'>Algobot creation currently disabled</p>
<div class='level m-t-2'>
<div class='level-item'>
<a
class='button is-primary is-medium'
disabled={true}
onclick={() => actions.algobot.toggleModal('AddAlgobot')}
>
Create an algobot
</a>
</div>
</div>
</div>
)
}
if (
(algobot.name !== '' && algobot.name !== null) ||
state.algobot.form.form === 'deleteAlgobotSuccess'
) {
const initial = algobot.name.substring(0, 1)
return (
<div
class='section'
key='algobotList3'
class='is-marginless is-paddingless'
>
<h2 class='title is-size-4 m-t-1'>Algobots</h2>
<table class='table is-fullwidth is-striped is-narrow'>
<thead>
<tr>
<th>&nbsp;</th>
<th>Name</th>
<th>Team</th>
<th>Owner</th>
</tr>
</thead>
<tbody>
{algobot.name !== '' ? (
<tr>
<td>
{algobot.avatar !== null && algobot.avatar !== '' ? (
<div class='image is-16x16 avatar'>
<img src={algobot.avatar} alt={algobot.name} />
</div>
) : (
<div class='image is-16x16 avatar has-text-white is-uppercase is-green is-size-7 is-centered'>
{initial}
</div>
)}
</td>
<td class='is-size-6'>
<strong>{algobot.name}</strong>
</td>
<td class='is-size-6'>{state.team.profile.name}</td>
<td class='is-size-6'>{state.user.profile.username}</td>
</tr>
) : (
''
)}
</tbody>
</table>
</div>
)
}
return (
<div
class='section'
key='algobotList4'
class='is-marginless is-paddingless'
>
<h2 class='title is-size-4 m-t-1'>Algobots</h2>
<p class='is-size-4'>{state.algobot.form.message}</p>
<div class='level m-t-2'>
<div class='level-item'>
<span class='icon has-text-primary is-large'>
<i class='fas fa-spinner fa-pulse' />
</span>
</div>
</div>
</div>
)
}
export default AlgobotPanel
@@ -0,0 +1,5 @@
import { AddAlgobotForm } from './add-algobot'
import { AlgobotList } from './algobot-list'
import { AlgobotPanel } from './algobot-panel'
export { AddAlgobotForm, AlgobotList, AlgobotPanel }
@@ -0,0 +1,569 @@
import { action, throttle, delay } from '@hyperapp/fx'
import { client } from '../../../index'
import { UniqueUsernameQuery } from '../graphql/UniqueUsernameQuery'
import { RegisterSubmitMutation } from '../graphql/RegisterSubmit'
import { VerifyEmailMutation } from '../graphql/VerifyEmail'
import { FinishRegisterMutation } from '../graphql/FinishRegisterMutation'
import { ForgotPassSubmitMutation } from '../graphql/ForgotPass'
import { ResetPassSubmitMutation } from '../graphql/ResetPass'
import { mergeDeep } from '../../../utils/js-helpers'
const emailRegex = new RegExp(
"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
)
export const auth = {
form: ({ form, message }) => state => {
return {
form: {
form,
message
}
}
},
clearForms: () => ({
form: {
form: 'login',
message: ''
},
username: {
input: '',
placeholder: 'Your username or email',
valid: false
},
createUsername: {
input: '',
placeholder: 'Create a username',
valid: false,
unique: false
},
email: {
input: '',
placeholder: 'Your email address',
valid: false
},
registerEmail: {
input: ''
},
password: {
input: '',
placeholder: 'Your password',
valid: false
},
createPassword: {
input: '',
placeholder: 'Your new password',
secure: false,
valid: false
},
confirmPassword: {
input: '',
placeholder: 'Confirm your password',
matches: false,
valid: false
},
forgotEmail: {
input: '',
placeholder: 'Enter your email address',
valid: false
},
resetPassword: {
input: '',
placeholder: 'Set a new password',
strength: '',
valid: false
},
confirmResetPassword: {
input: '',
placeholder: 'Confirm your new password',
matches: false,
valid: false
},
finishRegister: {
load: false,
error: false,
message: '',
success: false
}
}),
username: {
input: value => state => {
const usernameInput = {
input: value
}
return mergeDeep(usernameInput, state.username)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.username)
}
},
email: {
input: value => state => {
const emailInput = {
input: value,
valid: emailRegex.test(value)
}
return mergeDeep(emailInput, state.email)
},
sent: value => state => {
const emailInput = {
input: value
}
return mergeDeep(emailInput, state.email)
}
},
registerEmail: {
input: value => state => {
const emailInput = {
input: value
}
return mergeDeep(emailInput, state.registerEmail)
}
},
password: {
input: value => state => {
const passwordInput = {
input: value,
valid: value !== ''
}
return mergeDeep(passwordInput, state.password)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.password)
}
},
checkUsername: value => [
action('throttleUsername', value),
action('createUsername.input', value)
],
throttleUsername: value => throttle(750, 'uniqueUsername', value),
uniqueUsername: value => async (state, actions) => {
const result = await client.query({
query: UniqueUsernameQuery,
variables: { username: value }
})
const unique = result.data.uniqueUsername
if (unique) {
return actions.setUsername(unique)
} else {
return actions.setUsernameError(unique)
}
},
setUsername: value => [
action('createUsername.setUnique', value),
action('createUsername.error', '')
],
setUsernameError: value => [
action('createUsername.setUnique', value),
action('createUsername.error', 'This username already exists')
],
createUsername: {
input: value => state => {
const usernameInput = {
input: value
}
return mergeDeep(usernameInput, state.createUsername)
},
setUnique: value => state => {
const usernameInput = {
unique: value
}
return mergeDeep(usernameInput, state.createUsername)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.createUsername)
}
},
createPassword: {
input: value => state => {
let passStrength = 'weak'
const strongRegex = new RegExp(
'^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{10,})'
)
const mediumRegex = new RegExp(
'^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})'
)
if (mediumRegex.test(value)) {
passStrength = 'medium'
}
if (strongRegex.test(value)) {
passStrength = 'strong'
}
const passwordInput = {
input: value,
strength: passStrength
}
return mergeDeep(passwordInput, state.createPassword)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.createPassword)
}
},
confirmPassword: {
input: ({ value, confirm }) => state => {
const passwordInput = {
input: value,
matches: value === confirm
}
return mergeDeep(passwordInput, state.confirmPassword)
}
},
register: registerForm => [
action('form', {
form: 'registerLoad',
message: 'Submitting registration...'
}),
action('registerSubmit', registerForm)
],
registerSubmit: registerForm => async (state, actions) => {
registerForm.preventDefault()
registerForm.target.blur()
const email = registerForm.target.form[0].value
if (!state.email.valid) {
return action('form', {
form: 'registerSignup',
message: 'Please submit a valid email address'
})
}
const result = await client.mutate({
mutation: RegisterSubmitMutation,
variables: { input: { email } }
})
if (
result.data.registerSubmit.user !== '' &&
result.data.registerSubmit.user !== null
) {
return actions.registerSubmitSuccess(result)
} else {
return actions.form({
form: 'registerError',
message: result.data.registerSubmit.errors[0].message
})
}
},
registerSubmitSuccess: result => [
action('form', {
form: 'registerSuccess',
message:
'To continue registration, please check your email for further instructions'
}),
action('email.input', '')
],
verifyRegister: token => [
action('form', { form: 'loading', message: 'Verifying registration...' }),
action('verifyEmail', token)
],
verifyEmail: token => async (state, actions) => {
const result = await client.mutate({
mutation: VerifyEmailMutation,
variables: { token: token }
})
const email = await result.data.verifyEmail.user.email
console.log('actions.auth.verifyEmail: ', email)
if (email !== '' && email !== null) {
return actions.verifySuccess(email)
} else {
return actions.form({
form: 'error',
message: `Could not verify token: ${
result.data.verifyEmail.errors[0].message
}`
})
}
},
verifySuccess: email => [
action('registerEmail.input', email),
action('form', {
form: 'verifySuccess',
message: 'Continue registration...'
})
],
finishRegister: {
load: value => state => {
const newState = {
load: value
}
return mergeDeep(newState, state.finishRegister)
},
error: value => state => {
const newState = {
error: value
}
return mergeDeep(newState, state.finishRegister)
},
message: value => state => {
const newState = {
message: value
}
return mergeDeep(newState, state.finishRegister)
},
success: value => state => {
const newState = {
success: value
}
return mergeDeep(newState, state.finishRegister)
}
},
finish: registerForm => [
action('finishRegister.load', true),
action('finishRegister.message', 'Completing registration...'),
action('finishSubmit', registerForm)
],
finishSubmit: registerForm => async (state, actions) => {
registerForm.preventDefault()
registerForm.target.blur()
const username = registerForm.target.form[0].value
const password = registerForm.target.form[1].value
const confirm = registerForm.target.form[2].value
const email = registerForm.target.form[3].value
console.log(
'actions.auth.finishSubmit: ',
username,
password,
confirm,
email
)
var checkUsername = await client.query({
query: UniqueUsernameQuery,
variables: { username: username }
})
if (!checkUsername) {
actions.finishRegister.load(false)
actions.finishRegister.error(true)
actions.finishRegister.message('Username already exist.')
return false
}
if (password !== confirm) {
actions.finishRegister.load(false)
actions.finishRegister.error(true)
actions.finishRegister.message('Passwords do not match')
actions.confirmPassword.input({ value: confirm, confirm: password })
return false
}
if (checkUsername && password === confirm) {
const result = await client.mutate({
mutation: FinishRegisterMutation,
variables: { input: { username, email, password } }
})
if (result.data.finishRegister.user !== null) {
actions.finishRegister.load(false)
actions.finishRegister.error(false)
actions.finishRegister.success(true)
actions.finishRegister.message(
'Registration complete. Logging you in...'
)
return actions.form({
form: 'finishSuccess',
message: 'Registration complete. Logging you in...'
})
} else {
actions.finishRegister.load(false)
actions.finishRegister.error(true)
actions.finishRegister.message(
`${result.data.finishRegister.errors[0].field}: ${
result.data.finishRegister.errors[0].message
}`
)
actions.confirmPassword.input({ value: confirm, confirm: password })
return false
}
}
return state
},
finishSubmitError: result => [
action('form', { form: 'finishError', message: `Error: ${result}` })
],
checkEmailUsername: value => [
action('throttleUsername', value),
action('createUsername.input', value)
],
throttleEmailUsername: value => throttle(750, 'uniqueUsername', value),
forgotEmail: {
input: value => state => {
const usernameInput = {
input: value
}
return mergeDeep(usernameInput, state.forgotEmail)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.forgotEmail)
},
valid: value => state => {
const emailInput = {
valid: emailRegex.test(value)
}
return mergeDeep(emailInput, state.forgotEmail)
}
},
forgotPass: forgotPassForm => [
action('form', {
form: 'forgotPassLoad',
message: 'Submitting password reset...'
}),
action('forgotPassSubmit', forgotPassForm)
],
forgotPassSubmit: forgotPassForm => async (state, actions) => {
forgotPassForm.preventDefault()
forgotPassForm.target.blur()
const email = forgotPassForm.target.form[0].value
console.log('actions.auth.forgotPassSubmit: ', email)
if (!emailRegex.test(email)) {
return action('form', {
form: 'registerSignup',
message: 'Please submit a valid email address'
})
}
const result = await client.mutate({
mutation: ForgotPassSubmitMutation,
variables: { input: { email: email } }
})
console.log('actions.auth.forgotPassSubmit 2: ', result)
if (
result.data.forgotPassword !== '' &&
result.data.forgotPassword !== null
) {
if (result.data.forgotPassword === 'Success') {
return actions.form({
form: 'forgotPassSuccess',
message: 'Please check your email for further instructions'
})
}
return actions.form({
form: 'forgotPassError',
message: `Failed to reset password: ${result.data.forgotPassword}`
})
}
},
resetPassword: {
input: value => state => {
let passStrength = 'weak'
const strongRegex = new RegExp(
'^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{10,})'
)
const mediumRegex = new RegExp(
'^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})'
)
if (mediumRegex.test(value)) {
passStrength = 'medium'
}
if (strongRegex.test(value)) {
passStrength = 'strong'
}
const passwordInput = {
input: value,
strength: passStrength
}
return mergeDeep(passwordInput, state.resetPassword)
},
error: message => state => {
const errorInput = {
message: message
}
return mergeDeep(errorInput, state.resetPassword)
}
},
confirmResetPassword: {
input: ({ value, confirm }) => state => {
const passwordInput = {
input: value,
matches: value === confirm
}
return mergeDeep(passwordInput, state.confirmResetPassword)
}
},
resetPass: resetPassForm => [
action('form', {
form: 'resetPassLoad',
message: 'Updating your password...'
}),
action('resetPassSubmit', resetPassForm)
],
resetPassSubmit: resetPassForm => async (state, actions) => {
resetPassForm.preventDefault()
resetPassForm.target.blur()
const password = resetPassForm.target.form[0].value
const confirm = resetPassForm.target.form[1].value
const token = resetPassForm.target.form[2].value
console.log(
'actions.auth.resetPassSubmit: ',
password,
confirm,
token,
resetPassForm
)
if (password !== confirm) {
return action('form', {
form: 'resetPassError',
message: 'Passwords do not match'
})
}
const result = await client.mutate({
mutation: ResetPassSubmitMutation,
variables: {
input: {
password: password,
passwordConfirmation: confirm,
token: token
}
}
})
console.log('actions.auth.resetPassSubmit 2: ', result)
if (
result.data.resetPassword !== '' &&
result.data.resetPassword !== null
) {
if (result.data.resetPassword.errors === null) {
actions.form({
form: 'resetPassDone',
message: 'Redirecting to Login...'
})
return actions.delayResetRedirect()
}
return actions.form({
form: 'resetPassError',
message: `Failed to reset password: ${
result.data.resetPassword.errors[0].message
}`
})
}
},
delayResetRedirect: () => delay(3000, 'resetPassSuccess'),
resetPassSuccess: resetPassForm => [
action('form', {
form: 'resetPassSuccess',
message: 'Redirecting to Login...'
})
]
}
export default auth
@@ -0,0 +1,18 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const FinishRegisterMutation = gql`
mutation finishRegister($input: RegisterUserInput!) {
finishRegister(input: $input) {
user {
id
email
username
}
errors {
field
message
}
}
}
`
@@ -0,0 +1,8 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const ForgotPassSubmitMutation = gql`
mutation forgotPassword($input: ForgotPasswordInput!) {
forgotPassword(input: $input)
}
`
@@ -0,0 +1,53 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const RegisterSubmitMutation = gql`
mutation registerSubmit($input: RegisterSubmitInput!) {
registerSubmit(input: $input) {
user {
id
email
}
errors {
field
message
}
}
}
`
export const UserInfo = gql`
fragment UserInfo on User {
id
username
role
isActive
email
profile {
firstName
lastName
fullName
}
auth {
certificate {
serial
}
facebook {
fbId
displayName
}
google {
googleId
displayName
}
github {
ghId
displayName
}
linkedin {
lnId
displayName
}
}
}
`
@@ -0,0 +1,13 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const ResetPassSubmitMutation = gql`
mutation resetPassword($input: ResetPasswordInput!) {
resetPassword(input: $input) {
errors {
field
message
}
}
}
`
@@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const UniqueUsernameQuery = gql`
query uniqueUsername($username: String!) {
uniqueUsername(username: $username)
}
`
export default UniqueUsernameQuery
@@ -0,0 +1,16 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const VerifyEmailMutation = gql`
mutation verifyEmail($token: String!) {
verifyEmail(token: $token) {
user {
email
}
errors {
field
message
}
}
}
`
Whitespace-only changes.
@@ -0,0 +1,67 @@
export const auth = {
form: {
type: 'login',
message: ''
},
username: {
input: '',
placeholder: 'Your username or email',
valid: false
},
createUsername: {
input: '',
placeholder: 'Create a username',
valid: false,
unique: true
},
email: {
input: '',
placeholder: 'Your email address',
valid: false
},
registerEmail: {
input: ''
},
password: {
input: '',
placeholder: 'Your password',
valid: false
},
createPassword: {
input: '',
placeholder: 'Your new password',
strength: '',
valid: false
},
confirmPassword: {
input: '',
placeholder: 'Confirm your password',
matches: false,
valid: false
},
forgotEmail: {
input: '',
placeholder: 'Enter your email address or username',
valid: false
},
resetPassword: {
input: '',
placeholder: 'Set a new password',
strength: '',
valid: false
},
confirmResetPassword: {
input: '',
placeholder: 'Confirm your new password',
matches: false,
valid: false
},
finishRegister: {
load: false,
error: false,
message: '',
success: false
}
}
export default auth
@@ -0,0 +1,261 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
export const FinishRegisterForm = props => (state, actions) => (
<form name='finishRegisterForm' key='finishRegisterForm'>
<div class='field'>
<label class='label'>New Username:</label>
<div class='control has-icons-left has-icons-right'>
<input
name='new-username'
class='input'
type='text'
autocomplete='username'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.createUsername.placeholder}
oninput={({ target: { value } }) => actions.auth.checkUsername(value)}
onblur={({ target: { value } }) => actions.auth.checkUsername(value)}
value={state.auth.createUsername.input}
placeholder={state.auth.createUsername.placeholder}
/>
<span
key='createUsernameUserIcon'
class={`icon is-small is-left ${
state.auth.createUsername.unique ? 'has-text-success' : ''
}`}
>
<i class='fas fa-user' />
</span>
<span
key='createUsernameCheckIcon'
class={`icon is-small is-right ${
state.auth.createUsername.unique ? 'has-text-success' : ''
}`}
>
<i class='fas fa-check' />
</span>
<p
key='createUsernameUniqueMessage'
class={`m-t-1 has-text-centered ${
state.auth.createUsername.input !== '' &&
state.auth.createUsername.error !== null &&
state.auth.createUsername.error !== '' &&
!state.auth.createUsername.unique
? 'has-text-danger'
: 'is-invisible'
}`}
>
This username is already taken. Please create a new one.
</p>
</div>
</div>
<div class='field'>
<label class='label'>New Password:</label>
<div class='control has-icons-left has-icons-right'>
<input
name='new-password'
class='input'
type='password'
autocomplete='new-password'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.createPassword.placeholder}
oninput={({ target: { value } }) =>
actions.auth.createPassword.input(value)
}
onblur={({ target: { value } }) =>
actions.auth.createPassword.input(value)
}
value={state.auth.createPassword.input}
placeholder={state.auth.createPassword.placeholder}
/>
<span
key='createPasswordLockIcon'
class={`icon is-small is-left
${
state.auth.createPassword.strength === 'weak'
? 'has-text-danger'
: ''
}
${
state.auth.createPassword.strength === 'medium'
? 'has-text-warning'
: ''
}
${
state.auth.createPassword.strength === 'strong'
? 'has-text-success'
: ''
}
`}
>
<i class='fas fa-lock' />
</span>
<span
key='createPasswordCheckIcon'
class={`icon is-small is-right ${
state.auth.createPassword.strength === 'strong'
? 'has-text-success'
: ''
}`}
>
<i class='fas fa-check' />
</span>
<p
key='createPasswordStrengthMessage'
class={`m-t-1 has-text-centered ${
state.auth.createPassword.strength !== null &&
state.auth.createPassword.input !== ''
? 'has-text-primary'
: 'is-invisible'
}`}
>
Password strength is {state.auth.createPassword.strength}
</p>
<p
key='createPasswordStrengthMessage2'
class={`m-t-1 has-text-centered ${
state.auth.createPassword.strength === 'weak' ||
state.auth.createPassword.strength === 'medium'
? 'has-text-primary'
: 'is-invisible'
}`}
>
<span class='is-italic is-size-6'>
At least 10 characters, upper &amp; lower case letters<br />+ a
number + a special char [! @ # $ % ^ &amp; *]
</span>
</p>
</div>
</div>
<div class='field'>
<label class='label'>Confirm Password:</label>
<div class='control has-icons-left has-icons-right'>
<input
name='confirm-password'
class='input'
type='password'
autocomplete='new-confirm-password'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.confirmPassword.placeholder}
oninput={({ target: { value } }) =>
actions.auth.confirmPassword.input({
value: value,
confirm: state.auth.createPassword.input
})
}
onblur={({ target: { value } }) =>
actions.auth.confirmPassword.input({
value: value,
confirm: state.auth.createPassword.input
})
}
value={state.auth.confirmPassword.input}
placeholder={state.auth.confirmPassword.placeholder}
/>
<span
key='confirmPasswordLockIcon'
class={`icon is-small is-left
${
state.auth.confirmPassword.strength === 'weak'
? 'has-text-danger'
: ''
}
${
state.auth.confirmPassword.strength === 'medium'
? 'has-text-warning'
: ''
}
${
state.auth.confirmPassword.strength === 'strong'
? 'has-text-success'
: ''
}
`}
>
<i class='fas fa-lock' />
</span>
<span
key='confirmPasswordCheckIcon'
class={`icon is-small is-right ${
state.auth.confirmPassword.matches ? 'has-text-success' : ''
}`}
>
<i class='fas fa-check' />
</span>
<p
key='confirmPasswordMatchesMessage'
class={`m-t-1 has-text-centered ${
state.auth.confirmPassword.matches
? 'has-text-primary'
: 'is-invisible'
}`}
>
Passwords match!
</p>
</div>
</div>
<p
key='finishRegisterMessage'
class={`help
${state.auth.finishRegister.success ? 'has-text-success' : ''}
${state.auth.finishRegister.error ? 'has-text-danger' : ''}
${state.auth.finishRegister.load ? 'has-text-primary' : ''}
${
(state.auth.finishRegister.load ||
state.auth.finishRegister.error ||
state.auth.finishRegister.success) &&
state.auth.finishRegister.success !== ''
? 'is-size-4 m-t-hlf m-b-hlf'
: 'is-invisible'
}`}
>
{state.auth.finishRegister.message}
</p>
<div class='control buttons is-centered'>
<input
type='hidden'
name='email'
value={state.auth.registerEmail.input}
/>
<button
onclick={e => actions.auth.finish(e)}
class={`button is-primary ${
state.auth.finishRegister.load ? 'is-loading' : ''
}`}
aria-label='Finish Account Creation'
disabled={
!(
state.auth.createUsername.unique &&
state.auth.createPassword.strength === 'strong' &&
state.auth.confirmPassword.matches
)
}
>
Finish Account Creation
</button>
</div>
</form>
)
export default FinishRegisterForm
@@ -0,0 +1,79 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
export const ForgotPassForm = props => (state, actions) => (
<form name='forgotPassForm' key='forgotPassForm'>
<div class='field'>
<label class='label'>Enter your Email Address</label>
<div class='control has-icons-left has-icons-right'>
<input
class='input'
type='text'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.forgotEmail.placeholder}
oninput={({ target: { value } }) =>
actions.auth.forgotEmail.input(value)
}
value={state.auth.forgotEmail.input}
placeholder={state.auth.forgotEmail.placeholder}
/>
<span
class={`icon is-small is-left ${
state.auth.forgotEmail.valid ? 'has-text-success' : ''
}`}
>
<i class='fas fa-user' />
</span>
<span
class={`icon is-small is-right ${
state.auth.forgotEmail.valid
? 'has-text-success'
: 'has-text-light-grey'
}`}
>
<i class='fas fa-check' />
</span>
</div>
<p
class={`help is-size-4 m-t-hlf m-b-hlf
${state.auth.form.form === 'forgotPassLoad' ? 'is-primary' : ''}
${state.auth.form.form === 'forgotPassSuccess' ? 'is-success' : ''}
${state.auth.form.form === 'forgotPassError' ? 'is-danger' : ''}
${
state.auth.form.form === 'forgotPassLoad' ||
state.auth.form.form === 'forgotPassSuccess' ||
state.auth.form.form === 'forgotPassError'
? ''
: 'is-invisible'
}`}
>
{state.auth.form.message}
</p>
</div>
<div class='control'>
<button
onclick={e => actions.auth.forgotPass(e)}
class={`button is-primary ${
state.auth.form.form === 'forgotPassLoad' ? 'is-loading' : ''
}`}
aria-label='Send New Password'
disabled={state.auth.form.form === 'forgotPassLoad'}
>
Submit
</button>
<Link to='/login' class='is-link is-size-6 is-pulled-right'>
Back to Login &rarr;
</Link>
</div>
</form>
)
export default ForgotPassForm
@@ -0,0 +1,13 @@
import LoginForm from './login'
import RegisterForm from './register'
import FinishRegisterForm from './finish-register'
import ForgotPassForm from './forgot-pass-form'
import ResetPassForm from './reset-pass-form'
export {
LoginForm,
RegisterForm,
FinishRegisterForm,
ForgotPassForm,
ResetPassForm
}
@@ -0,0 +1,99 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
export const LoginForm = props => (state, actions) => (
<form name='loginForm' key='LoginForm'>
<div class='field'>
<label class='label'>Username or Email</label>
<div class='control has-icons-left has-icons-right'>
<input
class='input'
type='text'
autocomplete='username'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.username.placeholder}
oninput={({ target: { value } }) =>
actions.auth.username.input(value)
}
value={state.auth.username.input}
placeholder={state.auth.username.placeholder}
/>
<span class='icon is-small is-left'>
<i class='fas fa-user' />
</span>
<span class='icon is-small is-right is-invisible'>
<i class='fas fa-check' />
</span>
</div>
</div>
<div class='field'>
<label class='label'>Password</label>
<div class='control has-icons-left has-icons-right'>
<input
class='input'
type='password'
autocomplete='current-password'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.password.placeholder}
oninput={({ target: { value } }) =>
actions.auth.password.input(value)
}
value={state.auth.password.input}
placeholder={state.auth.password.placeholder}
/>
<span class='icon is-small is-left'>
<i class='fas fa-lock' />
</span>
<span
class={`icon is-small is-right ${
state.user.form.form === 'loginError' ? '' : 'is-invisible'
}`}
>
<i class='fas fa-exclamation-triangle' />
</span>
</div>
<p
key='loginMessage'
class={`help is-size-4 m-t-hlf m-b-hlf
${state.user.form.form === 'loginSuccess' ? 'has-text-success' : ''}
${state.user.form.form === 'loginNotice' ? 'has-text-primary' : ''}
${state.user.form.form === 'loginError' ? 'has-text-danger' : ''}
${
state.user.form.form === 'loginSuccess' ||
state.user.form.form === 'loginNotice' ||
state.user.form.form === 'loginError'
? ''
: 'is-invisible'
}`}
>
{state.user.form.message}
</p>
</div>
<div class='control'>
<button onclick={e => actions.user.loginSubmit(e)} class='button is-link'>
Submit
</button>
<Link to='/forgot-pass' class='is-link is-size-6 is-pulled-right'>
Forgot password?
</Link>
</div>
</form>
)
export default LoginForm
@@ -0,0 +1,71 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
export const RegisterForm = props => (state, actions) => (
<form name='registerForm' key='registerForm'>
<div class='field'>
<label class='label'>Email Address</label>
<div class='control has-icons-left has-icons-right'>
<input
class='input'
type='text'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.email.placeholder}
oninput={({ target: { value } }) => actions.auth.email.input(value)}
value={state.auth.email.input}
placeholder={state.auth.email.placeholder}
/>
<span
class={`icon is-small is-left ${
state.auth.email.valid ? 'has-text-success' : ''
}`}
>
<i class='fas fa-user' />
</span>
<span
class={`icon is-small is-right ${
state.auth.email.valid ? 'has-text-success' : 'has-text-light-grey'
}`}
>
<i class='fas fa-check' />
</span>
</div>
<p
class={`help is-size-4 m-t-hlf m-b-hlf
${state.auth.form.form === 'registerLoad' ? 'is-primary' : ''}
${state.auth.form.form === 'registerSuccess' ? 'is-success' : ''}
${state.auth.form.form === 'registerError' ? 'is-danger' : ''}
${
state.auth.form.form === 'registerLoad' ||
state.auth.form.form === 'registerSuccess' ||
state.auth.form.form === 'registerError'
? ''
: 'is-invisible'
}`}
>
{state.auth.form.message}
</p>
</div>
<div class='control buttons is-centered'>
<button
onclick={e => actions.auth.register(e)}
class={`button is-primary ${
state.auth.form.form === 'registerLoad' ? 'is-loading' : ''
}`}
aria-label='Register'
disabled={state.auth.form.form === 'registerLoad'}
>
Register
</button>
</div>
</form>
)
export default RegisterForm
@@ -0,0 +1,200 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
export const ResetPassForm = ({ token }) => (state, actions) => (
<form name='resetPassForm' key='resetPassForm'>
<div class='field'>
<label class='label'>New Password:</label>
<div class='control has-icons-left has-icons-right'>
<input
name='new-password'
class='input'
type='password'
autocomplete='new-password'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.resetPassword.placeholder}
oninput={({ target: { value } }) =>
actions.auth.resetPassword.input(value)
}
onblur={({ target: { value } }) =>
actions.auth.resetPassword.input(value)
}
value={state.auth.resetPassword.input}
placeholder={state.auth.resetPassword.placeholder}
/>
<span
key='resetPasswordLockIcon'
class={`icon is-small is-left
${
state.auth.resetPassword.strength === 'weak'
? 'has-text-danger'
: ''
}
${
state.auth.resetPassword.strength === 'medium'
? 'has-text-warning'
: ''
}
${
state.auth.resetPassword.strength === 'strong'
? 'has-text-success'
: ''
}
`}
>
<i class='fas fa-lock' />
</span>
<span
key='resetPasswordCheckIcon'
class={`icon is-small is-right ${
state.auth.resetPassword.strength === 'strong'
? 'has-text-success'
: ''
}`}
>
<i class='fas fa-check' />
</span>
<p
key='resetPasswordStrengthMessage'
class={`${
state.auth.resetPassword.strength !== null &&
state.auth.resetPassword.input !== ''
? 'm-t-1 has-text-centered has-text-primary'
: 'is-invisible'
}`}
>
Password strength is {state.auth.resetPassword.strength}
</p>
<p
key='resetPasswordStrengthMessage2'
class={`${
state.auth.resetPassword.strength === 'weak' ||
state.auth.resetPassword.strength === 'medium'
? 'm-t-1 has-text-centered has-text-primary'
: 'is-invisible'
}`}
>
<span class='is-italic is-size-6'>
At least 10 characters, upper &amp; lower case letters<br />+ a
number + a special char [! @ # $ % ^ &amp; *]
</span>
</p>
</div>
</div>
<div class='field'>
<label class='label'>Confirm Password:</label>
<div class='control has-icons-left has-icons-right'>
<input
name='confirm-password'
class='input'
type='password'
autocomplete='new-confirm-password'
onupdate={(element, oldAttributes) => {
if (
oldAttributes.value !== element.value ||
(oldAttributes.value !== '' && element.value === '')
) {
element.focus()
}
}}
aria-label={state.auth.confirmResetPassword.placeholder}
oninput={({ target: { value } }) =>
actions.auth.confirmResetPassword.input({
value: value,
confirm: state.auth.resetPassword.input
})
}
onblur={({ target: { value } }) =>
actions.auth.confirmResetPassword.input({
value: value,
confirm: state.auth.resetPassword.input
})
}
value={state.auth.confirmResetPassword.input}
placeholder={state.auth.confirmResetPassword.placeholder}
/>
<span
key='confirmResetPasswordLockIcon'
class={`icon is-small is-left
${
state.auth.confirmResetPassword.strength === 'weak'
? 'has-text-danger'
: ''
}
${
state.auth.confirmResetPassword.strength === 'medium'
? 'has-text-warning'
: ''
}
${
state.auth.confirmResetPassword.strength === 'strong'
? 'has-text-success'
: ''
}
`}
>
<i class='fas fa-lock' />
</span>
<span
key='confirmResetPasswordCheckIcon'
class={`icon is-small is-right ${
state.auth.confirmResetPassword.matches ? 'has-text-success' : ''
}`}
>
<i class='fas fa-check' />
</span>
<p
key='confirmResetPasswordMatchesMessage'
class={`${
state.auth.confirmResetPassword.matches
? 'm-t-1 has-text-centered has-text-primary'
: 'is-invisible'
}`}
>
Passwords match!
</p>
</div>
</div>
<p
key='resetPassMessage'
class={`help
${state.auth.form.form === 'resetPassDone' ? 'has-text-success' : ''}
${state.auth.form.form === 'resetPassError' ? 'has-text-danger' : ''}
${
state.auth.form.form === 'resetPassDone' ||
state.auth.form.form === 'resetPassError' ||
state.auth.form.form === 'resetPassLoad'
? ' is-size-4 m-t-hlf m-b-hlf has-text-centered'
: 'is-invisible'
}`}
>
{state.auth.form.message}
</p>
<div class='control buttons is-centered'>
<input type='hidden' name='token' value={token} />
<button
onclick={e => actions.auth.resetPass(e)}
class={`button is-primary ${
state.auth.form.form === 'resetPassLoad' ? 'is-loading' : ''
}`}
disabled={
!(
state.auth.resetPassword.strength === 'strong' &&
state.auth.confirmResetPassword.matches
)
}
>
Reset Password
</button>
</div>
</form>
)
export default ResetPassForm
@@ -0,0 +1,94 @@
import { action } from '@hyperapp/fx'
import { GetAllTeamsQuery } from '../graphql/GetAllTeamsQuery'
import { GetAllAlgobotsQuery } from '../graphql/GetAllAlgobotsQuery'
import { client } from '../../../index'
export const community = {
form: ({ form, message }) => state => {
return {
form: {
form,
message
}
}
},
clearForms: () => ({
form: {
form: 'login',
message: ''
}
}),
toggleModal: form => (state, actions) => {
return {
modal: {
content: form,
isActive: !state.modal.isActive
}
}
},
teams: teams => ({ teams }),
algobots: algobots => ({ algobots }),
getTeams: id => [
action('form', {
form: 'getTeamsLoad',
message: 'Getting community teams...'
}),
action('getAllTeams')
],
getAllTeams: () => async (state, actions) => {
const result = await client.query({
query: GetAllTeamsQuery,
variables: { limit: 10, after: 0 },
fetchPolicy: 'network-only'
})
console.log(result.data.teams)
if (result.data.teams.edges.length > 0) {
return actions.getAllTeamsSuccess(result.data.teams)
} else {
return actions.noTeams()
}
},
getAllTeamsSuccess: teams => [
action('form', { form: 'getAllTeamsSuccess', message: 'Community Teams' }),
action('teams', teams)
],
noTeams: profile => [
action('form', { form: 'getAlgobotsNotice', message: 'No teams found' }),
action('teams', [])
],
getAlgobots: id => [
action('form', {
form: 'getAlgobotsLoad',
message: 'Getting community algobots...'
}),
action('getAllAlgobots', id)
],
getAllAlgobots: () => async (state, actions) => {
const result = await client.query({
query: GetAllAlgobotsQuery,
variables: { limit: 10, after: 0 },
fetchPolicy: 'network-only'
})
console.log(result.data.getAlgobots)
if (result.data.getAlgobots !== null) {
return actions.getAllAlgobotsSuccess(result.data.getAlgobots)
} else {
return actions.noAlgobots()
}
},
getAllAlgobotsSuccess: algobots => [
action('form', {
form: 'getAlgobotSuccess',
message: 'Community Algobots'
}),
action('algobots', algobots)
],
noAlgobots: profile => [
action('form', { form: 'getAlgobotsNotice', message: 'No algobots found' }),
action('algobots', [])
]
}
export default community
@@ -0,0 +1,29 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const GetAllAlgobotsQuery = gql`
query algobots($limit: Int!, $after: Int) {
algobots(limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
id
name
slug
owner
avatar
repo
team_id
type
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`
export default GetAllAlgobotsQuery
@@ -0,0 +1,40 @@
/* eslint-disable no-unused-vars */
import gql from 'graphql-tag'
export const GetAllTeamsQuery = gql`
query teamsQuery {
teams {
edges {
node {
id
name
slug
profile {
avatar
description
motto
}
owner {
nickname
visible
profile {
avatar
}
}
members {
role
member {
nickname
visible
profile {
avatar
}
}
}
}
}
}
}
`
export default GetAllTeamsQuery
Whitespace-only changes.
@@ -0,0 +1,15 @@
export const community = {
form: {
form: 'login',
message: ''
},
filter: {},
teams: [],
algobots: [],
modal: {
content: 'AddAlgobot',
isActive: false
}
}
export default community
@@ -0,0 +1,141 @@
/* eslint-disable no-unused-vars */
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
export const AlgobotsList = props => (state, actions) => {
const algobot = state.algobot.profile
console.log(state.algobot.form.form, state.algobot.form.message, algobot.name)
if (
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'No algobot found' &&
algobot.name === ''
) {
return (
<div class='section' key='algobotsList2'>
<p class='is-size-4'>You don't have an algobot yet!</p>
<div class='level m-t-2'>
<div class='level-item'>
<a
class='button is-primary is-medium'
onclick={() => actions.algobot.toggleModal('AddAlgobot')}
>
Create an algobot
</a>
</div>
</div>
</div>
)
}
if (
(algobot.name !== '' && algobot.name !== null) ||
state.algobot.form.form === 'deleteAlgobotSuccess'
) {
const initial = algobot.name.substring(0, 1)
return (
<div class='section' key='algobotsList3'>
<p
class={`is-size-5 card-footer-item
${
state.algobot.form.form === 'deleteAlgobotSuccess'
? 'has-text-success'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotNotice'
? 'has-text-primary'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotError'
? 'has-text-danger'
: ''
}
${
state.algobot.form.form === 'deleteAlgobotSuccess' ||
state.algobot.form.form === 'deleteAlgobotNotice' ||
state.algobot.form.form === 'deleteAlgobotError'
? ''
: 'is-invisible'
}`}
>
{state.algobot.form.message}
</p>
<table class='table is-fullwidth is-striped'>
<thead>
<tr>
<th>&nbsp;</th>
<th>Algobot Name</th>
<th>Team</th>
<th>Type</th>
<th>Owner</th>
<th>Repo</th>
<th class='has-text-right'>Actions</th>
</tr>
</thead>
<tbody>
{algobot.name !== '' ? (
<tr>
<td>
{algobot.avatar !== null && algobot.avatar !== '' ? (
<div class='image is-16x16 avatar'>
<img src={algobot.avatar} alt={algobot.name} />
</div>
) : (
<div class='image is-16x16 avatar has-text-white is-uppercase is-green is-size-7 is-centered'>
{initial}
</div>
)}
</td>
<td>
<strong>{algobot.name}</strong>
</td>
<td>{state.team.profile.name}</td>
<td />
<td>{state.user.profile.username}</td>
<td />
<td class='has-text-right'>
<button
onclick={e =>
actions.algobot.deleteAlgobot(
state.algobot.profile.id,
state.algobot.profile.owner
)
}
class='button is-small is-red'
disabled={
state.algobot.form.form === 'getAlgobotNotice' &&
state.algobot.form.message === 'Removing your algobot...'
}
>
<span class='icon'>
<i class='fas fa-trash-alt' />
</span>
</button>
</td>
</tr>
) : (
''
)}
</tbody>
</table>
</div>
)
}
return (
<div class='section' key='algobotsList4'>
<p class='is-size-4'>{state.algobot.form.message}</p>
<div class='level m-t-2'>
<div class='level-item'>
<span class='icon has-text-primary is-large'>
<i class='fas fa-spinner fa-pulse' />
</span>
</div>
</div>
</div>
)
}
export default AlgobotsList
@@ -0,0 +1,5 @@
import { AlgobotsList } from './algobots-list'
import { TeamsList } from './teams-list'
import { TeamsListWrapper } from './teams-list-wrapper'
export { AlgobotsList, TeamsList, TeamsListWrapper }
Loaded 100 of 203 files, more files were not shown because too many files have changed in this diff. Show more