Repackage Team Module
No files matched your search
@@ -0,0 +1,3 @@
|
||||
/.cache
|
||||
.vscode
|
||||
docs/notes
|
||||
@@ -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)
|
||||
@@ -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
|
||||

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

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

|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
After Width: | Height: | Size: 72 KiB |
@@ -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 |
|
After Width: | Height: | Size: 33 KiB |
@@ -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 |
|
After Width: | Height: | Size: 39 KiB |
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,9 @@
|
||||
/.env
|
||||
/database/.env
|
||||
/database/docker-compose.prisma.prod.yml
|
||||
/docker-compose.prisma.prod.yml
|
||||
dist
|
||||
node_modules
|
||||
.idea
|
||||
.vscode
|
||||
*.log
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-dynamic-import",
|
||||
["transform-react-jsx", { "pragma": "h" }]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.cache
|
||||
dist
|
||||
build
|
||||
.DS_Store
|
||||
node_modules
|
||||
npm-debug.log
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "standard"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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...
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 758 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -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&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"
|
||||
}
|
||||
|
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,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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
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 |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 758 B |
|
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">
|
||||
|
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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 →
|
||||
</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> </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> </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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -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 & lower case letters<br />+ a
|
||||
number + a special char [! @ # $ % ^ & *]
|
||||
</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 →
|
||||
</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 & lower case letters<br />+ a
|
||||
number + a special char [! @ # $ % ^ & *]
|
||||
</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
|
||||
@@ -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> </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 }
|
||||