Platform with user dashboard and admin dashboard for handling WCASG Widget SaaS.
- Initial Accounts / Seed Data
- Product :: Plan for Sites
- JSON Schema
- Site Context Restraints
- Roles
- Artisan Commands
- Development
- Widget API Setup
- Accessibility Statements
- Models / Relationships
- Database
- File Storage
- Webhook Leads
- Site Statistics
- Glossary
There are 15 Users added with random assignments, activations, etc. To test them out, open the Admin dashboard, select Users, copy the email address of the User, and Impersonate them. This is useful for playing around with users who have no active subscriptions and the like.
{
"id": "prod_H3ekPDcHD4OmUB",
"object": "product",
"active": true,
"description": "Accessibility Widget",
"name": "Accessibility Widget",
"statement_descriptor": "Accessibility Widget",
"type": "service",
"unit_label": "site"
}{
"id": "plan_H3ekwrhfyH9NFI",
"object": "plan",
"active": true,
"amount": 500,
"amount_decimal": "500",
"billing_scheme": "per_unit",
"currency": "usd",
"interval": "month",
"interval_count": 1,
"metadata": {
"max_sites": 5
},
"nickname": "5 Sites per Month",
"product": "prod_H3ekPDcHD4OmUB",
"trial_period_days": 14,
"usage_type": "licensed"
}| id | product_id | amount | currency | interval | nickname | active | trial_period_days | context | coupon_id |
|---|---|---|---|---|---|---|---|---|---|
| plan_HDj3a0y455rnsh | prod_HDj3KTwSH0UCXr | 5000 | usd | month | 5 Sites per month | 1 | 0 | {"plan": {"restraints": [{"value": 5, "entity": "site", "comparator": "<="}]}} | NULL |
| plan_HDj3chzvuez7XI | prod_HDj3KTwSH0UCXr | 15000 | usd | month | 15 Sites per month | 1 | 0 | {"plan": {"restraints": [{"value": 15, "entity": "site", "comparator": "<="}]}} | EARLY_ADOPTER_20_OFF_FOREVER |
| plan_HDj3dAstQCTPQj | prod_HDj3KTwSH0UCXr | 1000 | usd | month | 1 Sites per month | 1 | 0 | {"plan": {"restraints": [{"value": 1, "entity": "site", "comparator": "<="}]}} | ALPHA_100_OFF_3_MONTH |
| plan_HDj3ueUFkXAJdT | prod_HDj3KTwSH0UCXr | 25000 | usd | month | 25 Sites per month | 1 | 0 | {"plan": {"restraints": [{"value": 25, "entity": "site", "comparator": "<="}]}} | NULL |
- Schema: resources/schemas/wcasg.json
- Specification: http://json-schema.org/specification.html
- Tests: tests/Unit/Schema/SchemaTest.php
This schema is used to define resource-specific business logic that can be saved alongside a database record in JSON format. Using JSON Schema specification allows us to validate data before saving to the database.
Each restraint object forms a tuple used for business logic validation against the database model containing the given JSON object.
{
"plan": {
"restraints": [
{
"entity": "site",
"comparator": "<=",
"value": 5
}
]
}
}Here, a Plan database model may have the above JSON object associated with it in the database. This informs the rest of the app to perform a restraint check against Site models related to the Plan instance and ensure there are 5 or fewer.
The sites database table's context field supports a JSON context object using the JSON Schema specification. Any Dashboard request that attempts to alter the number of active Sites is validated against all Plans subscribed by the User. Any Plan with a valid context object is validated against the requested Site change.
The following subscriptions table record indicates User 1 has an active subscription with a Plan id of plan_H4Hm2JT2SJQDQs:
| id | plan_id | user_id | stripe_status |
|---|---|---|---|
| sub_H4HmCDl8z5gWfV | plan_H4Hm2JT2SJQDQs | 1 | active |
The matching plans table entry shows the details of the Plan, including its context JSON object defined by the project resources/schemas/wcasg.json schema:
| id | product_id | nickname | active | context |
|---|---|---|---|---|
| plan_H4Hm2JT2SJQDQs | prod_H4HmTlAFxIur1P | Monthly | 1 | {"plan": {"restraints": [{"value": 5, "entity": "site", "comparator": "<="}]}} |
In this case, the only restraint is a limit on the number of active Sites, which must be 5 or fewer.
The sites table shows the collection of Sites assigned to this user's Subscription:
| id | domain | active | name | token | subscription_id |
|---|---|---|---|---|---|
| 1 | localhost:84 | 1 | Koepp, Bins and Feeney | 30c818375b2ee1fbfb37f8a5cc2e8d05b5bb | sub_H4HmCDl8z5gWfV |
| 3 | jakubowski.com | 1 | Bogan, Stamm and Zboncak | 89fa6356b0506342d82988af0368f5f14260 | sub_H4HmCDl8z5gWfV |
| 4 | mertz.com | 0 | Maggio, Emmerich and Kreiger | 75d93c400f1c82cdeee75828289e60cb55d7 | sub_H4HmCDl8z5gWfV |
| 5 | boyle.com | 1 | Romaguera LLC | 1f4badc60238adeb4bae365f5b3c9c71878b | sub_H4HmCDl8z5gWfV |
| 6 | russel.com | 1 | Wolf-McKenzie | 81f460bb9956a7f40de35ee7720022757824 | sub_H4HmCDl8z5gWfV |
| 7 | lynch.com | 1 | Roberts-Konopelski | 9fd37afd37f792168f933f1eda2aa9830d62 | sub_H4HmCDl8z5gWfV |
The user currently has 5 active sites, so attempting to add a new Site or alter Site.id = 4 to an active state fails and returns an error to the user indicating their current Plan does not support any additional sites.
Description of role
ignore-site-limit: Ignores site limits
- Create
admin-testerrole - Create
ignore-site-limitpermission - Assign
ignore-site-limitpermission toadmin-testerrole
Generates fake Site request statistics for Sites owned by special users (devs, admins, etc).
- Signature:
php artisan site:generate-statistics {count?}
Optional count argument determines the number of fake requests generated per Site. Defaults to 10 requests per Site.
MockRequests are used to generate each request record, which includes fake elements and emulates a random timestamp within the past year.
Create a new User with the specified email address and optional password, plan, coupon, and activation status.
- Signature:
php artisan user:create {email} {password?} {--activated : Activate user.} {--plan= : Plan id to assign to user.} {--coupon= : Coupon id to apply to subscription.}
$ php artisan user:create john2@example.com
Creating new user of id 1 and email john@example.com.
Generated password: 3T81tQDANmIo
Save this password somewhere secure as it cannot be recovered.
Sending activation email.
$ php artisan user:create john@example.com --activated
Creating new user of id 1 and email john@example.com.
Generated password: u5UEkIOxtE3W
Save this password somewhere secure as it cannot be recovered.
$ php artisan user:create john@example.com password --activated
Creating new user of id 1 and email john@example.com.
$ php artisan user:create john@example.com --activated --plan=plan_HMGmstfZ3MRFtr
Creating new user of id 1 and email john@example.com.
Generated password: 6R5fJ0lae5gV
Save this password somewhere secure as it cannot be recovered.
Creating subscription.
- Copy old commit to directory.
npm install- Install Composer
composer installphp artisan key:generatephp artisan migrate:freshphp artisan db:seedphp artisan passport:installphp artisan storage:link
git clonerepo.- Copy content of the
.env.developmentsnippet to the a<root>/.envfile. - Run
composer install. - Run
yarn run db:devto create a local MySQL Docker. - Open
http://localhost:8090/, login withroot:example, and add a new database namedvoydev_wcasgadmin. - Add db user specified in
.env. - Run
yarn run db:resetto run all db migrations and seed the database. - Run
php artisan storage:link(just once). - Run
yarn run devto rebuild the client app, then navigate on your local PHP server tolocalhostto view the site.
- Open SSH tunnel to remote MongoDB:
yarn run dev:db:mongo:ssh-tunnel. - Start up local
pa11y-webservice-laravelapp. If installed in sibling directory (i.e.../audit) it can be run withyarn run dev:webservice:start. - Start up local Laravel work queue to handle jobs:
yarn run artisan:queue:work. - Start watcher to monitor for JS / view changes:
yarn run watch.
- Run
yarn run testto execute PHPUnit test suite.
Tests will begin by deleting, rebuilding, and seeding the test database, as defined in phpunit.xml (default is MySQL voydev_wcasgadmin_testing).
- Double-check
phpunit.xml,.env, andconfig/database.phpsettings. - Clear configuration cache:
php artisan config:clear.
- Push changes to new repository branch.
- GitLab CI will create a new pipeline and migrate the new build to the testing server: https://dashboard.widget.wcasg.solarix.dev
- (Optional) If a testing environment database reset is required this can be performed as part of a
testingpipeline. Open the pipeline page in GitLab and manually execute thetesting:build:db:resetBuild stage job.
- Create a merge request to
masterbranch. - Upon MR acceptance, GitLab CI will push new build to production: https://dashboard.widget.wcasg.solarix.host
- Ensure
php artisan:queue:workis running to handle Audit services. - Ensure
php artisan:queue:work:sqsis running to handle Statistic/Coeus services.
Both testing (srn:rds:wcasg:widget:wcasg:testing::db/mysql) and production (srn:rds:wcasg:widget:wcasg:production::db/mysql) RDS instances use the Solarix Dev MySQL security group (srn:vpc:solarix::sg/dev-mysql) to provide external access.
- Add your local public IP address to the
srn:vpc:solarix::sg/dev-mysqlsecurity group's Ingress rules for port3360. - You can use http://checkip.amazonaws.com/ to get your IP.
- With the AWS CLI you can get the
group-idof thesrn:vpc:solarix::sg/dev-mysqlsecurity group:
$ aws ec2 describe-security-groups --filters Name=tag:srn,Values=srn:vpc:solarix::sg/dev-mysql --query "SecurityGroups[0].[GroupId]" --output text
sg-00cb767069e9979fc
- Authorize
TCPon3306port to your IP with the following command (replace123.123.123.123with your IP) and verify the samegroup-idas returned above:
aws ec2 authorize-security-group-ingress --group-id=sg-00cb767069e9979fc --protocol tcp --port 3306 --cidr 123.123.123.123/32
- The RDS servers will now accept incoming connections to port
3306from your IP.
The <root>/api/widget endpoint is the primary entry for all incoming Widget JavaScript requests made by individuals viewing a User's website with a <script src='<root>/api/widget' crossorigin='anonymous' defer></script> tag injected.
Consider the following table entries (some columns truncated for simplicity):
| id | name | stripe_plan_id | interval | amount | active | trial_period_days |
|---|---|---|---|---|---|---|
| 1 | Primary | ipsum_ducimus_fuga | month | 325.00 | 1 | 3 |
| id | first_name | last_name | username | activated | |
|---|---|---|---|---|---|
| 1 | Gabe | Wyatt | gabestah | gabe@solarixdigital.com | 1 |
| id | user_id | name | stripe_id | stripe_status | stripe_plan_id | plan_id |
|---|---|---|---|---|---|---|
| 1 | 1 | main | sub_GgT5ufj9MFaZZ5 | active | ipsum_ducimus_fuga | 1 |
| id | domain | active | token | subscription_id |
|---|---|---|---|---|
| 3 | gabewyatt.com | 1 | iVGUxr5vwxBypx6oOtQvB4YvHB8eyIT94Ecl | 1 |
- An
Admincreates thePrimaryplan, which generates thestripe_plan_idvalue and creates a matching plan via the Stripe API. - The
User(gabe@gabewyatt.com) subscribes to thePrimaryplan, which generates theSubscriptionmodel entry associated with thatPrimaryplan. It also creates a matching subscription via the Stripe API (sub_GgT5ufj9MFaZZ5). - The
Useradds aSiteto their newSubscription, entering a domain ofgabewyatt.com. This auto-generates a uniquetokenfor thatSiterecord. - The
Usercopies the provided<script>tag/api/widgetendpoint with the associatedtokenand pasted it into the header of their website:
<script
crossorigin="anonymous"
src="https://app.wcasg.com/api/widget/iVGUxr5vwxBypx6oOtQvB4YvHB8eyIT94Ecl"
defer
></script>Any request made to /api/widget is handled by the CheckValidWidgetRequest middleware. This middleware completes a series of validations and authorization checks to determine if the request is valid and matches an active subscription. If any check fails, an appropriate exception is thrown and the API response sends a short <script></script> snippet that outputs the error message to the end-user's console log.
- It first checks for the
originrequest header, which is only present forGETrequests (the type of request made by<script>tags) that haveCORSenabled. Thus, the need for thecrossorigin="anonymous"attribute in the script widget provided toCustomers. If nooriginis found, aMissingOriginexception is thrown and outputs the localized message for that error. - A missing
tokenparameter is checked next. - Authentication is based on the passed
tokenfound within theSites.tokendatabase entries. If no match is found anInvalidTokenexception is thrown. - The
hostvalue of theSites.domainfield is compared to thehostvalue of theoriginheader from which the request originated. A match continues and a failure throwsInvalidOrigin. - The
Siteis then checked to ensure itisActive. - The final check is to ensure the matching
Sitesentry is associated with aSubscriptionthat is active / valid. Failure throws anInvalidSubscriptionerror.
If all middleware checks succeed the WidgetController handles the response by pulling the Widget script contents from local storage, assigning appropriate headers, and responding with the full script content that is then injected into the Customer's site.
Below are examples are the API responses provided to the end user based on various forms of <script> tags, assuming database values are as seen above.
<script src="http://app.wcasg.com/api/widget/iVGUxr5vwxBypx6oOtQvB4YvHB8eyIT94Ecl" defer></script>: Console output:[WCASG]: Request origin cannot be determined. Cross Origin request has failed.<script crossorigin="anonymous" src="http://app.wcasg.com/api/widget" defer></script>: Console output:[WCASG]: A token parameter is required to complete this request.<script crossorigin="anonymous" src="http://app.wcasg.com/api/widget/1234567890" defer></script>: Console output:[WCASG]: Token parameter is invalid.<script crossorigin="anonymous" src="http://app.wcasg.com/api/widget/iVGUxr5vwxBypx6oOtQvB4YvHB8eyIT94Ecl" defer></script>: All checks pass and Widget script is returned and rendered in browser.- Same valid script tag as above, but it is being injected into a page hosted on the domain
hackers.net. Console output:[WCASG]: Request origin is invalid. - Same valid script tag as above, but this time the
Subscriptionof id1is set to invalid. Console output:[WCASG]: The subscription associated with this site is disabled or invalid.
Accessibility statements start as a Statement Template model instance. Seed data contains a few initial templates (as seen in resources/assets/templates/).
A template can be any combination of plain text, valid HTML tags, and custom {% %} tags used to inject runtime data.
{% %} tags must enclose an object name and a bracketed key. Each valid object refers to a collection of relevant data. Object name can be any of the following:
app: The Dashboard application config. For example,app['app.url']inserts the URL of the Dashboard app. See Laravel config and local config directory.config: The customStatementconfiguration, otherwise the parentStatement Templateconfiguration. For example,{% config['includeContactInfo'] %}inserts the current configuration value of theincludeContactInfokey.site: TheSitemodel object that is using thisStatement. For example,{% site['domain'] %}inserts theSite'sdomain.subscription: TheSubscriptionmodel object associated withSite. For example,{% subscription['ends_at'] %}inserts the ending date of theSubscription, if applicable.timestamp: Current timestamp.user: The owningUsermodel object of the associatedSite. For example,{% user['email'] %}inserts the email address of the owningUser.
A custom {% %} tag can surround any valid object['key'] combination and be inserted anywhere in the statement HTML, e.g.:
<h1>{% site['name'] %} Accessibility Statement</h1>
<p>
Updated: {% timestamp %}.
</p>
<p>
{% site['domain'] %} makes available the
<a
href="{% app['app.url'] %}"
alt="{% app['app.name'] %} Widget"
title="{% app['app.name'] %} Widget"
>{% app['app.name'] %} Widget</a
>
that is powered by a dedicated accessibility server. The software allows {%
site['domain'] %} to improve its compliance with the Web Content Accessibility
Guidelines (WCAG 2.1).
</p>Some of Laravel Blade's directives can be used, but are sanitized to prevent malicious code injection. The following directives are allowed:
ifunlessissetempty
A directive can be used to check the truthy/falsey value of a custom {% object['key'] %} value and optionally render or discard the enclosed content. For example:
@if({% config['includeContactInfo'] %})
<h2>Contact Us</h2>
<p>
If you wish to report an accessibility issue, have any questions or need
assistance, please contact {% site['domain'] %} Customer Support as follows:
</p>
<p>Email: <a href="mailto:{% user['email'] %}">{% user['email'] %}</a></p>
@endifThe parser will initially replace inner content such as {% site['domain'] %} and {% user['email'] %}, then it will perform validation of the @if ... @endif directive. In this case, an if directive checks for a truthy value, so if the result of {% config['includeContactInfo'] %} is truthy the inner content will be included in the rendered statement.
The following directives are disabled for security purposes.
phpforforeachforelsewhile
Any text within a disabled directive will be discarded and not rendered.
Each Statement Template has a default_config that is a JSON object defining the initial configuration settings for all sites/statements that will use this template.
{
"includeContactInfo": true
}An Admin can add or modify Statement Templates under the Admin > Statements dashboard view.
Once instance of an existing Statement Template is always marked as the current default_template_statement in the settings db table. This ensures that User-generated Sites will always have a default Statement Template to fall back to.
An Admin will receive an error if attempting to delete the current default Statement Template and must assign the default to a different template before deletion.
- Viewing a
Statement Templateas anAdminshows the HTML-ified template prior to dynamic data injection. - Editing a
Statement Templateallows theAdminto change thename,is_default,default_config, and templatecontentvia a WYSIWYG editor. As with the default, Laravel Blade syntax is mixed with the properly rendered HTML in the editor, allowing for advanced data injection.- The
is_defaultfield is disabled if the currently edited template is already the default to prevent user from trying to unset the sole default without first assigning a new default.
- The
- At present,
Admincan only delete aStatement Templatenot assigned to anything else (Statements). - The create
Statement Templateview uses thedefault.htmltemplate, but can be overridden byAdminas needed.
Admin can also view and adjust some Statement settings.
From within the account/sites view a User can adjust the accessibility Statement assigned to each Site.
- Viewing the
Statementassigned to aUser'sSiteshows the fully-rendered, injected statement content based on the currentUser,Site,Statement, and parentStatement Template. - The Edit view shows the rendered statement using the currently-assigned
Statement Template. Choosing a different template in the dropdown shows a preview of theStatementcontent using the selected template.- Here the
Usercan also customize theStatementconfiguration (currently a JSON object. TODO: Update to a more user-friendly form). The default configuration for the parentStatement Templatewill be used unless theUseroverrides the configuration with their ownconfigsettings.
- Here the
This section describes models and relationships as they are developed.
- Globally unique
- Identified by
email - Can select one or more
Plansto pay for, each of which creates a uniqueSubscriptionassociated withUser.
- Globally shared
- Directly created by
Admin - Details a payment model that
Usercan pay for via aSubscription, e.g.:
| Name | Interval | Price | plan type | Site limit | Team limit | Date | Status |
|---|---|---|---|---|---|---|---|
| Basic Plan | month | 5.00 | Normal plan | 1 | 11 minutes ago | Active | |
| Advanced Plan | month | 15.00 | Normal plan | 3 | 9 minutes ago | Active |
- Unique to
User - Indirectly created by
Userwhen signing up for a selectedPlan. - Associated with one-to-many
Site. - While
Subscriptionisactive, API requests toapi/widget:get()that originate from an associatedSiterespond with a valid Widget JavaScript injection snippet.
- Unique to
UserandSubscription. - Directly created by associated
Userand related to selectedSubscription. - Associated with one
Subscriptionand oneUser. - Each
Sitecan be assigned to a givenSubscriptionbyUser. - Multiple
Sitescan be associated with aSubscriptionandUser, but no more thanSubscription > Plan > SiteLimitSitescan beactiveat one time.
| Column | Type | Comment |
|---|---|---|
| id | varchar(50) | |
| currency | enum('usd') [usd] | |
| duration | enum('forever','once','repeating') | Describes how long a customer who applies this coupon will get the discount. |
| duration_in_months | int NULL | If duration is repeating, the number of months the coupon applies. Null if coupon duration is forever or once. |
| max_redemptions | int NULL | Maximum number of times this coupon can be redeemed, in total, across all customers, before it is no longer valid. |
| metadata | json NULL | |
| name | varchar(255) | Name of the coupon displayed to customers on for instance invoices or receipts. |
| percent_off | double(8,2) | Percent that will be taken off the subtotal of any invoices for this customer for the duration of the coupon. |
| redeem_by | timestamp NULL | Date after which coupon can no longer be redeemed. |
| valid | tinyint(1) [1] | Taking account of the above properties, whether this coupon can still be applied to a customer |
idis the promo/coupon code identifier and must be a unique string.- Coupon model is generally a 1-to-1 match of the Stripe Coupon counterpart.
- CouponObserver performs necessary Stripe Coupon updates for Coupon model lifecycle events.
Coupon.toString()is a convenience for getting a human-readable string indicating what the Coupon does (e.g.100% Off, First 3 Months).- Each Coupon must be associated with one or more Plans to have any affect. New Subscriptions generated from a Coupon-associated Plan will have the discount applied.
- Run
yarn run db:reset. WARNING: Deletes existing data, reruns migrations, and reseeds.
Disks are used to store files locally or remotely based on application site.
# Saves a file to 'local' disk.
Storage::disk('local')->put('file.txt', 'Contents');
# Check if a file exists on 'widget' disk.
Storage::disk('widget')->exists('file.jpg');local: All other files.widget: All Widget-related files.public: All publicly available files.
See: https://laravel.com/docs/5.8/filesystem
- Webhook request is made to webhook endpoint with private
token,type, andid, e.g./api/webhook?token=fae376381710061bc7052892c4485ffbc174&id=plan_HDj3chzvuez7XI&type=funnel - Request body contains some identifying information that can be used to verify the Stripe transaction. In the example below, the
id_at_gatewaycorresponds to a StripeBalanceTransactionobject id:
{
"transaction": {
"gateway": "stripe",
"id_at_gateway": "txn_1GiN2XAIFSjPGiCSPwFWEPRQ",
"status_at_gateway": "succeeded"
}
}- The
ValidateWebhookRequestmiddleware performs a series of request validations:- Checks for required
id,type, andtokenvalues. - Checks that the
idcorresponds to an object (currently onlyPlansupported). - Verifies the
tokenandidmatch. Since theidis a Stripe identifier that is publicly available elsewhere (such as via the front page) thetokenis a private identifier to reduce chance of malicious requests. typeis validated (currently onlyfunnelis supported).- Identifying data is validated.
transaction.status_at_gatewayandtransaction.gatewayare checked for success. - Lastly, the
transaction.id_at_gatewayvalue is queried from Stripe to get the associated records and ensure it is valid.
- Checks for required
- If every check passes the request is processed by the
WebhookControllerand a newLeadis generated containing:- User data retrieved from Stripe.
Plan.idmetadata.source.typeis set tostripe_chargemetadata.source.datais set to the full Stripe Charge object content, for reference later in the pipeline.
- The
WebhookLeadGeneratedevent is fired which triggers theWebhookLeadGeneratedListenerlistener. - A new
LeadToRegistrationMailis sent to the user's email address requesting them to click through to complete the registration process. This email has a valid confirmation token that expires after14 days(this can easily be adjusted). - Clicking the button in the email redirects the user to
/register. - Fields are pre-filled with available data from the Stripe transaction record.
- Once the user signs up via normal registration an activate email is sent requesting them to activate their account.
Site statistics are based on end-user device requests to /api/widget. In addition to responding with widget payload, a background event records the request within Coeus.
Text-to-Speech statistics are generated via AWS Lambda events.
- SRN:
srn:sqs:wcasg:widget:dashboard:testing::queue - URL:
https://sqs.us-west-2.amazonaws.com/696585593443/wcasg-widget-dashboard-testing-statistics
- An incoming Widget request is handled via the
/api/widgetendpoint. - Prior to payload return a new WidgetRequested event is generated.
- A RecordWidgetRequest job is dispatched via Amazon SQS.
- The
php artisan queue:work sqsqueue catches incoming Amazon SQS messages. - The
RecordWidgetRequesthandler makes an API request to Coeus as thewcasg-widget-dashboard-statisticsUser - A Coeus
/data/insertbody contains widget request and site data, e.g.:
{
"db": "wcasg",
"collection": "srn:coeus:wcasg:widget:dashboard::collection/statistics",
"document": [
{
"_id": { "$oid": "5f71ccc83e6a7a5a88612c10" },
"request": {
"bytes": 241215,
"fingerprint": "d885e8754d7fc7e1b215a7007b5135f7365c3016",
"headers": {
"host": "wcasg.test",
"connection": "keep-alive",
"origin": "http://localhost:5000",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"accept": "*/*",
"referer": "http://localhost:5000/",
"accept-encoding": "gzip, deflate",
"accept-language": "en-US,en;q=0.9"
},
"host": "wcasg.test",
"ip": "127.0.0.1",
"timestamp": 1601293510760,
"url": "http://wcasg.test/api/widget/c48014811431eaaa5c3df81d0fe6dc78b45405d3d460bbd9d9b09778c023b3ac56f79a9676073217b317a59d564a9c9200a7a1373f3897f991093f595812b4a9?XDEBUG_SESSION_START=1"
},
"site": {
"id": 1,
"domain": "localhost:84",
"active": true,
"name": "Gerlach, Robel and Treutel",
"widget_request_count": 0,
"token": "cb8fd34d1fa3731593a503f9476127f9dce1",
"created_at": "2020-09-26 08:12:20",
"updated_at": "2020-09-26 08:12:20",
"subscription_id": "sub_I5jzKUmjlmEmdo",
"statement_id": 1
},
"subscription": {
"id": "sub_I5jzKUmjlmEmdo",
"plan_id": "plan_I5jz3eT9Krfiew",
"user_id": 1,
"name": "25 Sites per month",
"stripe_status": "active",
"stripe_id": "sub_I5jzKUmjlmEmdo",
"stripe_plan": "plan_I5jz3eT9Krfiew",
"quantity": 1,
"trial_ends_at": null,
"ends_at": null,
"created_at": "2020-09-26 08:12:17",
"updated_at": "2020-09-26 08:12:17"
},
"user": {
"id": 1,
"first_name": "Gabe",
"last_name": "Wyatt",
"username": "gabestah",
"profile_image": "/img/avatar.png",
"email": "gabe@gabewyatt.com",
"phone": "606-447-4553",
"activated": 1,
"created_at": "2020-09-26 08:12:13",
"updated_at": "2020-09-26 08:12:16",
"deleted_at": null,
"stripe_id": "cus_I5jzLyc09MiV8H",
"card_brand": "mastercard",
"card_last_four": "4444",
"trial_ends_at": null,
"company_name": "Schamberger Ltd",
"address1": "7750 Tyrel Lakes",
"address2": "Suite 487",
"city": "Rolfsonside",
"state": "Maryland",
"country": "BTN",
"postal_code": "60868",
"name": "Gabe Wyatt"
}
}
]
}The following Coeus Users are registered via the /user/register endpoint:
- Basic WCASG Statistic User: Allows full
dataservice privileges to allwcasg.*resources. This User handles all Dashboard-related statistic requests.
{
"email": "dev@solarixdigital.com",
"org": "wcasg",
"password": "<password>",
"policy": {
"statement": [
{
"action": "data:*",
"constraints": [
{ "type": "maxRequests", "value": 500 },
{
"type": "ip",
"value": ["54.201.114.22", "127.0.0.1", "35.167.22.233"]
}
],
"resource": "wcasg.*"
}
],
"version": "1.1.0"
},
"username": "wcasg-widget-dashboard-statistics"
}- WCASG TTS Statistic User: Only allowed to insert data in the statistics collection. This User is invoked by AWS Lambda to handle TTS request events.
{
"email": "dev@solarixdigital.com",
"org": "wcasg",
"password": "<password>",
"policy": {
"statement": [
{
"action": "data:insert",
"constraints": [{ "type": "maxRequests", "value": 120 }],
"resource": "wcasg.srn:coeus:wcasg:widget:dashboard::collection/statistics"
}
],
"version": "1.1.0"
},
"username": "wcasg-widget-dashboard-tts"
}End-user TTS requests via the widget are passed to an Amazon API Gateway endpoint to keep internal Google TTS Service request secure. Therefore, securely generating TTS request statistics within Coeus requires a multi-stage process:
- Each WCASG Dashboard
/api/widgetAPI request now includes encryptedWcasgWidgetTTSRequestDatawith the widget payload. This data includes basic Site information and Coeus environment URL:
protected function getWidgetTTSRequestDataPayload(Site $site) {
$encrypted = '';
try {
$encrypted = Version2::encrypt(
json_encode([
'coeus' => [
'url' => config('solarix.coeus.url')
],
'request' => [
'timestamp' => (new DateTime())->getTimestamp(),
'type' => 'tts'
],
'site' => $site->toArraySimple()
]),
new SymmetricKey(config('app.paseto.secret'))
);
} catch (PasetoException $e) {
// noop
} catch (SodiumException $e) {
// noop
} finally {
return webpackify('WcasgWidgetTTSRequestData', $encrypted);
}
}- An end-user makes a TTS request via to the loaded widget, which is handled by the API Gateway endpoint (
https://9qytmg2tn1.execute-api.us-west-2.amazonaws.com/Prod). The widget adds a customX-Wcasg-Widget-Tts-Request-Dataheader to the request to identify this request for further pipeline processing. - The API Gateway handles the TTS request via the lambda-to-google-tts function.
- Along with responding to the end-user with the converted speech audio stream, a new Lambda function is invoked to record the TTS request via the Coeus API.
- If the payload is valid and can be decrypted then the srn-lambda-wcasg-widget-dashboard_coeus-insert-tts-request Lambda function makes a Coeus
/data/insertrequest and adds the TTS request record to thewcasg.srn:coeus:wcasg:widget:dashboard::collection/statisticsresource. - This request includes TTS-specific data such as audio options and input text, along with originating Site data:
{
"_id": { "$oid": "5f7a54064dc3ba58d60c4116" },
"request": {
"body": {
"audioConfig": {
"audioEncoding": "LINEAR16",
"speakingRate": 1,
"pitch": 2,
"sampleRateHertz": 24000,
"volumeGainDb": 0,
"effectsProfileId": ["handset-class-device"]
},
"input": {
"text": "\n IF THIS STORY IS to be something resembling my book of deeds, we must\n begin at the beginning. At the heart of who I truly am. To do this,\n you must remember that before I was anything else, I was one of the\n Edema Ruh.\n "
},
"voice": {
"languageCode": "en-US",
"name": "en-US-Wavenet-H",
"ssmlGender": "FEMALE"
}
},
"bytes": 747871,
"headers": {
"Host": "127.0.0.1:3000",
"Connection": "keep-alive",
"Content-Length": "527",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"Sec-Ch-Ua": "\"\\\\Not;A\\\"Brand\";v=\"99\", \"Google Chrome\";v=\"85\", \"Chromium\";v=\"85\"",
"X-Wcasg-Widget-Tts-Request-Data": "v2.local.V0zObJ9uVkQMixq1nEDP_h3h7LqYC3GXq74HrB4d56i2qSPUKQYejpcGKyp1ic6U9Do077sHqWDTh9FysJBkoLLUDN_-2W8_uKLg0HCcTrDaqcDLhUlFW1zE6Vdz2abUiG0uGDZrnsl5Mp6gRWmnvcj3aduqTK5aIw2FqL8ByGFuT3Ljnb3GpdbAQaPwGjIpNXsZXrFLsBQa-jiSCiJkrvPzH6aYRcRjyfKIbXlq5-fCjigF8HQr6ZEyUnzC6Ixly70uststpMHWF3M3JDcjTuW9KlwsnhXSUM_gToBg9ZM0YsQY4kE-fSI231XMqdx9Kje-oeJ21SaEM76u7-9iXZpYIbX9JsTWWfUtp6VOyNjGWCITgfsUcbmraqp9Zgw0iKjBO3HuSGG00oVq5W03lit-JIGnuxMMgALuZFqlUIRyIV9Q6TQ4LWqU1e_D5epcp7xsobwCfTdOEsbj3KRAbZmNsn4sikBWabCWxwKc2QH1itiP-AFZeUqo9uOWlIfs3v8VKKQM26rGw8p5RzHZmPtAW9Q0y8eqEdc",
"Sec-Ch-Ua-Mobile": "?0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"Content-Type": "application/json",
"Accept": "*/*",
"Origin": "http://localhost:5000",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": "http://localhost:5000/",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"X-Forwarded-Proto": "http",
"X-Forwarded-Port": "3000"
},
"timestamp": 1601851072,
"type": "tts"
},
"site": {
"id": 1,
"domain": "localhost:84",
"active": true,
"name": "Gerlach, Robel and Treutel",
"widget_request_count": 0,
"token": "cb8fd34d1fa3731593a503f9476127f9dce1",
"created_at": "2020-09-26 08:12:20",
"updated_at": "2020-09-26 08:12:20",
"subscription_id": "sub_I5jzKUmjlmEmdo",
"statement_id": 1
}
}- The WCASG Dashboard's StatisticsService makes a
/data/aggregateCoeus API call to retrieve both widget and TTS request statistics for the Site(s) in question.
- DB:
wcasg - Collection:
srn:coeus:wcasg:widget:dashboard::collection/statistics
The running, interactive version of the snippet. References to widget usually refer to the interactive popup that an end user manipulates to adjust personal accessibility.
The sole party working with Solarix and paying to have the widget and dashboard developed.
Any non-admin individual using the SaaS website / dashboard. Will ideally be paying for service to inject snippet into their websites as identified by unique sites.
An individual using the widget once it has been injected as a snippet into a customer's site.
Backend of SaaS website that allows both customers and admins use to adjust interact with client's SaaS business.