Merge Complete

This commit is contained in:
Luis Fernando Molina committed 2020-11-20 20:28:04 +01:00
1 parent 482ef7a81a
commit dabe23d505
6483 files changed

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
### Auth0 Integration
[Auth0](https://auth0.com/) is a third-party service that simplifies integrating authentication into a web application.
The main use of Auth0 will be for [Oauth 2.0](https://auth0.com/docs/protocols/oauth2) authentication requesting an [*implicit grant*](https://tools.ietf.org/html/rfc6749#section-1.3.2) flow. Auth0 also hosts the login/oauth authentication forms.
While Auth0 provides a depth of user-security services such as authorization (scopes/roles), multi-factor authentication, currently only basic authentication will be used.
#### Auth0 Function Flow
![Auth0 Function Flow](./assets/AATeamAuth0FunctionFlow.svg)
Following a likely User Experience flow and revealing the Auth0 API functionality below:
1. **User loads AAWeb app and clicks login**. App should initially check session/local storage on-load to see if user is already logged-in. For Single-sign on, can use [Auth0.checkSession](https://auth0.github.io/auth0.js/global.html#checkSession). Otherwise, a first action could be for user to click on the login button to initiate the authentication flow.
2. **AAWeb app sends authorize call to Auth0 and display universal login screen** The login screen is hosted by Auth0. A user can choose to login or signup using email/password or Oauth2 login through strategy provider like Github. After user submits and Auth0 authenticates user, it returns a callback address to the web app.
3. **AAWeb receives callback address and replies to that callback address.**
4. **Auth0 now sends back a JSON Web Token (JWT) back to AAWeb** AAWeb parses JWT and extracts id_token for user information. Saves access_token, id_token and token expiration for future authentication checks. AAWeb can make an `authenticate` mutation query to the API to create an initial account store in the database.
5. **After login, AAWeb checks for user's team information by making request to GraphQL API.** Sends a POST request with an `Authorization: Bearing <auth0_access_token>`header, AAWeb queries API for `teamByOwner` using Auth0 user_info *idTokenPayload.sub* as the owner_id.
6. **GraphQL API receives query request** API parses authentication token and verifies user, compares user_id to existing user or creates a new one. Searches for any teams owned by user. Returns data in JSON format.
7. **User does not have a team and clicks on `Create Team` button**
8. User is redirected to team module client. Session is checked and if user previously logged-in, then user is automatically signed in, otherwise user has to sign-in again. *Note 1: Single-sign-on (SSO) is just a convenience function that shows a single button in login form as opposed to some "auto-login" function.* *Note 2: If user signs in with different method (ie email/pass instead of social[github]) is treated as a new user.*
9. **Auth0 sends new access_token, id_token, expiration, etc** Web app should store values for future api calls.
10. **Make authenticated API class - same as step 5** Once logged in, user can create teams, edit profile, invite others to their team, etc.
11. **Data or errors returned** Both data and errors are returned in JSON format. Data usually returned with HTTP status 204 and errors returned with status 400.
## Integration
There are three main areas to consider for integrating Auth0 into your client and server.
1. **The client** — The main entry point for the user to authenticate with Auth0. This entails using the Auth0 Lock API/library that provides the login/signup screens and other supporting auth functionalities. The main mechanism for authenticating a user is enable by, upon successful login, the provisioning of an access_token — a javascript web token (JWT).
2. **Authenticating between API and Client** - While, the client may be authenticated, we still have to reconfirm that the user is who they are. This requires us to send the logged-in users access_token to the API to be reconfirm. Also, The client needs to reconfirm their authentication if the API server returns an error such as 'Not logged in' or 'JWT expired.'
3. **The API** – On the server, once we have the access token, we need to reconfirm with Auth0 that the client is who they say they are and then capture their auth0id.
### Client
Using the Auth0 Lock package which has less "features" but allows for a more seamless experience by
```
npm i auth0-lock --save
```
On the client, I have all my auth0 logic in an [auth folder](../packages/aateam-client/src/auth)
1. First, pull in your auth config info from your .env.
```
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,
logoutUrl: process.env.AUTH0_LOGOUT_URL
}
```
2. Then setup the main Auth0 Lock object:
```
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`
}
}
})
```
3. Finally, we need an Auth class that initializes the authentication process and binds it to our API.
```
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 () {
// check for valid session
}
setSession (authResult) {
// If authenticated, set session otherwise signin or create account
}
async signinOrCreateAccount ({ accessToken, idToken, expiresAt }) {
// if session expired, sign in, and if needed, also create account
}
logout () {
// Clear access token and ID token from local storage
...
}
async isAuthenticated () {
// check session and run Auth0 SS0
...
}
}
```
Once we have the auth0 library installed, configured and integrated, we need to send the auth0 access_token with our API calls and handle any auth related responses.
For this we attach middlewares to our Apollo GraphQL client.
1. **AuthRetryLink - Apollo Error Link:**
```
const authRetryLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
//check for GraphQL errors
if (graphQLErrors) {
// User access token has expired
// console.log('authLink: ', graphQLErrors) // check for error message to intercept and resend with Auth0 access token
if (graphQLErrors[0].message === 'Not logged in') {
// We assume we have auth0 access token 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 auth0 access token available, we force user to login
observer.error(error)
})
})
}
}
})
```
2. ***AuthLink - Apollo Link:**
```
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = window.localStorage.getItem('access_token')
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : ``
}
}
})
```
Finally, we bind to Apollo client:
3.
```
let apolloClient
const cache = new InMemoryCache().restore(window.__APOLLO_STATE__)
export const client = new ApolloClient({
link: ApolloLink.from([authRetryLink, authLink, link]),
cache,
connectToDevTools: true
})
```
@@ -0,0 +1,5 @@
# Code Styling and Formatting
```
npm install --save-dev eslint-config-standard eslint-config-standard-react eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node eslint-plugin-react
```
+162
View File
@@ -0,0 +1,162 @@
# AA Team Module Documentation
## Architecture
Single-Page App (SPA) Modules are NodeJS applications that consist of a client-side application that loads and runs within a users browsers. While a SPA is a fully functional application and doesn't need backend support, it easily meets its limitations when relegated to solely the resources of a web browser.
Single-page apps are able to expand their capabilities by connecting to external datasources or other services, usually through a web API(application programming interface). These APIs allow the web app to pull in curated data from other sources like trade exchanges or movie information, connect to functionality that allows the app to authenticate, create, manage and save information in databases and much more.
### Overview
![AA Team Module Demo Architecture](./assets/AATeamDemoArchiteture.svg)
1. Postgres Database: relational data store
2. Prisma GraphQL service: wrap Postgres as a Relational Database Manager (RDBM)
3. GraphQL API server: Provides backend support with connections to data storage and other datasources
4. NGinx Web Server: Serves the front-end web applications. HTTPS provided by LetsEncrypt.
5. https://api.algocommunity.org: The API endpoint
6. https://aaweb.algocommunity.org: The aaweb demo endpoint
7. https://algocommunity.org: the team module front-end demo endpoint
8. Auth0: Third-party service providing authentication to front-end clients and Graphql API server.
9. User Web Browser: Location where front-end web apps are loaded to
### GraphQL
This module uses a GraphQL API server as its backend support providing it with the ability to query stored data on AA teams and members as well as create and manage that data.
[GraphQL](https://graphql.org/) is a specification and query language for APIs and is implemented by using the [Apollo Server](https://www.apollographql.com/) platform. Behind the GraphQL API server is a Prisma service that connects to a Postgres relational database. The Prisma service acts as a lower-level GraphQL relational-database-manager wrapping the Postgres DB.
A simple comparison between GraphQL and REST can be found at [howtographql.com](https://www.howtographql.com/basics/1-graphql-is-the-better-rest/)
In short, this combination allows
- rapid database development by simply creating a GraphQL schema
- connecting multiple datasource endpoints and wrapping them into a single endpoint (Postgres DB, Azure storage API, other micro-services and apis (like Sendgrid or even other AA modules)
- a single endpoint simplifies caching and authentication
### Auth0 Integration
[Auth0](https://auth0.com/) is a third-party service that simplifies integrating authentication into a web application.
The main use of Auth0 will be for [Oauth 2.0](https://auth0.com/docs/protocols/oauth2) authentication requesting an [*implicit grant*](https://tools.ietf.org/html/rfc6749#section-1.3.2) flow. Auth0 also hosts the login/oauth authentication forms.
While Auth0 provides a depth of user-security services such as authorization (scopes/roles), multi-factor authentication, currently only basic authentication will be used.
#### Auth0 Function Flow
![Auth0 Function Flow](./assets/AATeamAuth0FunctionFlow.svg)
Following a likely User Experience flow and revealing the Auth0 API functionality below:
1. **User loads AAWeb app and clicks login**. App should initially check session/local storage on-load to see if user is already logged-in. For Single-sign on, can use [Auth0.checkSession](https://auth0.github.io/auth0.js/global.html#checkSession). Otherwise, a first action could be for user to click on the login button to initiate the authentication flow.
2. **AAWeb app sends authorize call to Auth0 and display universal login screen** The login screen is hosted by Auth0. A user can choose to login or signup using email/password or Oauth2 login through strategy provider like Github. After user submits and Auth0 authenticates user, it returns a callback address to the web app.
3. **AAWeb receives callback address and replies to that callback address.**
4. **Auth0 now sends back a JSON Web Token (JWT) back to AAWeb** AAWeb parses JWT and extracts id_token for user information. Saves access_token, id_token and token expiration for future authentication checks. AAWeb can make an `authenticate` mutation query to the API to create an initial account store in the database.
5. **After login, AAWeb checks for user's team information by making request to GraphQL API.** Sends a POST request with an `Authorization: Bearing <auth0_access_token>`header, AAWeb queries API for `teamByOwner` using Auth0 user_info *idTokenPayload.sub* as the owner_id.
6. **GraphQL API receives query request** API parses authentication token and verifies user, compares user_id to existing user or creates a new one. Searches for any teams owned by user. Returns data in JSON format.
7. **User does not have a team and clicks on `Create Team` button**
8. User is redirected to team module client. Session is checked and if user previously logged-in, then user is automatically signed in, otherwise user has to sign-in again. *Note 1: Single-sign-on (SSO) is just a convenience function that shows a single button in login form as opposed to some "auto-login" function.* *Note 2: If user signs in with different method (ie email/pass instead of social[github]) is treated as a new user.*
9. **Auth0 sends new access_token, id_token, expiration, etc** Web app should store values for future api calls.
10. **Make authenticated API class - same as step 5** Once logged in, user can create teams, edit profile, invite others to their team, etc.
11. **Data or errors returned** Both data and errors are returned in JSON format. Data usually returned with HTTP status 204 and errors returned with status 400.
## Working with GraphQL
If you're unfamiliar with working with GraphQL, the best way to start is learning the [Core Concepts](https://www.howtographql.com/basics/2-core-concepts/) and reading through some of the introduction on [HowToGraphql](https://www.howtographql.com/).
The next step is to start playing with queries in a GraphQL playground. Browse around this one: [https://api.algocommunity.org/playground](https://api.algocommunity.org/playground)
![GraphQL Playground](./assets/AAGraphQLPlayground.png)
1. The main query builder area. GraphQL schemas can be introspected, so auto-suggestion and linting is built in.
2. The response area: where data and errors will show up.
3. The schema documentation: The format of a GraphQL schema allows it to be "Self-documenting" showing the various queries and mutations along with their expected parameters and response forms.
4. Query Variables or Headers can be set. Especially the Authorization Header for queries/mutations that require authentication. The Bearer token is the Auth0 access_token.
### GraphQL tutorial
1. In the playground, let's make a first query. Right now, there's really only one call that doesn't require authenticated: getting teams list.
2. A GraphQL query is written in a JSON-like format of nested layers between brackets. The main searching function is called a "query" and is started by typing **`query {}`** Go ahead an enter that in the query builder section.
3. You should notice some syntax highlighting where the last bracket is marked red and if you hover your mouse over the red, you should see a message like `Syntax Error: Expected Name.` This is useful for letting you know that your query is malformed and it this case it's just telling you that it needs a Name to query — or to be more specific, a Name of a Type to query.
4. To know what our options are, click the Schema tab on the right-hand side. We are going to query the teams and you should see a **Query called teams:TeamConnection**.
5. Go ahead and **click on the query**. What's being show is that the `query teams` returns the type TeamConnection. There are no parameters. But we can somewhat choose what shape of response we get.
6. You'll notice this query returns three main TeamConnection items: pageInfo, edges, and aggregate. **Click on Edges** You should see a type of Edges with item `node: Team` Go ahead and explore the Team item down to see our query options. [Edges/node?](https://blog.apollographql.com/explaining-graphql-connections-c48b7c3d6976)
7. Back in the query builder, let's start building our query. Since we are querying `teams` add it to your query like so: ``` query { teams }``` Notice an autocomplete will pop up to help you figure out what to type.
8. `teams` will be red, and the syntax highlighting should give you a hint. That's right, we need to tell the query what we want returned. After `teams` add `{ }`
9. The cool thing about GraphQL is we can shape our response. We could return all three main query items, but if we really only need one, then we can tell it to return one item. In this case, start typing `edges` Autocomplete will help you again, and you can hit the [Tab] key to take advantage of the suggestions.
11. You probably clicked through a fair amount of pages when exploring the teams schema. Each slideout could be thought of as another layer of brackets and you working your way down the graph until you hit the end of a connection that ends in some type of scalar like String, Int, Boolean or custom ones you defined like Date or Role.
12. So using the syntax highlighting and schema, finish creating a query. No red is usually a sign that the query form is correct. Here's an example:
```
query {
teams {
edges {
node {
name
owner {
nickname
}
profile {
avatar
}
}
}
}
}
```
Go ahead and hit the large triangle button and see what you get!
Congrats, you've created your first GraphQL query.
### GraqphQL schema
The Teams Module graphql schema:
```
enum Role {
OWNER
MANAGER
MEMBER
}
type Team {
id: ID! @unique
createdAt: DateTime!
name: String!
slug: String! @unique
owner: Member! @relation (name: "TeamOwner", onDelete: CASCADE)
profile: TeamProfile
members: [TeamMembers!]! @relation (name: "TeamMembers", onDelete: SET_NULL)
anonymous: Boolean! @default(value: true)
}
type TeamMembers {
id: ID! @unique
member: Member @relation (name: "MemberOnTeam", onDelete: SET_NULL)
team: Team @relation (name: "TeamMembers", onDelete: SET_NULL)
role: Role @default(value: MEMBER)
}
type TeamProfile {
id: ID! @unique
createdAt: DateTime!
description: String
motto: String
avatar: String
}
type Member {
id: ID! @unique
nickname: String
teams: [TeamMembers!]! @relation (name: "MemberOnTeam", onDelete: SET_NULL)
profile: MemberProfile
auth0id: String! @unique
visible: Boolean! @default(value: false)
anonymous: Boolean! @default(value: true)
}
type MemberProfile {
id: ID! @unique
createdAt: DateTime!
bio: String
status: String
avatar: String
repo: String
email: String
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

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

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

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

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB