From 1630bae026fd1a8abe28236963fe5991793eefb6 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 16 May 2024 12:09:39 -0400
Subject: [PATCH 01/59] Add components for auth in online-survey-app

---
 online-survey-app/package.json                |   5 +-
 .../src/app/app-routing.module.ts             |   9 +-
 online-survey-app/src/app/app.module.ts       |  11 +-
 .../edit-user/edit-user.component.css         |   9 +
 .../edit-user/edit-user.component.html        |  59 +++++
 .../edit-user/edit-user.component.spec.ts     |  25 ++
 .../edit-user/edit-user.component.ts          |  78 ++++++
 .../_components/login/login.component.css     |  33 +++
 .../_components/login/login.component.html    |  31 +++
 .../_components/login/login.component.spec.ts |  25 ++
 .../auth/_components/login/login.component.ts |  58 +++++
 .../update-personal-profile.component.css     |   4 +
 .../update-personal-profile.component.html    |  68 ++++++
 .../update-personal-profile.component.spec.ts |  25 ++
 .../update-personal-profile.component.ts      |  87 +++++++
 .../update-user-role.component.css            |  16 ++
 .../update-user-role.component.html           |  16 ++
 .../update-user-role.component.spec.ts        |  25 ++
 .../update-user-role.component.ts             |  76 ++++++
 .../user-registration.component.css           |   3 +
 .../user-registration.component.html          |  70 ++++++
 .../user-registration.component.spec.ts       |  25 ++
 .../user-registration.component.ts            |  81 ++++++
 .../user-registration/user.model.interface.ts |   8 +
 .../has-a-permission.directive.spec.ts        |   8 +
 .../_directives/has-a-permission.directive.ts |  31 +++
 .../has-all-permissions.directive.spec.ts     |   8 +
 .../has-all-permissions.directive.ts          |  29 +++
 .../has-some-permissions.directive.spec.ts    |   8 +
 .../has-some-permissions.directive.ts         |  31 +++
 .../core/auth/_guards/login-guard.service.ts  |  19 ++
 .../_services/authentication.service.spec.ts  |  15 ++
 .../auth/_services/authentication.service.ts  | 230 ++++++++++++++++++
 .../core/auth/_services/user.service.spec.ts  |  15 ++
 .../app/core/auth/_services/user.service.ts   | 154 ++++++++++++
 .../src/app/core/auth/auth-routing.module.ts  |  20 ++
 .../src/app/core/auth/auth.module.ts          |  43 ++++
 .../src/app/shared/_factories/db.factory.ts   |  94 +++++++
 .../shared/_services/forms-service.service.ts |   8 +-
 .../src/app/shared/_services/menu.service.ts  |  20 ++
 .../_services/tangy-error-handler.service.ts  |  12 +
 .../shared/_services/translation-marker.ts    |   3 +
 .../app/shared/classes/app-config.class.ts    | 197 +++++++++++++++
 .../app/shared/classes/user-account.class.ts  |  12 +
 .../app/shared/classes/user-database.class.ts | 148 +++++++++++
 .../app/shared/classes/user-signup.class.ts   |  10 +
 .../src/app/shared/shared.module.ts           |  30 +++
 47 files changed, 1980 insertions(+), 12 deletions(-)
 create mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
 create mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts
 create mode 100644 online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
 create mode 100644 online-survey-app/src/app/core/auth/_services/authentication.service.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_services/authentication.service.ts
 create mode 100644 online-survey-app/src/app/core/auth/_services/user.service.spec.ts
 create mode 100644 online-survey-app/src/app/core/auth/_services/user.service.ts
 create mode 100644 online-survey-app/src/app/core/auth/auth-routing.module.ts
 create mode 100644 online-survey-app/src/app/core/auth/auth.module.ts
 create mode 100644 online-survey-app/src/app/shared/_factories/db.factory.ts
 create mode 100644 online-survey-app/src/app/shared/_services/menu.service.ts
 create mode 100644 online-survey-app/src/app/shared/_services/tangy-error-handler.service.ts
 create mode 100644 online-survey-app/src/app/shared/_services/translation-marker.ts
 create mode 100644 online-survey-app/src/app/shared/classes/app-config.class.ts
 create mode 100644 online-survey-app/src/app/shared/classes/user-account.class.ts
 create mode 100644 online-survey-app/src/app/shared/classes/user-database.class.ts
 create mode 100644 online-survey-app/src/app/shared/classes/user-signup.class.ts
 create mode 100644 online-survey-app/src/app/shared/shared.module.ts

diff --git a/online-survey-app/package.json b/online-survey-app/package.json
index 3792ce7c22..feb1c86b68 100644
--- a/online-survey-app/package.json
+++ b/online-survey-app/package.json
@@ -21,7 +21,10 @@
     "@angular/platform-browser": "~10.1.0",
     "@angular/platform-browser-dynamic": "~10.1.0",
     "@angular/router": "~10.1.0",
+    "@ngx-translate/core": "^11.0.1",
+    "@ngx-translate/http-loader": "^4.0.0",
     "@webcomponents/webcomponentsjs": "^2.4.4",
+    "jwt-decode": "^4.0.0",
     "material-design-icons-iconfont": "^6.1.0",
     "redux": "^4.0.5",
     "rxjs": "~6.6.0",
@@ -34,9 +37,9 @@
     "@angular-devkit/build-angular": "~0.1001.0",
     "@angular/cli": "~10.1.0",
     "@angular/compiler-cli": "~10.1.0",
-    "@types/node": "^12.11.1",
     "@types/jasmine": "~3.5.0",
     "@types/jasminewd2": "~2.0.3",
+    "@types/node": "^12.11.1",
     "codelyzer": "^6.0.0",
     "jasmine-core": "~3.6.0",
     "jasmine-spec-reporter": "~5.0.0",
diff --git a/online-survey-app/src/app/app-routing.module.ts b/online-survey-app/src/app/app-routing.module.ts
index 1870aea915..840288ee4b 100644
--- a/online-survey-app/src/app/app-routing.module.ts
+++ b/online-survey-app/src/app/app-routing.module.ts
@@ -3,12 +3,13 @@ import { Routes, RouterModule } from '@angular/router';
 import { FormSubmittedSuccessComponent } from './form-submitted-success/form-submitted-success.component';
 import { FormsListComponent } from './forms-list/forms-list.component';
 import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
+import { LoginGuard } from './core/auth/_guards/login-guard.service';
 
 const routes: Routes = [
-  //{ path: '', component: FormsListComponent },
-  { path: 'form-submitted-success', component: FormSubmittedSuccessComponent },
-  { path: 'form/:id', component: TangyFormsPlayerComponent },
-  { path: 'form/option/:formId/:option', component: TangyFormsPlayerComponent }
+  { path: 'forms-list', component: FormsListComponent, canActivate: [LoginGuard] },
+  { path: 'form-submitted-success', component: FormSubmittedSuccessComponent, canActivate: [LoginGuard] },
+  { path: 'form/:id', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
+  { path: 'form/option/:formId/:option', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
 ];
 
 @NgModule({
diff --git a/online-survey-app/src/app/app.module.ts b/online-survey-app/src/app/app.module.ts
index 54277c80a5..8080a7af5b 100644
--- a/online-survey-app/src/app/app.module.ts
+++ b/online-survey-app/src/app/app.module.ts
@@ -3,6 +3,7 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
 
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
+import { AuthModule } from './core/auth/auth.module';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import {MatToolbarModule} from '@angular/material/toolbar';
 import {MatCardModule} from '@angular/material/card';
@@ -10,11 +11,13 @@ import {MatTabsModule} from '@angular/material/tabs';
 import {MatMenuModule} from '@angular/material/menu';
 import {MatListModule} from '@angular/material/list';
 import { MatIconModule } from '@angular/material/icon';
+import { MatSnackBarModule } from '@angular/material/snack-bar';
 import { TangySvgLogoComponent } from './shared/tangy-svg-logo/tangy-svg-logo.component';
 import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
 import { HttpClientModule } from '@angular/common/http';
 import { FormsListComponent } from './forms-list/forms-list.component';
 import { FormSubmittedSuccessComponent } from './form-submitted-success/form-submitted-success.component';
+import { TangyErrorHandler } from './shared/_services/tangy-error-handler.service';
 
 @NgModule({
   declarations: [
@@ -25,8 +28,9 @@ import { FormSubmittedSuccessComponent } from './form-submitted-success/form-sub
     FormSubmittedSuccessComponent
   ],
   imports: [
-    BrowserModule,
+    AuthModule,
     AppRoutingModule,
+    BrowserModule,
     BrowserAnimationsModule,
     MatToolbarModule,
     MatMenuModule,
@@ -34,10 +38,11 @@ import { FormSubmittedSuccessComponent } from './form-submitted-success/form-sub
     HttpClientModule,
     MatCardModule,
     MatTabsModule,
-    MatListModule
+    MatListModule,
+    MatSnackBarModule
   ],
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
-  providers: [],
+  providers: [TangyErrorHandler],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
new file mode 100644
index 0000000000..7e475170aa
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
@@ -0,0 +1,9 @@
+form {
+  margin: 20px;
+}
+
+:host() {
+
+  display: block;
+  padding-right: 45px;
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
new file mode 100644
index 0000000000..99bdd90ae9
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
@@ -0,0 +1,59 @@
+<div *ngIf="user">
+      <h1 class="tangy-foreground-secondary">
+        {{'Edit User'|translate}} {{user.username}}
+      </h1>
+        <form class="tangy-full-width" novalidate #editUserForm="ngForm">
+          
+          <mat-form-field class="tangy-full-width">
+            <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
+              required>
+              <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
+                {{'This Field is Required'|translate}}
+              </mat-error>
+          </mat-form-field>
+          <br>
+          <br>
+          <mat-form-field class="tangy-full-width">
+            <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
+              required>
+              <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
+                {{'This Field is Required'|translate}}
+              </mat-error>
+          </mat-form-field>
+          <br>
+          <br>
+          <mat-form-field class="tangy-full-width">
+            <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
+              matInput placeholder="{{'Email'|translate}}" required>
+            <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
+              {{'Please enter a valid email address'|translate}}
+            </mat-error>
+          </mat-form-field>
+          <br>
+          <br>
+          <mat-checkbox [(ngModel)]="updateUserPassword" name="updateUserPassword">Update User password?</mat-checkbox>
+          <br>
+          <br>
+          <div *ngIf="updateUserPassword">
+            <mat-form-field class="tangy-full-width">
+              <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'Password'|translate}}"
+                required>
+            </mat-form-field>
+            <br>
+            <br>
+            <mat-form-field class="tangy-full-width">
+              <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
+                required>
+              <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
+                {{'Passwords do not match'|translate}}
+              </mat-error>
+            </mat-form-field>
+          </div>
+          <span [id]="statusMessage.type" *ngIf="statusMessage.type==='error'">
+            <small>{{statusMessage.message}}</small>
+          </span>
+          <p>
+            <button [disabled]="editUserForm.invalid||(user.confirmPassword!==user.password)" mat-raised-button color="warn" (click)="editUser()">{{'UPDATE USER'|translate}}</button>
+          </p>
+        </form>
+    </div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
new file mode 100644
index 0000000000..3702bd7a39
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { EditUserComponent } from './edit-user.component';
+
+describe('EditUserComponent', () => {
+  let component: EditUserComponent;
+  let fixture: ComponentFixture<EditUserComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ EditUserComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(EditUserComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
new file mode 100644
index 0000000000..d261999573
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
@@ -0,0 +1,78 @@
+import { Component, OnInit } from '@angular/core';
+import { User } from '../user-registration/user.model.interface';
+import { UserService } from '../../_services/user.service';
+import { ActivatedRoute, Router } from '@angular/router';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import {HttpClient} from "@angular/common/http";
+
+@Component({
+  selector: 'app-edit-user',
+  templateUrl: './edit-user.component.html',
+  styleUrls: ['./edit-user.component.css']
+})
+export class EditUserComponent implements OnInit {
+  user: User;
+  updateUserPassword = false;
+  statusMessage = { type: '', message: '' };
+  disableSubmit = false;
+  passwordIsNotStrong = { type: 'error', message: _TRANSLATE('Password is not strong enough.') };
+  incorrectAdminPassword = { type: 'error', message: _TRANSLATE('Incorrect Admin Password') };
+  passwordPolicy: string
+  passwordRecipe: string
+  constructor(
+    private userService: UserService,
+    private route: ActivatedRoute,
+    private errorHandler: TangyErrorHandler,
+    private router: Router,
+    private httpClient: HttpClient
+  ) { }
+
+  async ngOnInit() {
+    this.user = await this.userService.getAUserByUsername(this.route.snapshot.paramMap.get('username')) as User;
+    this.user.password = '';
+    this.user.confirmPassword = '';
+    try {
+      const result: any = await this.httpClient.get('/configuration/passwordPolicyConfig').toPromise();
+      if(result.T_PASSWORD_POLICY){
+        this.passwordPolicy = result.T_PASSWORD_POLICY;
+      }
+      if(result.T_PASSWORD_RECIPE){
+        this.passwordRecipe = result.T_PASSWORD_RECIPE;
+      }
+    } catch (error) {
+      if (typeof error.status === 'undefined') {
+        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+      }
+    }
+    this.passwordIsNotStrong.message = this.passwordIsNotStrong.message + ' ' + this.passwordRecipe
+  }
+
+  async editUser() {
+    try {
+      if (!this.updateUserPassword) {
+        this.user.password = null;
+        this.user.confirmPassword = null;
+      }
+      const policy = new RegExp(this.passwordPolicy)
+      if (!policy.test(this.user.password)) {
+        this.statusMessage = this.passwordIsNotStrong
+        // this.disableSubmit = true
+        // this.errorHandler.handleError(this.passwordIsNotStrong.message);
+        return
+      } else {
+        // Clear out statusMessage is it had been set earlier.
+        this.statusMessage = { type: '', message: '' };
+      }
+      const data = await this.userService.updateUserDetails({...this.user, updateUserPassword: this.updateUserPassword});
+      if (data === 200) {
+        this.errorHandler.handleError(_TRANSLATE('User Details Updated Successfully'));
+        this.router.navigate(['users'])
+      } else {
+        this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
+      }
+    } catch (error) {
+      this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
+    }
+  }
+}
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.css b/online-survey-app/src/app/core/auth/_components/login/login.component.css
new file mode 100644
index 0000000000..e347211a77
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.css
@@ -0,0 +1,33 @@
+mat-card {
+  width: 300px;
+  margin: 30px auto;
+}
+.mat-title {
+  color: var(--primary-color);
+  font-size: 1.5em;
+  font-weight: 400;
+  font-family: Roboto, 'Helvetica Nue', sans-serif;
+}
+#logo {
+  margin-bottom: 15px;
+}
+mat-placeholder i {
+  margin-right: 0.075em;
+}
+
+input.mat-input-element {
+  margin-top: 1em;
+}
+
+.mat-placeholder-required.mat-form-field-required-marker.ng-tns-c6-2
+  .mat-form-field-invalid
+  .mat-form-field-placeholder.mat-accent,
+.mat-form-field-invalid
+  .mat-form-field-placeholder
+  .mat-form-field-required-marker {
+  position: relative;
+  bottom: 1em !important;
+}
+.mat-form-field {
+  width: 100%;
+}
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.html b/online-survey-app/src/app/core/auth/_components/login/login.component.html
new file mode 100644
index 0000000000..839e51e028
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.html
@@ -0,0 +1,31 @@
+<mat-card *ngIf="!ready">
+  Loading...
+</mat-card>
+<mat-card *ngIf="ready">
+    <form role="form" #login='ngForm' novalidate>
+      <img id="logo" src="/logo.png" width="100%">
+      <mat-form-field>
+        <input matInput type="text" required [(ngModel)]="user.username" id="username" name="username">
+        <mat-placeholder>
+          <i class="material-icons app-input-icon">face</i>
+          <span>{{'Username'|translate}}</span>
+        </mat-placeholder>
+      </mat-form-field>
+      <br>
+      <mat-form-field>
+        <input matInput type="password" required [(ngModel)]="user.password" id="password" name="password">
+        <mat-placeholder>
+          <i class="material-icons app-input-icon">lock_open</i>
+          <span>{{'Password'|translate}}</span>
+        </mat-placeholder>
+      </mat-form-field>
+      <br>
+      <button (click)="loginUser()" mat-raised-button color="accent" name="action">{{'LOGIN'|translate}}</button>
+      <!-- <a [routerLink]="['/forgot-password']" mat-button color="primary">{{'Forgot Password?'|translate}}</a> -->
+      <a [routerLink]="['/register-user']" mat-button color="primary">{{'Register'|translate}}</a>
+      <span id="err">
+        <small>{{errorMessage}}</small>
+      </span>
+    </form>
+</mat-card>
+<span #customLoginMarkup></span>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
new file mode 100644
index 0000000000..d6d85a8465
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ LoginComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
new file mode 100644
index 0000000000..4ef807c3f9
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
@@ -0,0 +1,58 @@
+import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AuthenticationService } from '../../_services/authentication.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+
+
+@Component({
+  selector: 'app-login',
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+
+  errorMessage = '';
+  returnUrl: string; // stores the value of the url to redirect to after login
+  user = { username: '', password: '' };
+  @ViewChild('customLoginMarkup', {static: true}) customLoginMarkup: ElementRef;
+  ready = false
+
+  constructor(
+    private authenticationService: AuthenticationService,
+    private route: ActivatedRoute,
+    private router: Router,
+  ) { }
+
+  async ngOnInit() {
+    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || 'forms-list';
+
+    if (await this.authenticationService.isLoggedIn()) {
+      this.router.navigate([this.returnUrl]);
+    }
+    // We always want to log in from the "front door". If we log in from path of `/app/<groupId>`, then the global cookie
+    // will be tied to that pathname and not available at other pathnames such as `/csv/` that is looking for authentication.
+    if (window.location.pathname !== '/') { 
+      window.location.pathname = '/'
+    }
+    this.customLoginMarkup.nativeElement.innerHTML = await this.authenticationService.getCustomLoginMarkup()
+    this.ready = true
+  }
+  async loginUser() {
+    try {
+
+      if (window.location.origin.startsWith('http://localhost')) {
+        // If we are running on localhost, we want to use the local server for authentication 
+        this.router.navigate([this.returnUrl]);
+  
+      } else if (await this.authenticationService.login(this.user.username, this.user.password)) {
+        this.router.navigate([this.returnUrl]);
+      } else {
+        this.errorMessage = _TRANSLATE('Login Unsuccesful');
+      }
+    } catch (error) {
+      this.errorMessage = _TRANSLATE('Login Unsuccesful');
+      console.error(error);
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
new file mode 100644
index 0000000000..09e9b04a15
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
@@ -0,0 +1,4 @@
+#container {
+  margin: 0px 15px;
+
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
new file mode 100644
index 0000000000..dea5885145
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
@@ -0,0 +1,68 @@
+<h1 class="tangy-foreground-secondary" *ngIf="user; else elseBlock">
+  {{'Edit User'|translate }} {{user.username}}
+</h1>
+<ng-template #elseBlock>
+  <h1 class="tangy-foreground-secondary">
+    {{'Edit User'|translate }}
+  </h1>
+</ng-template>
+<div id="container" *ngIf="user">
+  <form class="tangy-full-width" novalidate #editUserForm="ngForm">
+    
+    <mat-form-field class="tangy-full-width">
+      <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
+        required>
+        <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
+          {{'This Field is Required'|translate}}
+        </mat-error>
+    </mat-form-field>
+    <br>
+    <br>
+    <mat-form-field class="tangy-full-width">
+      <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
+        required>
+        <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
+          {{'This Field is Required'|translate}}
+        </mat-error>
+    </mat-form-field>
+    <br>
+    <br>
+    <mat-form-field class="tangy-full-width">
+      <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
+        matInput placeholder="{{'Email'|translate}}" required>
+      <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
+        {{'Please enter a valid email address'|translate}}
+      </mat-error>
+    </mat-form-field>
+    <br>
+    <br>
+    <mat-checkbox [(ngModel)]="updateUserPassword" name="updateUserPassword">Update User password?</mat-checkbox>
+    <br>
+    <br>
+    <div *ngIf="updateUserPassword">
+      <mat-form-field class="tangy-full-width">
+        <input name="password" type="password" [(ngModel)]="user.currentPassword" #currentPassword="ngModel" matInput placeholder="{{'Current Password'|translate}}"
+          required>
+      </mat-form-field>
+      <mat-form-field class="tangy-full-width">
+        <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'New Password'|translate}}"
+          required>
+      </mat-form-field>
+      <br>
+      <br>
+      <mat-form-field class="tangy-full-width">
+        <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
+          required>
+        <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
+          {{'Passwords do not match'|translate}}
+        </mat-error>
+      </mat-form-field>
+    </div>
+    <span [id]="statusMessage.type" *ngIf="statusMessage.type==='error'">
+        <small>{{statusMessage.message}}</small>
+    </span>
+    <p>
+    <button [disabled]="editUserForm.invalid||(user.confirmPassword!==user.password)||disableSubmit" mat-raised-button color="warn" (click)="editUser()">{{'UPDATE DETAILS'|translate}}</button>
+    </p>
+  </form>
+</div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
new file mode 100644
index 0000000000..e3bb688e39
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpdatePersonalProfileComponent } from './update-personal-profile.component';
+
+describe('UpdatePersonalProfileComponent', () => {
+  let component: UpdatePersonalProfileComponent;
+  let fixture: ComponentFixture<UpdatePersonalProfileComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UpdatePersonalProfileComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UpdatePersonalProfileComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
new file mode 100644
index 0000000000..12dee61574
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
@@ -0,0 +1,87 @@
+import { Router } from '@angular/router';
+import { Component, OnInit } from '@angular/core';
+import { MenuService } from 'src/app/shared/_services/menu.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import { UserService } from '../../_services/user.service';
+import { User } from '../user-registration/user.model.interface';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import {HttpClient} from "@angular/common/http";
+
+@Component({
+  selector: 'app-update-personal-profile',
+  templateUrl: './update-personal-profile.component.html',
+  styleUrls: ['./update-personal-profile.component.css']
+})
+export class UpdatePersonalProfileComponent implements OnInit {
+
+  user;
+  updateUserPassword = false;
+  statusMessage = { type: '', message: '' };
+  disableSubmit = false;
+  passwordIsNotStrong = { type: 'error', message: _TRANSLATE('Password is not strong enough.') };
+  incorrectAdminPassword = { type: 'error', message: _TRANSLATE('Incorrect Admin Password') };
+  passwordPolicy: string
+  passwordRecipe: string
+  config:any = { enabledModules: [] }
+  
+  constructor(
+    private menuService: MenuService,
+    private userService: UserService,
+    private errorHandler: TangyErrorHandler,
+    private router: Router,
+    private httpClient: HttpClient
+  ) { }
+
+  async ngOnInit() {
+    this.menuService.setContext(_TRANSLATE('Update My Profile'));
+    this.user = await this.userService.getMyUser();
+    this.user.password = '';
+    this.user.confirmPassword = '';
+    try {
+      const result: any = await this.httpClient.get('/configuration/passwordPolicyConfig').toPromise();
+      if(result.T_PASSWORD_POLICY){
+        this.passwordPolicy = result.T_PASSWORD_POLICY;
+      }
+      if(result.T_PASSWORD_RECIPE){
+        this.passwordRecipe = result.T_PASSWORD_RECIPE;
+      }
+    } catch (error) {
+      if (typeof error.status === 'undefined') {
+        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+      }
+    }
+    this.passwordIsNotStrong.message = this.passwordIsNotStrong.message + ' ' + this.passwordRecipe
+  }
+  async editUser() {
+    this.statusMessage = { type: '', message: '' };
+    try {
+      if (!this.updateUserPassword) {
+        this.user.password = null;
+        this.user.confirmPassword = null;
+        this.user.currentPassword = null;
+      }
+
+      // const policy = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})');
+      const policy = new RegExp(this.passwordPolicy)
+      if (!policy.test(this.user.password)) {
+        this.statusMessage = this.passwordIsNotStrong
+        // this.disableSubmit = true
+        // this.errorHandler.handleError(this.passwordIsNotStrong.message);
+        return
+      } else {
+        // Clear out statusMessage is it had been set earlier.
+        this.statusMessage = { type: '', message: '' };
+      }
+      
+      const data = await this.userService.updateMyUser({...this.user, updateUserPassword: this.updateUserPassword});
+      if (data === 200) {
+        this.errorHandler.handleError(_TRANSLATE('User Details Updated Successfully'));
+        this.router.navigate(['/'])
+      } else {
+        this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
+      }
+    } catch (error) {
+      this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
+    }
+  }
+}
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
new file mode 100644
index 0000000000..03834b16a8
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
@@ -0,0 +1,16 @@
+.tangy-input-width{
+    width: 80%;
+}
+
+p {
+    margin: 15px;
+}
+
+mat-checkbox{
+    display: block;
+    margin: 0 10px;
+}
+
+button {
+    margin-left: 15px;
+}
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
new file mode 100644
index 0000000000..81450a32a7
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
@@ -0,0 +1,16 @@
+<app-breadcrumb [title]="title" [breadcrumbs]="breadcrumbs"></app-breadcrumb>
+<form class="tangy-full-width" novalidate #updateUserRoleForm="ngForm">
+    <div>
+        <p>{{'Update Role of' | translate}}: {{username}}</p>
+
+    </div>
+    <p *ngIf="allRoles" class="tangy-input-width">
+        <mat-checkbox #f (change)="onSelectChange(role.role, f.checked)"
+            [checked]="doesUserHaveRole(role.role)" *ngFor="let role of allRoles" [name]="role.role">
+            {{role.role}}
+        </mat-checkbox>
+    </p>
+    <p *ngIf="allRoles.length<1" class="tangy-input-width">{{'No Roles exist yet. '|translate}}</p>
+    <button [disabled]="!username" mat-raised-button color="warn"
+        (click)="addUserToGroup();updateUserRoleForm.reset()">{{"submit"|translate}}</button>
+</form>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
new file mode 100644
index 0000000000..1db6f21c83
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpdateUserRoleComponent } from './update-user-role.component';
+
+describe('UpdateUserRoleComponent', () => {
+  let component: UpdateUserRoleComponent;
+  let fixture: ComponentFixture<UpdateUserRoleComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UpdateUserRoleComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UpdateUserRoleComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
new file mode 100644
index 0000000000..497de34091
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
@@ -0,0 +1,76 @@
+import { Component, OnInit, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
+import { Breadcrumb } from 'src/app/shared/_components/breadcrumb/breadcrumb.component';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import { ActivatedRoute, Router } from '@angular/router';
+import { GroupsService } from 'src/app/groups/services/groups.service';
+import { UserService } from '../../_services/user.service';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import { AuthenticationService } from '../../_services/authentication.service';
+
+@Component({
+  selector: 'app-update-user-role',
+  templateUrl: './update-user-role.component.html',
+  styleUrls: ['./update-user-role.component.css']
+})
+export class UpdateUserRoleComponent implements OnInit {
+
+  user;
+  username;
+  role;
+  groupId;
+  title = _TRANSLATE('Update User\'s roles');
+  breadcrumbs: Array<Breadcrumb> = [];
+  allRoles;
+  myGroup;
+  @ViewChild('search', { static: false }) search: ElementRef;
+  constructor(
+    private route: ActivatedRoute,
+    private groupsService: GroupsService,
+    private usersService: UserService,
+    private errorHandler: TangyErrorHandler,
+    private router: Router,
+    private authenticationService: AuthenticationService
+  ) { }
+
+  async ngOnInit() {
+    this.breadcrumbs = [
+      <Breadcrumb>{
+        label: _TRANSLATE('Security'),
+        url: `security`
+      },
+      <Breadcrumb>{
+        label: _TRANSLATE(`Update User's Roles`),
+        url: `security/assign-role`
+      }
+    ];
+    this.username = this.route.snapshot.paramMap.get('username');
+    this.groupId = this.route.snapshot.paramMap.get('groupId');
+    this.allRoles = await this.authenticationService.getAllRoles(this.groupId);
+    this.user = await this.usersService.getAUserByUsername(this.username);
+    this.myGroup = this.user.groups.find(g => g.groupName === this.groupId);
+    this.myGroup.roles = this.myGroup?.roles ?? [];
+    this.role = { groupName: this.groupId, roles: this.myGroup.roles };
+  }
+
+  async addUserToGroup() {
+    try {
+      await this.groupsService.addUserToGroup(this.groupId, this.username, this.role);
+      this.errorHandler.handleError(_TRANSLATE('User Added to Group Successfully'));
+      this.router.navigate([`groups/${this.groupId}`]);
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
+  doesUserHaveRole(role) {
+    return this.myGroup.roles.indexOf(role) >= 0;
+  }
+  onSelectChange(role, value) {
+    if (value) {
+      this.role.roles = [...new Set([...this.role.roles, role])];
+    } else {
+      this.role.roles = this.role.roles.filter(perm => perm !== role);
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
new file mode 100644
index 0000000000..6c157bdf31
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
@@ -0,0 +1,3 @@
+.tangy-full-width {
+  width: 98%;
+}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
new file mode 100644
index 0000000000..f80e1a10f4
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
@@ -0,0 +1,70 @@
+<div class="tangy-content-top-margin">
+<mat-card>
+  <mat-card-title class="tangy-foreground-secondary">
+    {{'Add New User'|translate}}
+  </mat-card-title>
+  <mat-card-content>
+    <form class="tangy-full-width" novalidate #registrationForm="ngForm">
+      <mat-form-field class="tangy-full-width">
+        <input name="username" [(ngModel)]="user.username" #username="ngModel" matInput placeholder="{{'Username'|translate}}" (blur)="doesUserExist(user.username)"
+          required>
+        <br>
+        <br>
+        <span>
+          <mat-error *ngIf="userExists" style="font-size:75%;"> {{'Username Unavailable'|translate}}</mat-error>
+          <mat-error *ngIf="(userExists!==null&&!userExists)&&(username.dirty || username.touched)" style="font-size:75%;color: green"> {{'Username Available'|translate}}</mat-error>
+          <mat-error *ngIf="(username.invalid||username.errors) && (username.dirty || username.touched)">
+            {{'This Field is Required'|translate}}
+          </mat-error>
+        </span>
+      </mat-form-field>
+      <br>
+      <br>
+
+      <mat-form-field class="tangy-full-width">
+        <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
+          required>
+          <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
+            {{'This Field is Required'|translate}}
+          </mat-error>
+      </mat-form-field>
+      <br>
+      <br>
+      <mat-form-field class="tangy-full-width">
+        <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
+          required>
+          <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
+            {{'This Field is Required'|translate}}
+          </mat-error>
+      </mat-form-field>
+      <br>
+      <br>
+      <mat-form-field class="tangy-full-width">
+        <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
+          matInput placeholder="{{'Email'|translate}}" required>
+        <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
+          {{'Please enter a valid email address'|translate}}
+        </mat-error>
+      </mat-form-field>
+      <br>
+      <br>
+      <mat-form-field class="tangy-full-width">
+        <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'Password'|translate}}"
+          required>
+      </mat-form-field>
+      <br>
+      <br>
+      <mat-form-field class="tangy-full-width">
+        <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
+          required>
+        <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
+          {{'Passwords do not match'|translate}}
+        </mat-error>
+      </mat-form-field>
+      <br>
+      <br>
+      <button [disabled]="registrationForm.invalid||userExists" mat-raised-button color="warn" (click)="createUser();registrationForm.reset()">{{'REGISTER'|translate}}</button>
+    </form>
+  </mat-card-content>
+</mat-card>
+</div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
new file mode 100644
index 0000000000..39b3cb325f
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserRegistrationComponent } from './user-registration.component';
+
+describe('UserRegistrationComponent', () => {
+  let component: UserRegistrationComponent;
+  let fixture: ComponentFixture<UserRegistrationComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UserRegistrationComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserRegistrationComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
new file mode 100644
index 0000000000..48bd0d4359
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
@@ -0,0 +1,81 @@
+import { Component, OnInit, AfterContentInit } from '@angular/core';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import { UserService } from '../../_services/user.service';
+import { User } from './user.model.interface';
+import {ActivatedRoute} from '@angular/router';
+
+
+@Component({
+  selector: 'app-user-registration',
+  templateUrl: './user-registration.component.html',
+  styleUrls: ['./user-registration.component.css']
+})
+export class UserRegistrationComponent implements OnInit, AfterContentInit {
+
+  user: User = {
+    username: '',
+    password: '',
+    email: '',
+    firstName: '',
+    lastName: ''
+  };
+  userExists: boolean = null;
+  userNameUnavailableMessage = { type: 'error', message: _TRANSLATE('Username Unavailable') };
+  userNameAvailableMessage = { type: 'success', message: _TRANSLATE('Username Available') };
+  couldNotCreateUserMessage = { type: 'error', message: _TRANSLATE('Could Not Create User') };
+  id: string;
+
+  constructor(private userService: UserService, private errorHandler: TangyErrorHandler, private route: ActivatedRoute) { }
+
+  ngOnInit() {
+  }
+
+  async ngAfterContentInit() {
+    this.route.params.subscribe(async params => {
+      this.id = params.id;
+      // await this.userService.doesUserExist(this.id);
+    })
+  }
+
+  async createUser() {
+    try {
+      delete this.user.confirmPassword;
+      const userData = Object.assign({}, this.user);
+      if (!(await this.doesUserExist(this.user.username))) {
+        const result: any = await this.userService.createUser(userData);
+        if (result && result.statusCode && result.statusCode === 200) {
+          this.errorHandler.handleError(_TRANSLATE('User Created Succesfully'));
+          this.user = {
+            username: '',
+            password: '',
+            email: '',
+            firstName: '',
+            lastName: ''
+          };
+        } else {
+          this.errorHandler.handleError(_TRANSLATE('Could Not Create User'));
+        }
+        return result;
+      }
+    } catch (error) {
+      this.errorHandler.handleError(_TRANSLATE('Could Not Create User'));
+    }
+  }
+
+  async doesUserExist(username: string) {
+    try {
+      this.user.username = username.replace(/\s/g, ''); // Remove all whitespaces including spaces and tabs
+      if (this.user.username.length > 0) {
+        this.userExists = <boolean>await this.userService.doesUserExist(username);
+        return this.userExists;
+      } else {
+        this.userExists = null;
+        return this.userExists;
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
new file mode 100644
index 0000000000..3346f356e9
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
@@ -0,0 +1,8 @@
+export interface User {
+    username: string;
+    password: string;
+    email: string;
+    firstName:string;
+    lastName:string;
+    confirmPassword?: string;
+}
diff --git a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
new file mode 100644
index 0000000000..403650b0ba
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
@@ -0,0 +1,8 @@
+import { HasAPermissionDirective } from './has-a-permission.directive';
+
+describe('HasAPermissionDirective', () => {
+  it('should create an instance', () => {
+    const directive = new HasAPermissionDirective();
+    expect(directive).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
new file mode 100644
index 0000000000..1204beeb01
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
@@ -0,0 +1,31 @@
+import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
+import { AuthenticationService } from '../_services/authentication.service';
+
+@Directive({
+  selector: '[appHasAPermission]'
+})
+export class HasAPermissionDirective implements OnInit {
+  private _groupId;
+  private _permission;
+  @Input()
+  set appHasAPermissionGroup(value: string) {
+    this._groupId = value;
+  }
+  @Input()
+  set appHasAPermissionPermission(value: string[]) {
+    this._permission = value;
+  }
+
+  constructor(private authenticationService: AuthenticationService,
+    private templateRef: TemplateRef<any>,
+    private viewContainer: ViewContainerRef) { }
+  async ngOnInit() {
+    const isPermitted = await this.authenticationService.doesUserHaveAPermission(this._groupId, this._permission);
+    if (isPermitted) {
+      this.viewContainer.createEmbeddedView(this.templateRef);
+    } else {
+      this.viewContainer.clear();
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
new file mode 100644
index 0000000000..00447ee7b3
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
@@ -0,0 +1,8 @@
+import { HasAllPermissionsDirective } from './has-all-permissions.directive';
+
+describe('HasAllPermissionsDirective', () => {
+  it('should create an instance', () => {
+    const directive = new HasAllPermissionsDirective();
+    expect(directive).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
new file mode 100644
index 0000000000..4f19f18b07
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
@@ -0,0 +1,29 @@
+import { Directive, Input, AfterViewInit, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
+import { AuthenticationService } from '../_services/authentication.service';
+@Directive({
+  selector: '[appHasAllPermissions]'
+})
+export class HasAllPermissionsDirective implements OnInit {
+  private _groupId;
+  private _permissions;
+  @Input()
+  set appHasAllPermissionsGroup(value: string) {
+    this._groupId = value;
+  }
+  @Input()
+  set appHasAllPermissionsPermissions(value: string[]) {
+    this._permissions = value;
+  }
+
+  constructor(private authenticationService: AuthenticationService,
+    private templateRef: TemplateRef<any>,
+    private viewContainer: ViewContainerRef) { }
+  async ngOnInit() {
+    const isPermitted = await this.authenticationService.doesUserHaveAllPermissions(this._groupId, this._permissions);
+    if (isPermitted) {
+      this.viewContainer.createEmbeddedView(this.templateRef);
+    } else {
+      this.viewContainer.clear();
+    }
+  }
+}
diff --git a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
new file mode 100644
index 0000000000..83187b8309
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
@@ -0,0 +1,8 @@
+import { HasSomePermissionsDirective } from './has-some-permissions.directive';
+
+describe('HasSomePermissionsDirective', () => {
+  it('should create an instance', () => {
+    const directive = new HasSomePermissionsDirective();
+    expect(directive).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts
new file mode 100644
index 0000000000..e1dcdbcd77
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts
@@ -0,0 +1,31 @@
+import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
+import { AuthenticationService } from '../_services/authentication.service';
+
+@Directive({
+  selector: '[appHasSomePermissions]'
+})
+export class HasSomePermissionsDirective implements OnInit {
+  private _groupId;
+  private _permissions;
+  @Input()
+  set appHasSomePermissionsGroup(value: string) {
+    this._groupId = value;
+  }
+  @Input()
+  set appHasSomePermissionsPermissions(value: string[]) {
+    this._permissions = value;
+  }
+
+  constructor(private authenticationService: AuthenticationService,
+    private templateRef: TemplateRef<any>,
+    private viewContainer: ViewContainerRef) { }
+  async ngOnInit() {
+    const isPermitted = await this.authenticationService.doesUserHaveSomePermissions(this._groupId, this._permissions);
+    if (isPermitted) {
+      this.viewContainer.createEmbeddedView(this.templateRef);
+    } else {
+      this.viewContainer.clear();
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
new file mode 100644
index 0000000000..1b48299f84
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+import { AuthenticationService } from '../_services/authentication.service';
+
+@Injectable()
+export class LoginGuard implements CanActivate {
+
+  constructor(
+    private router: Router,
+    private authenticationService: AuthenticationService
+  ) { }
+  async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    if (await this.authenticationService.isLoggedIn()) {
+      return true;
+    }
+    this.router.navigate([route, 'login'], { queryParams: { returnUrl: state.url } });
+    return false;
+  }
+}
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.spec.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.spec.ts
new file mode 100644
index 0000000000..0d04c8a68a
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { AuthenticationService } from './authentication.service';
+
+describe('AuthenticationService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [AuthenticationService]
+    });
+  });
+
+  it('should be created', inject([AuthenticationService], (service: AuthenticationService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
new file mode 100644
index 0000000000..90395ebd19
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -0,0 +1,230 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { UserService } from './user.service';
+import { Subject } from 'rxjs';
+import { jwtDecode } from 'jwt-decode';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+
+@Injectable()
+export class AuthenticationService {
+  public currentUserLoggedIn$: any;
+  private _currentUserLoggedIn: boolean;
+  constructor(private userService: UserService, private http: HttpClient, private errorHandler: TangyErrorHandler) {
+    this.currentUserLoggedIn$ = new Subject();
+  }
+  async login(username: string, password: string) {
+    try {
+     const data = await this.http.post('/login', {username, password}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        const token = data.body['data']['token'];
+        await this.setTokens(token);
+        return true;
+      } else {
+        return false;
+      }
+    } catch (error) {
+      console.error(error);
+      localStorage.removeItem('token');
+      localStorage.removeItem('user_id');
+      localStorage.removeItem('permissions');
+      return false;
+    }
+  }
+  async isLoggedIn():Promise<boolean> {
+
+    if (window.location.origin.startsWith('http://localhost')) {
+      localStorage.setItem('user_id', 'testuser');
+  }
+
+    this._currentUserLoggedIn = false;
+    this._currentUserLoggedIn = !!localStorage.getItem('user_id');
+    this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
+    return this._currentUserLoggedIn;
+  }
+
+  async validateSession():Promise<boolean> {
+    const status = await this.http.get(`/login/validate/${localStorage.getItem('user_id')}`).toPromise()
+    return status['valid']
+  }
+
+
+  async logout() {
+    await localStorage.removeItem('token');
+    await localStorage.removeItem('user_id');
+    await localStorage.removeItem('password');
+    localStorage.removeItem('permissions');
+    document.cookie = "Authorization=;max-age=-1";
+    this._currentUserLoggedIn = false;
+    this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
+  }
+
+  async extendUserSession() {
+    const username = localStorage.getItem('user_id');
+    try {
+      const data = await this.http.post('/extendSession', {username}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        const token = data.body['data']['token'];
+       await this.setTokens(token);
+        return true;
+      } else {
+        return false;
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
+  async setTokens(token) {
+    const jwtData = jwtDecode(token);
+    document.cookie = "Authorization=;max-age=-1";
+    localStorage.setItem('token', token);
+    localStorage.setItem('user_id', jwtData['username']);
+    localStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
+    document.cookie = `Authorization=${token}`;
+
+    const user = await this.userService.getMyUser();
+    window['userProfile'] = user;
+    window['userId'] = user._id;
+    window['username'] = jwtData['username'];
+  }
+  async getPermissionsList() {
+    try {
+      const data = await this.http.get('/permissionsList', {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body;
+      }
+    } catch (error) {
+      console.error(error);
+      return {groupPermissions: [], sitewidePermissions: []}
+    }
+  }
+  async getSitewidePermissionsByUsername(username) {
+    try {
+      const data = await this.http.get(`/sitewidePermissionsByUsername/${username}`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body;
+      }
+    } catch (error) {
+      console.error(error);
+      return {groupPermissions:[], sitewidePermissions:[]}
+    }
+  }
+
+  async getUserGroupPermissionsByGroupName(groupName) {
+    try {
+      const data = await this.http.get(`/users/groupPermissionsByGroupName/${groupName}`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        const token = data.body['data']['token'];
+        await this.setTokens(token);
+        return true;
+      }
+    } catch (error) {
+      console.error(error);
+      return false;
+    }
+  }
+
+  setUserGroupPermissionsByGroupName(groupName) {
+
+  }
+  // TODO FIX this
+  async updateUserPermissions(username, sitewidePermissions) {
+    try {
+      const data = await this.http.
+      post(`/permissions/updateUserSitewidePermissions/${username}`, {sitewidePermissions}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body;
+      }
+    } catch (error) {
+      console.error(error);
+      return {groupPermissions:[], sitewidePermissions:[]}
+    }
+  }
+
+  async addNewRoleToGroup(groupId, data) {
+    try {
+      const result = await this.http.
+      post(`/permissions/addRoleToGroup/${groupId}`, {data}, {observe: 'response'}).toPromise();
+      if (result.status === 200) {
+        return result.body;
+      }
+    } catch (error) {
+      console.error(error);
+      if (typeof error.status === 'undefined') {
+        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+      }
+      if (error.status === 409) {
+        this.errorHandler.handleError(_TRANSLATE(error.error));
+      }
+    }
+  }
+  async updateRoleInGroup(groupId, role) {
+    try {
+      const result = await this.http.
+      post(`/permissions/updateRoleInGroup/${groupId}`, role, {observe: 'response'}).toPromise();
+      if (result.status === 200) {
+        return result.body;
+      }
+    } catch (error) {
+      console.error(error);
+      if (typeof error.status === 'undefined') {
+        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+      }
+      if (error.status === 409) {
+        this.errorHandler.handleError(_TRANSLATE(error.error));
+      }
+    }
+  }
+
+  async getAllRoles(groupId) {
+    try {
+      const data = await this.http.get(`/rolesByGroupId/${groupId}/roles`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body['data'];
+      }
+    } catch (error) {
+      console.error(error);
+      return [];
+    }
+  }
+  async findRoleByName(groupId, roleName) {
+    try {
+      const data = await this.http.get(`/rolesByGroupId/${groupId}/role/${roleName}`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body['data'];
+      }
+    } catch (error) {
+      console.error(error);
+      return {};
+    }
+  }
+
+  getUserGroupPermissions(groupId) {
+    const allGroupsPermissions = JSON.parse(localStorage.getItem('permissions'))?.groupPermissions ?? [];
+    const groupPermissions = (allGroupsPermissions.find(group => group.groupName === groupId)).permissions;
+    return groupPermissions;
+  }
+  doesUserHaveAPermission(groupId, permission) {
+    const groupPermissions = this.getUserGroupPermissions(groupId);
+    return groupPermissions.includes(permission);
+  }
+  doesUserHaveAllPermissions(groupId, permissions= []) {
+    const groupPermissions = this.getUserGroupPermissions(groupId);
+    return permissions.every(e => groupPermissions.includes(e));
+  }
+
+  doesUserHaveSomePermissions(groupId, permissions) {
+    const groupPermissions = this.getUserGroupPermissions(groupId);
+    return permissions.some(e => groupPermissions.includes(e));
+  }
+
+  async getCustomLoginMarkup() {
+    try {
+      return <string>await this.http.get('/custom-login-markup', {responseType: 'text'}).toPromise()
+    } catch (error) {
+      console.error('No custom-login-markup found');
+      return '';
+    }
+  }
+}
diff --git a/online-survey-app/src/app/core/auth/_services/user.service.spec.ts b/online-survey-app/src/app/core/auth/_services/user.service.spec.ts
new file mode 100644
index 0000000000..b26195c25d
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_services/user.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [UserService]
+    });
+  });
+
+  it('should be created', inject([UserService], (service: UserService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/online-survey-app/src/app/core/auth/_services/user.service.ts b/online-survey-app/src/app/core/auth/_services/user.service.ts
new file mode 100644
index 0000000000..100d81d8d5
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_services/user.service.ts
@@ -0,0 +1,154 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+
+@Injectable()
+export class UserService {
+
+  constructor(private httpClient: HttpClient, private errorHandler: TangyErrorHandler, private http: HttpClient) { }
+
+  async createUser(payload) {
+    try {
+      if (!(await this.doesUserExist(payload.username))) {
+        const result = await this.httpClient.post('/users/register-user', payload).toPromise();
+        return result;
+      }
+
+    } catch (error) {
+      this.showError(error);
+    }
+
+  }
+
+  async doesUserExist(username: string) {
+    try {
+      const result: any = await this.httpClient.get(`/users/userExists/${username}`).toPromise();
+      return result.data;
+    } catch (error) {
+      this.showError(error);
+
+    }
+  }
+
+  async getAllUsers() {
+    try {
+      const users: any = await this.httpClient.get('/users').toPromise();
+      return users.data;
+    } catch (error) {
+      this.showError(error);
+
+    }
+  }
+  
+  async getCurrentUser() {
+    return await localStorage.getItem('user_id');
+  }
+  private showError(error: any) {
+    console.log(error);
+    if (typeof error.status === 'undefined') {
+      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+    }
+  }
+
+  async canManageSitewideUsers() {
+    return <boolean>await this.http.get('/user/permission/can-manage-sitewide-users').toPromise()
+  }
+
+  async getMyUser() {
+    try {
+      if (localStorage.getItem('user_id') === 'user1') {
+        return {
+          email: 'user1@tangerinecentral.org',
+          firstName: 'user1',
+          lastName: 'user1',
+          username: 'user1',
+          _id: 'user1'
+        }
+      } else {
+        const data = await this.http.get(`/users/findMyUser/`, {observe: 'response'}).toPromise();
+        if (data.status === 200) {
+          return data.body['data'];
+        }
+      }
+    } catch (error) {
+      console.error(error);
+      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+    }
+  }
+
+  async getAUserByUsername(username) {
+    try {
+      const data = await this.http.get(`/users/findOneUser/${username}`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.body['data'];
+      }
+    } catch (error) {
+      console.error(error);
+      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+    }
+  }
+
+  async searchUsersByUsername(username: string) {
+    try {
+      const data: any = await this.httpClient
+        .get(`/users/byUsername/${username}`)
+        .toPromise();
+      return data.data;
+    } catch (error) {
+      console.error(error);
+      if (typeof error.status === 'undefined') {
+        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+      }
+    }
+  }
+
+  async deleteUser(username: string) {
+    try {
+      const data = await this.http.delete(`/users/delete/${username}`, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return true;
+      }
+    } catch (error) {
+      console.error(error);
+      return false;
+    }
+  }
+  async restoreUser(username: string) {
+    try {
+      const data = await this.http.patch(`/users/restore/${username}`, {isActive: true}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return true;
+      }
+    } catch (error) {
+      console.error(error);
+      return false;
+    }
+  }
+
+  async updateUserDetails(payload) {
+    try {
+      const data = await this.httpClient.put(`/users/update/${payload.username}`, payload, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.status;
+      }
+    } catch (error) {
+      console.error(error);
+      return 500;
+    }
+  }
+  async updateMyUser(payload) {
+    try {
+      const data = await this.httpClient.put(`/users/updateMyUser/`,
+      payload, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        return data.status;
+      }
+    } catch (error) {
+      console.error(error);
+      return 500;
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/auth-routing.module.ts b/online-survey-app/src/app/core/auth/auth-routing.module.ts
new file mode 100644
index 0000000000..5e01fdbcf7
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/auth-routing.module.ts
@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { LoginComponent } from './_components/login/login.component';
+import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
+
+const routes: Routes = [
+  {
+    path: 'register-user',
+    component: UserRegistrationComponent
+  },
+  {
+    path: 'login',
+    component: LoginComponent
+  }
+];
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class AuthRoutingModule { }
diff --git a/online-survey-app/src/app/core/auth/auth.module.ts b/online-survey-app/src/app/core/auth/auth.module.ts
new file mode 100644
index 0000000000..3c8985c5f6
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/auth.module.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from '@angular/common';
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatInputModule } from '@angular/material/input';
+import { MatListModule } from '@angular/material/list';
+import { MatSelectModule } from '@angular/material/select';
+import { MatTableModule } from '@angular/material/table';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { SharedModule } from '../../shared/shared.module';
+import { AuthRoutingModule } from './auth-routing.module';
+import { LoginComponent } from './_components/login/login.component';
+import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
+import { LoginGuard } from './_guards/login-guard.service';
+import { AuthenticationService } from './_services/authentication.service';
+import { UserService } from './_services/user.service';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+
+@NgModule({
+  schemas: [CUSTOM_ELEMENTS_SCHEMA],
+    imports: [
+        CommonModule,
+        SharedModule,
+        MatListModule,
+        MatInputModule,
+        MatButtonModule,
+        MatSelectModule,
+        MatCardModule,
+        FormsModule,
+        ReactiveFormsModule,
+        BrowserAnimationsModule,
+        AuthRoutingModule,
+        MatAutocompleteModule,
+        MatTableModule,
+        MatCheckboxModule
+
+    ],
+  declarations: [UserRegistrationComponent, LoginComponent],
+  providers: [AuthenticationService, LoginGuard, UserService]
+})
+export class AuthModule { }
diff --git a/online-survey-app/src/app/shared/_factories/db.factory.ts b/online-survey-app/src/app/shared/_factories/db.factory.ts
new file mode 100644
index 0000000000..7cc9177b48
--- /dev/null
+++ b/online-survey-app/src/app/shared/_factories/db.factory.ts
@@ -0,0 +1,94 @@
+// @ts-ignore
+import PouchDB from 'pouchdb';
+// @ts-ignore
+// import * as PouchDBFind from 'pouchdb-find';
+import * as cordovaSqlitePlugin from 'pouchdb-adapter-cordova-sqlite';
+import CryptoPouch from 'crypto-pouch';
+import * as PouchDBUpsert from 'pouchdb-upsert';
+import debugPouch from 'pouchdb-debug';
+PouchDB.plugin(debugPouch);
+import PouchDBFind from 'pouchdb-find';
+PouchDB.plugin(PouchDBFind);
+PouchDB.plugin(PouchDBUpsert);
+PouchDB.plugin(cordovaSqlitePlugin);
+PouchDB.plugin(window['PouchReplicationStream'].plugin);
+let PouchDBLoad = (window as { [key: string]: any })["PouchDBLoad"];
+PouchDB.plugin({
+  loadIt: PouchDBLoad.load
+});
+PouchDB.adapter('writableStream', window['PouchReplicationStream'].adapters.writableStream);
+const defaults = {}
+PouchDB.plugin(CryptoPouch)
+
+export function connectToSqlCipherDb(name, key = ''):PouchDB {
+  let pouchDBOptions = <any>{
+    adapter: 'cordova-sqlite',
+    location: 'default',
+    androidDatabaseImplementation: 2
+  };
+  if (key) {
+    pouchDBOptions.key = key
+  }
+  if (window['changes_batch_size'] && name === 'shared-user-database') {
+    pouchDBOptions.changes_batch_size = window['changes_batch_size']
+  }
+  try {
+    const pouch = new PouchDB(name, {...defaults, ...pouchDBOptions});
+    return pouch
+  } catch (e) {
+    console.log("Database error: " + e);
+    console.trace();
+  }
+}
+
+export function connectToCryptoPouchDb(name, key = ''):PouchDB {
+  let pouchDBOptions = <any>{}
+  if (window['isCordovaApp']) {
+    pouchDBOptions = {
+      view_adapter: 'cordova-sqlite',
+      location: 'default',
+      androidDatabaseImplementation: 2
+    }
+    if (key) {
+      pouchDBOptions.key = key
+    }
+  }
+  if (window['changes_batch_size'] && name === 'shared-user-database') {
+    pouchDBOptions.changes_batch_size = window['changes_batch_size']
+  }
+  try {
+    const pouch = new PouchDB(name, {...defaults, ...pouchDBOptions});
+    if (key) {
+      pouch.crypto(key)
+      pouch.cryptoPouchIsEnabled = true
+    }
+    return pouch
+  } catch (e) {
+    console.log("Database error: " + e);
+    console.trace();
+  }
+}
+
+export function connectToPouchDb(name):PouchDB {
+  const pouchDBOptions = <any>{}
+  if (window['changes_batch_size'] && name === 'shared-user-database') {
+    pouchDBOptions.changes_batch_size = window['changes_batch_size']
+  }
+  try {
+    const pouch = new PouchDB(name, {...defaults, ...pouchDBOptions});
+    return pouch
+  } catch (e) {
+    console.log("Database error: " + e);
+    console.trace();
+  }
+}
+
+export function DB(name, key = ''):PouchDB {
+  if (window['cryptoPouchRunning']) {
+    return connectToCryptoPouchDb(name, key)
+  } else if (window['sqlCipherRunning']) {
+    return connectToSqlCipherDb(name, key)
+  } else {
+    return connectToPouchDb(name)
+  }
+}
diff --git a/online-survey-app/src/app/shared/_services/forms-service.service.ts b/online-survey-app/src/app/shared/_services/forms-service.service.ts
index 81250f48c6..50b65e9a81 100644
--- a/online-survey-app/src/app/shared/_services/forms-service.service.ts
+++ b/online-survey-app/src/app/shared/_services/forms-service.service.ts
@@ -37,15 +37,15 @@ export class FormsServiceService {
 
   async uploadFormResponse(formResponse): Promise<boolean>{
     try {
-      const {formUploadURL, groupId, uploadKey} = await this.appConfigService.getAppConfig();
+      const config = await this.appConfigService.getAppConfig();
 
       // Set the groupId or it will be missing from the form
       // TODO: Move this logic to tangy-form so it happens for all responses
-      formResponse.groupId = groupId
+      formResponse.groupId = config.groupId
 
       const headers = new HttpHeaders();
-      headers.set('formUploadToken', uploadKey);
-      const data = await this.httpClient.post(formUploadURL, formResponse, {headers, observe: 'response'}).toPromise();
+      headers.set('formUploadToken', config.uploadKey);
+      const data = await this.httpClient.post(config.formUploadURL, formResponse, {headers, observe: 'response'}).toPromise();
       return data.status === 200;
     } catch (error) {
       console.error(error);
diff --git a/online-survey-app/src/app/shared/_services/menu.service.ts b/online-survey-app/src/app/shared/_services/menu.service.ts
new file mode 100644
index 0000000000..b6588bbd3c
--- /dev/null
+++ b/online-survey-app/src/app/shared/_services/menu.service.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class MenuService {
+
+  title = ''
+  groupId:string = ''
+  section:string = ''
+
+  constructor() { }
+
+  setContext(title = '', groupId = '', section = '',) {
+    this.title = title 
+    this.section = section
+    this.groupId = groupId
+  }
+
+}
diff --git a/online-survey-app/src/app/shared/_services/tangy-error-handler.service.ts b/online-survey-app/src/app/shared/_services/tangy-error-handler.service.ts
new file mode 100644
index 0000000000..142614140d
--- /dev/null
+++ b/online-survey-app/src/app/shared/_services/tangy-error-handler.service.ts
@@ -0,0 +1,12 @@
+import { Injectable } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { _TRANSLATE } from './translation-marker';
+
+@Injectable()
+export class TangyErrorHandler {
+
+  constructor(private snackbar: MatSnackBar) { }
+  public handleError(error: string, duration = 10000) {
+    this.snackbar.open(error, _TRANSLATE('Close'), { duration });
+  }
+}
diff --git a/online-survey-app/src/app/shared/_services/translation-marker.ts b/online-survey-app/src/app/shared/_services/translation-marker.ts
new file mode 100644
index 0000000000..33ee2ce040
--- /dev/null
+++ b/online-survey-app/src/app/shared/_services/translation-marker.ts
@@ -0,0 +1,3 @@
+export function _TRANSLATE(str: string) {
+    return str;
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/shared/classes/app-config.class.ts b/online-survey-app/src/app/shared/classes/app-config.class.ts
new file mode 100644
index 0000000000..2479a5589b
--- /dev/null
+++ b/online-survey-app/src/app/shared/classes/app-config.class.ts
@@ -0,0 +1,197 @@
+export class AppConfig {
+
+  // 
+  // Tangerine Flavor
+  //
+
+  // The homeUrl determines which Tangerine Flavor. Options are as follows.
+  //
+  // Tangerine Coach: 'case-management'
+  // Tangerine Case Management: 'case-home'
+  // Tangerine Teach: 'dashboard'
+  homeUrl = "case-management"
+  // customLoginMarkup Custom Markup to include on the login page
+  customLoginMarkup: string
+  //
+  // i18n configuration.
+  //
+
+  languageDirection  = "ltr"
+  languageCode = "en"
+  useEthiopianCalendar:boolean
+  // Format of the date to be displayed in the application. Use Moment's Format standard. https://momentjs.com/docs/#/displaying/format/
+  dateFormat = "M/D/YYYY"
+
+  //
+  // Sync configuration
+  // 
+
+  serverUrl = "http://localhost/"
+  syncProtocol = '1'
+  groupId:string
+  groupName:string
+ 
+  //
+  // Sync Protocol 1 configuration.
+  //
+
+  uploadToken = "change_this_token"
+  uploadUrl = ''
+  uploadUnlockedFormReponses = false
+  usageCleanupBatchSize
+  minimumFreeSpace
+
+  //
+  // Sync Protocol 2 configuration.
+  //
+
+  // In a sync batch, control the number of database records read, sent over the network, and written to the database. Tweak this down when using the SqlCipher encryption plugin to avoid database crashes.
+  batchSize:number
+  // On a Devices first sync. Control in a batch the number of database records read, sent over the network, and written to the database. Tweak this down when using the SqlCipher encryption plugin to avoid database crashes.
+  initialBatchSize:number
+  // The max number of documents that will get written to disk during a sync. Tweak this down when using the SqlCipher encryption plugin to avoid database crashes.
+  writeBatchSize:number
+  // The max number of documents that will indexed at a time. Tweak this down when using the SqlCipher encryption plugin to avoid database crashes.
+  changesBatchSize:number
+  // The number of IDs to read from the database at a time when doing a Comparison Sync.
+  compareLimit: number;
+  // List of views to skip optimization of after a sync.
+  doNotOptimize: Array<string>
+  // Prevent database optimization after a sync other than the first sync. This is not recommended, will lead to performance issues when using Devices.
+  indexViewsOnlyOnFirstSync:boolean = false
+  // Enables support for reducing the number of documents processed in the changed feed when syncing by passing this value to pouchDBOptions used when instantiating a db connection in db.factory.ts. This setting can help sites that experience crashes when syncing or indexing documents. Using this setting *will* slow sync times. (default: 50) 
+  changes_batch_size:number
+ 
+  //
+  // Account auth configuration.
+  //
+
+  listUsernamesOnLoginScreen = true
+  securityQuestionText = "What is your year of birth?"
+  // Regex used to validate passwords for Device accounts.
+  passwordPolicy:string
+  // Description of validation for passwords of Device accounts.
+  passwordRecipe:string
+  // Disregard password functionality.
+  noPassword = false
+  // Prevent users from logging out and accessing menu. Useful when used in conjunction with a kiosk mode app.
+  kioskMode = false
+  // How many clicks to exit kioskMode?
+  exitClicks:number
+
+  //
+  // Profile configuration.
+  //
+
+  // centrallyManagedUserProfile and registrationRequiresServerUser are mutually exclusive. If both are set to true, the app will default to using centrallyManagedUserProfile.
+  // The user profile is managed by the server and not the Device. This setting is only applicable to Sync Protocol 1.
+  centrallyManagedUserProfile = false
+  // The user profile is initially managed by the server and can be updated on the client. This setting is only applicable to Sync Protocol 2.
+  registrationRequiresServerUser = false
+  // Hides the user profile link to edit when on the Device.
+  hideProfile = false
+  // Hides the about page.
+  hideAbout = false
+  // When using Sync Protocol 2 and associating a new Device Account with a Device User, setting this to true will show them all Device Users synced
+  // down to the Device rather than filtering those Device Users based on the single Device Assignment.
+  disableDeviceUserFilteringByAssignment:boolean
+
+  //
+  // Encryption configuration.
+  //
+
+  // Options are "SqlCipher" or "CryptoPouch". "SqlCipher" is the default but "CryptoPouch" is probably more stable.
+  encryptionPlugin:EncryptionPlugin
+  // Turns off all app level encryption. App will then report as depending on System (disk) level encryption.
+  turnOffAppLevelEncryption:boolean
+
+  //
+  // GPS configuration.
+  //
+
+  // Don't collect much GPS data and want to save battery? Set this to true to prevent the GPS chip from running constantly.
+  disableGpsWarming:boolean
+
+  //
+  // Tangerine Case module configuration.
+  //
+
+  showQueries:boolean
+  showCaseReports:boolean
+  // Determines wether or not the Issues tab is shown on the case module's home screen.
+  showIssues:boolean
+  barcodeSearchMapFunction:string
+  // Determines if a "Create Issue" button appears when viewing submitted Event Forms.
+  allowCreationOfIssues:boolean
+  // Determines if a "Merge Issue" button appears in the Issues Revision page.
+  allowMergeOfIssues:boolean
+  filterCaseEventScheduleByDeviceAssignedLocation:boolean = false
+
+
+  //
+  // Tangerine Coach configuration.
+  //
+
+  columnsOnVisitsTab = []
+  categories = []
+  
+  //
+  // Teach configuration properties
+  //
+
+  teachProperties = {
+    units: [],
+    unitDates: [],
+    cutoffRange:10,
+    attendancePrimaryThreshold: 80,
+    attendanceSecondaryThreshold: 70,
+    scoringPrimaryThreshold: 70,
+    scoringSecondaryThreshold: 60,
+    behaviorPrimaryThreshold: 90,
+    behaviorSecondaryThreshold: 80,
+    useAttendanceFeature: false,
+    showAttendanceCalendar: false,
+    studentRegistrationFields:[],
+    showLateAttendanceOption: false
+  }
+
+  //
+  // Custom App configuration
+  //
+
+  goHomeAfterFormSubmit = false
+  forceCompleteForms = false
+
+  //
+  // Backup configuration.
+  //
+
+  useCachedDbDumps:boolean
+  dbBackupSplitNumberFiles: number;
+  
+  // Media files configuration
+  mediaFileStorageLocation:string  // Options: 'database' (default) or 'file'. If undefined, will save to database.
+
+  //
+  // Experimental configuration.
+  //
+
+  // Use experimental mode in Tangy Form that only captures the properties of inputs that have changed from their original state in the form.
+  saveLessFormData:boolean
+  p2pSync = 'false'
+  attachHistoryToDocs:boolean = false
+  usePouchDbLastSequenceTracking:boolean
+  forceNewEventFormConfirmation:boolean = false
+
+  //
+  // @TODO Sort these.
+  //
+
+  calculateLocalDocsForLocation:boolean;
+  
+}
+
+export enum EncryptionPlugin {
+  SqlCipher = 'SqlCipher',
+  CryptoPouch = 'CryptoPouch'
+}
diff --git a/online-survey-app/src/app/shared/classes/user-account.class.ts b/online-survey-app/src/app/shared/classes/user-account.class.ts
new file mode 100644
index 0000000000..a729af5e6c
--- /dev/null
+++ b/online-survey-app/src/app/shared/classes/user-account.class.ts
@@ -0,0 +1,12 @@
+export class UserAccount {
+  _id:string
+  username:string
+  password:string
+  securityQuestionResponse:string
+  userUUID:string
+  initialProfileComplete:boolean
+  constructor(data:any) {
+    Object.assign(this, data)
+    this.username = data?._id
+  }
+} 
\ No newline at end of file
diff --git a/online-survey-app/src/app/shared/classes/user-database.class.ts b/online-survey-app/src/app/shared/classes/user-database.class.ts
new file mode 100644
index 0000000000..50b0d34932
--- /dev/null
+++ b/online-survey-app/src/app/shared/classes/user-database.class.ts
@@ -0,0 +1,148 @@
+import {_TRANSLATE} from '../translation-marker';
+const SHARED_USER_DATABASE_NAME = 'shared-user-database';
+import PouchDB from 'pouchdb';
+import { DB } from '../_factories/db.factory';
+import * as jsonpatch from "fast-json-patch";
+
+export class UserDatabase {
+
+  userId: string;
+  username: string;
+  name: string;
+  deviceId: string;
+  buildId:string;
+  buildChannel:string;
+  groupId:string;
+  db: PouchDB;
+  attachHistoryToDocs:boolean
+
+  constructor(username: string, userId: string, key:string = '', deviceId: string, shared = false, buildId = '', buildChannel = '', groupId = '', attachHistoryToDocs = false) {
+    this.userId = userId
+    this.username = username
+    this.name = username
+    this.deviceId = deviceId
+    this.buildId = buildId
+    this.buildChannel = buildChannel
+    this.groupId = groupId 
+    this.attachHistoryToDocs = attachHistoryToDocs 
+    if (shared) {
+      this.db = DB(SHARED_USER_DATABASE_NAME, key)
+    } else {
+      this.db = DB(username, key)
+    }
+  }
+
+  async synced(doc) {
+    return await this.db.put({
+      ...doc,
+      tangerineSyncedOn: Date.now()
+    });
+  }
+
+  async get(_id) {
+    const doc = await this.db.get(_id);
+    // @TODO Temporary workaround for CryptoPouch bug where it doesn't include the _rev when getting a doc.
+    if (this.db.cryptoPouchIsEnabled) {
+      const tmpDb = new PouchDB(this.db.name)
+      const encryptedDoc = await tmpDb.get(_id)
+      doc._rev = encryptedDoc._rev
+    }
+    return doc
+  }
+
+  async put(doc) {
+    const newDoc = {
+      ...doc,
+      tangerineModifiedByUserId: this.userId,
+      tangerineModifiedByDeviceId: this.deviceId,
+      tangerineModifiedOn: Date.now(),
+      buildId: this.buildId,
+      deviceId: this.deviceId,
+      groupId: this.groupId,
+      buildChannel: this.buildChannel,
+      // Backwards compatibility for sync protocol 1. 
+      lastModified: Date.now()
+    }
+    return await this.db.put({
+      ...newDoc,
+      ...this.attachHistoryToDocs
+        ? { history: await this._calculateHistory(newDoc) }
+        : { }
+    });
+  }
+
+  async post(doc) {
+    const newDoc = {
+      ...doc,
+      tangerineModifiedByUserId: this.userId,
+      tangerineModifiedByDeviceId: this.deviceId,
+      tangerineModifiedOn: Date.now(),
+      buildId: this.buildId,
+      deviceId: this.deviceId,
+      groupId: this.groupId,
+      buildChannel: this.buildChannel,
+      // Backwards compatibility for sync protocol 1. 
+      lastModified: Date.now()
+    }
+    return await this.db.post({
+      ...newDoc,
+      ...this.attachHistoryToDocs
+        ? { history: await this._calculateHistory(newDoc) }
+        : { } 
+    });
+  }
+
+  remove(doc) {
+    return this.db.remove(doc);
+  }
+
+  query(queryName: string, options = {}) {
+    return this.db.query(queryName, options);
+  }
+
+  destroy() {
+    return this.db.destroy();
+  }
+
+  changes(options) {
+    return this.db.changes(options);
+  }
+
+  allDocs(options) {
+    return this.db.allDocs(options);
+  }
+
+  sync(remoteDb, options) {
+    return this.db.sync(remoteDb, options);
+  }
+
+  upsert(docId, callback) {
+    return this.db.upsert(docId, callback);
+  }
+
+  compact() {
+    return this.db.compact();
+  }
+
+  async _calculateHistory(newDoc) {
+    let history = []
+    try {
+      const currentDoc = await this.db.get(newDoc._id)
+      const entry = {
+        lastRev: currentDoc._rev,
+        patch: jsonpatch.compare(currentDoc, newDoc).filter(mod => mod.path.substr(0,8) !== '/history')
+      }
+      history = currentDoc.history
+        ? [ entry, ...currentDoc.history ]
+        : [ entry ]
+    } catch (e) {
+      const entry = {
+        lastRev: 0,
+        patch: jsonpatch.compare({}, newDoc).filter(mod => mod.path.substr(0,8) !== '/history')
+      }
+      history = [ entry ]
+    }
+    return history 
+  }
+
+}
diff --git a/online-survey-app/src/app/shared/classes/user-signup.class.ts b/online-survey-app/src/app/shared/classes/user-signup.class.ts
new file mode 100644
index 0000000000..477f278f86
--- /dev/null
+++ b/online-survey-app/src/app/shared/classes/user-signup.class.ts
@@ -0,0 +1,10 @@
+export class UserSignup {
+  adminPassword:string = ''
+  username:string = ''
+  password:string = ''
+  confirmPassword:string = ''
+  securityQuestionResponse:string = ''
+  constructor(data) {
+    Object.assign(this, data)
+  }
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/shared/shared.module.ts b/online-survey-app/src/app/shared/shared.module.ts
new file mode 100644
index 0000000000..beb8d34855
--- /dev/null
+++ b/online-survey-app/src/app/shared/shared.module.ts
@@ -0,0 +1,30 @@
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { HttpClientModule, HttpClient } from '@angular/common/http';
+import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
+import { TranslateHttpLoader } from '@ngx-translate/http-loader';
+export function HttpClientLoaderFactory(httpClient: HttpClient) {
+  return new TranslateHttpLoader(httpClient, './assets/', '.json');
+}
+
+@NgModule({
+    schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
+    imports: [
+      CommonModule,
+      HttpClientModule,
+      TranslateModule.forRoot({
+        loader: {
+          provide: TranslateLoader,
+          useFactory: HttpClientLoaderFactory,
+          deps: [HttpClient]
+        }
+      })
+    ],
+    providers: [
+    ],
+    declarations: [
+    ],
+    exports: [TranslateModule]
+  })
+  export class SharedModule { }
+  
\ No newline at end of file

From 8971b53372414898f8777dd5899dfe58b3982c41 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 23 May 2024 15:07:48 -0400
Subject: [PATCH 02/59] Add online survey get and save form response from case
 event form id

---
 develop.sh                                    |  1 +
 .../src/app/app-routing.module.ts             |  3 +-
 .../_services/forms-service.service.spec.ts   |  8 +--
 .../shared/_services/forms-service.service.ts | 58 ++++++++++++++++-
 .../tangy-forms-player.component.ts           | 46 +++++++++++---
 server/src/case-api.js                        | 29 +++++++++
 server/src/express-app.js                     | 11 +++-
 server/src/online-survey.js                   | 62 +++++++++++++++++--
 8 files changed, 196 insertions(+), 22 deletions(-)
 create mode 100644 server/src/case-api.js

diff --git a/develop.sh b/develop.sh
index a7af59e3cb..76ede2af08 100755
--- a/develop.sh
+++ b/develop.sh
@@ -230,6 +230,7 @@ OPTIONS="--link $T_COUCHDB_CONTAINER_NAME:couchdb \
   --volume $(pwd)/editor/src:/tangerine/editor/src:delegated \
   --volume $(pwd)/translations:/tangerine/translations:delegated \
   --volume $(pwd)/online-survey-app/src:/tangerine/online-survey-app/src:delegated \
+  --volume $(pwd)/online-survey-app/dist:/tangerine/online-survey-app/dist:delegated \
   --volume $(pwd)/tangy-form-editor:/tangerine/tangy-form-editor:delegated \
   --volume $(pwd)/tangy-form:/tangerine/tangy-form:delegated \
   tangerine/tangerine:local
diff --git a/online-survey-app/src/app/app-routing.module.ts b/online-survey-app/src/app/app-routing.module.ts
index 840288ee4b..d93f6569d3 100644
--- a/online-survey-app/src/app/app-routing.module.ts
+++ b/online-survey-app/src/app/app-routing.module.ts
@@ -8,8 +8,9 @@ import { LoginGuard } from './core/auth/_guards/login-guard.service';
 const routes: Routes = [
   { path: 'forms-list', component: FormsListComponent, canActivate: [LoginGuard] },
   { path: 'form-submitted-success', component: FormSubmittedSuccessComponent, canActivate: [LoginGuard] },
-  { path: 'form/:id', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
+  { path: 'form/:formId', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
   { path: 'form/option/:formId/:option', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
+  { path: 'caseFormResponse/:caseEventFormId', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
 ];
 
 @NgModule({
diff --git a/online-survey-app/src/app/shared/_services/forms-service.service.spec.ts b/online-survey-app/src/app/shared/_services/forms-service.service.spec.ts
index 589605dd51..d0a0f4d907 100644
--- a/online-survey-app/src/app/shared/_services/forms-service.service.spec.ts
+++ b/online-survey-app/src/app/shared/_services/forms-service.service.spec.ts
@@ -1,13 +1,13 @@
 import { TestBed } from '@angular/core/testing';
 
-import { FormsServiceService } from './forms-service.service';
+import { FormsService } from './forms-service.service';
 
-describe('FormsServiceService', () => {
-  let service: FormsServiceService;
+describe('FormsService', () => {
+  let service: FormsService;
 
   beforeEach(() => {
     TestBed.configureTestingModule({});
-    service = TestBed.inject(FormsServiceService);
+    service = TestBed.inject(FormsService);
   });
 
   it('should be created', () => {
diff --git a/online-survey-app/src/app/shared/_services/forms-service.service.ts b/online-survey-app/src/app/shared/_services/forms-service.service.ts
index 50b65e9a81..ee4ddb17ff 100644
--- a/online-survey-app/src/app/shared/_services/forms-service.service.ts
+++ b/online-survey-app/src/app/shared/_services/forms-service.service.ts
@@ -2,13 +2,18 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Form } from '../classes/form';
 import { AppConfigService } from './app-config.service';
+import { AuthenticationService } from 'src/app/core/auth/_services/authentication.service';
 
 @Injectable({
   providedIn: 'root'
 })
-export class FormsServiceService {
+export class FormsService {
 
-  constructor(private httpClient: HttpClient, private appConfigService: AppConfigService) { }
+  constructor(
+    private httpClient: HttpClient, 
+    private appConfigService: AppConfigService,
+    private authenticationService: AuthenticationService
+  ) { }
 
   async getForms(): Promise<Form[]> {
     try {
@@ -35,6 +40,36 @@ export class FormsServiceService {
     }
   }
 
+  async getFormResponse(formResponseId: string): Promise<any> {
+    try {
+      const config = await this.appConfigService.getAppConfig();
+
+      const headers = new HttpHeaders();
+      headers.set('formUploadToken', config.uploadKey);
+      const url = `/onlineSurvey/getResponse/${config.groupId}/${formResponseId}`;
+      const data = await this.httpClient.get(url, {headers}).toPromise();
+
+      return data;
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  async getEventFormData(eventFormId: string): Promise<any> {
+    try {
+      const config = await this.appConfigService.getAppConfig();
+
+      const headers = new HttpHeaders();
+      headers.set('formUploadToken', config.uploadKey);
+      const url = `/case/getEventFormData/${config.groupId}/${eventFormId}`;
+      const data = await this.httpClient.get(url, {headers}).toPromise();
+
+      return data;
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
   async uploadFormResponse(formResponse): Promise<boolean>{
     try {
       const config = await this.appConfigService.getAppConfig();
@@ -52,4 +87,23 @@ export class FormsServiceService {
       return false;
     }
   }
+
+  async uploadFormResponseForCase(formResponse, eventFormId): Promise<boolean>{
+    try {
+      const config = await this.appConfigService.getAppConfig();
+
+      // Set the groupId or it will be missing from the form
+      // TODO: Move this logic to tangy-form so it happens for all responses
+      formResponse.groupId = config.groupId
+
+      const headers = new HttpHeaders();
+      const formUploadURL = `/onlineSurvey/saveResponse/${config.groupId}/${eventFormId}/${formResponse._id}`;
+      headers.set('formUploadToken', config.uploadKey);
+      const data = await this.httpClient.post(formUploadURL, formResponse, {headers, observe: 'response'}).toPromise();
+      return data.status === 200;
+    } catch (error) {
+      console.error(error);
+      return false;
+    }
+  }
 }
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
index d32e305593..b3d3a7eccf 100644
--- a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
@@ -1,7 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { FormsServiceService } from '../shared/_services/forms-service.service';
+import { FormsService } from '../shared/_services/forms-service.service';
 
 @Component({
   selector: 'app-tangy-forms-player',
@@ -10,21 +10,51 @@ import { FormsServiceService } from '../shared/_services/forms-service.service';
 })
 export class TangyFormsPlayerComponent implements OnInit {
   @ViewChild('container', {static: true}) container: ElementRef;
-  constructor(private route: ActivatedRoute, private formsService: FormsServiceService, private router: Router,
-    private httpClient:HttpClient
-  ) { }
+
+  formId: string;
+  formResponseId: string;
+  caseEventFormId: string;
+  
+  constructor(private route: ActivatedRoute, private formsService: FormsService, private router: Router, private httpClient:HttpClient
+  ) { 
+    this.router.events.subscribe(async (event) => {
+        this.formId = this.route.snapshot.paramMap.get('formId');
+        this.formResponseId = this.route.snapshot.paramMap.get('formResponseId');
+        this.caseEventFormId = this.route.snapshot.paramMap.get('caseEventFormId');
+    });
+  }
 
   async ngOnInit(): Promise<any> {
     const data = await this.httpClient.get('./assets/form/form.html', {responseType: 'text'}).toPromise();
     this.container.nativeElement.innerHTML = data;
-    const tangyForm = this.container.nativeElement.querySelector('tangy-form');
+    let tangyForm = this.container.nativeElement.querySelector('tangy-form');
+
+    if (this.caseEventFormId) {
+      try {
+        const eventForm = await this.formsService.getEventFormData(this.caseEventFormId);
+        if (eventForm && eventForm.formResponseId) {
+          tangyForm.response = await this.formsService.getFormResponse(eventForm.formResponseId);
+        }
+      } catch (error) {
+        //pass
+      }
+    }
+
     tangyForm.addEventListener('after-submit', async (event) => {
       event.preventDefault();
       try {
-        if (await this.formsService.uploadFormResponse(event.target.response)){
-          this.router.navigate(['/form-submitted-success']);
+        if (this.caseEventFormId) {
+          if (await this.formsService.uploadFormResponseForCase(event.target.response, this.caseEventFormId)) {
+            this.router.navigate(['/form-submitted-success']);
+          } else {
+            alert('Form could not be submitted. Please retry');
+          }
         } else {
-          alert('Form could not be submitted. Please retry');
+          if (await this.formsService.uploadFormResponse(event.target.response)) {
+            this.router.navigate(['/form-submitted-success']);
+          } else {
+            alert('Form could not be submitted. Please retry');
+          }
         }
       } catch (error) {
         console.error(error);
diff --git a/server/src/case-api.js b/server/src/case-api.js
new file mode 100644
index 0000000000..3aee6e6cd4
--- /dev/null
+++ b/server/src/case-api.js
@@ -0,0 +1,29 @@
+const DB = require('./db.js')
+const clog = require('tangy-log').clog
+const log = require('tangy-log').log
+
+getEventFormData = async (req, res) => {
+  const groupDb = new DB(req.params.groupId)
+  let data = {}
+  try {
+    let options = { key: req.params.eventFormId, include_docs: true }
+    const results = await groupDb.query('eventForms/eventForms', options);
+    if (results.rows.length > 0) {
+      const doc = results.rows[0].doc
+      for (let event of doc.events) {
+        let eventForm = event.eventForms.find((f) => f.id === req.params.eventFormId);
+        if (eventForm) {
+          data = eventForm
+          break;
+        }
+      }
+    }
+  } catch (err) {
+    res.status(500).send(err);
+  }
+  res.send(data)
+}
+
+module.exports = {
+  getEventFormData
+}
\ No newline at end of file
diff --git a/server/src/express-app.js b/server/src/express-app.js
index ad69f9cd50..90dd3eddc4 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -46,7 +46,8 @@ const { extendSession, findUserByUsername,
 const {registerUser,  getUserByUsername, isUserSuperAdmin, isUserAnAdminUser, getGroupsByUser, deleteUser,
    getAllUsers, checkIfUserExistByUsername, findOneUserByUsername,
    findMyUser, updateUser, restoreUser, updateMyUser} = require('./users');
- const {saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
+const {getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
+const { getEventFormData } = require('./case-api')
 log.info('heartbeat')
 setInterval(() => log.info('heartbeat'), 5*60*1000)
 const cookieParser = require('cookie-parser');
@@ -179,6 +180,11 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
  app.get('/configuration/archiveToDisk', isAuthenticated, archiveToDiskConfig);
  app.get('/configuration/passwordPolicyConfig', isAuthenticated, passwordPolicyConfig);
 
+/**
+ * Case API routes
+ */
+
+app.get('/case/getEventFormData/:groupId/:eventFormId', getEventFormData);
 
 /**
  * Online survey routes
@@ -187,6 +193,9 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
 app.post('/onlineSurvey/publish/:groupId/:formId', isAuthenticated, publishSurvey);
 app.put('/onlineSurvey/unpublish/:groupId/:formId', isAuthenticated, unpublishSurvey);
 app.post('/onlineSurvey/saveResponse/:groupId/:formId', hasSurveyUploadKey, saveSurveyResponse);
+
+app.get('/onlineSurvey/getResponse/:groupId/:formResponseId', /* hasSurveyUploadKey,*/ getSurveyResponse);
+app.post('/onlineSurvey/saveResponse/:groupId/:caseEventFormId/:formResponseId', /* hasSurveyUploadKey,*/ saveSurveyResponse);
 /*
  * More API
  */
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 9c44d03586..9ac726d581 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -3,17 +3,66 @@ const GROUPS_DB = new DB('groups');
 const { v4: uuidV4 } = require('uuid');
 const createFormKey = () => uuidV4();
 
+const getResponse = async (req, res) => {
+  try {
+
+    const { groupId, formResponseId } = req.params;
+
+    console.log('getResponse', groupId, formResponseId);
+
+    const db = new DB(groupId);
+    const doc = await db.get(formResponseId);
+    return res.status(200).send(doc);
+  } catch (error) {
+    console.error(error);
+    return res.status(500).send('Could not get response');
+  }
+};
+
 const saveResponse = async (req, res) => {
   try {
-    const { groupId, formId } = req.params;
+    const { groupId, formId, caseEventFormId } = req.params;
+
+    console.log('saveResponse', groupId, formId, caseEventFormId);
+
     const db = new DB(groupId);
     const data = req.body;
     data.formId = formId;
+
+    if (caseEventFormId) {
+      try {
+        const results = await db.query("eventForms/eventForms", {key: caseEventFormId, include_docs: true});
+        if (results.rows.length > 0) {
+          const caseDoc = results.rows[0].doc;
+          console.log('caseId', caseDoc._id)
+
+          for (let event of caseDoc.events) {
+            let eventForm = event.eventForms.find((f) => f.id === caseEventFormId);
+            if (eventForm) {
+
+              console.log('eventForm', eventForm.id);
+
+              data.caseId = caseDoc._id;
+              data.caseEventId = event.id;
+              data.caseEventFormId = eventForm.id;
+
+              eventForm.formResponseId = data._id;
+              eventForm.complete = true
+              await db.put(caseDoc);
+              break;
+            }
+          }
+        }
+      } catch (error) {
+        console.error(error);
+      }
+    }
+
     await db.post(data);
     return res.status(200).send({ data: 'Response Saved Successfully' });
   } catch (error) {
     console.error(error);
-    return res.status(500).send('Could  not save response');
+    return res.status(500).send('Could not save response');
   }
 };
 
@@ -66,8 +115,9 @@ const unpublishSurvey = async (req, res) => {
   }
 };
 
-module.exports ={
-    saveResponse,
-    publishSurvey,
-    unpublishSurvey
+module.exports = {
+  getResponse,
+  saveResponse,
+  publishSurvey,
+  unpublishSurvey
 }
\ No newline at end of file

From d4a713cf556142bc145fedb8941edfa0799433e4 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 29 May 2024 16:07:14 -0400
Subject: [PATCH 03/59] Working version of online-survey login routing

---
 develop.sh                                    |   7 +-
 .../src/app/app-routing.module.ts             |   2 +
 .../auth/_components/login/login.component.ts |  10 +-
 .../survey-login/survey-login.component.css   |  33 +++
 .../survey-login/survey-login.component.html  |  22 ++
 .../survey-login/survey-login.component.ts    |  55 +++++
 .../core/auth/_guards/login-guard.service.ts  |   2 +-
 .../auth/_services/authentication.service.ts  | 189 +++---------------
 .../src/app/core/auth/auth-routing.module.ts  |   6 +-
 .../src/app/core/auth/auth.module.ts          |   3 +-
 server/src/auth-utils.js                      |  10 +
 server/src/express-app.js                     |   3 +-
 server/src/online-survey.js                   |  31 +++
 13 files changed, 191 insertions(+), 182 deletions(-)
 create mode 100644 online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.css
 create mode 100644 online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
 create mode 100644 online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts

diff --git a/develop.sh b/develop.sh
index 76ede2af08..7182a9f976 100755
--- a/develop.sh
+++ b/develop.sh
@@ -77,11 +77,12 @@ if echo "$T_MODULES" | grep mysql; then
   ./mysql-create-dirs.sh
 fi
 
-if echo "$T_USE_MYSQL_CONTAINER" | grep "true"; then
+  if echo "$T_USE_MYSQL_CONTAINER" | grep "true"; then
     ./mysql-start-container.sh
     echo "Waiting 60 seconds for mysql container to start..."
-        sleep 60
-        ./mysql-setup.sh
+    sleep 60
+    ./mysql-setup.sh
+  fi
 fi
 
 if echo "$T_MYSQL_PHPMYADMIN" | grep "TRUE"; then
diff --git a/online-survey-app/src/app/app-routing.module.ts b/online-survey-app/src/app/app-routing.module.ts
index d93f6569d3..7370b484d0 100644
--- a/online-survey-app/src/app/app-routing.module.ts
+++ b/online-survey-app/src/app/app-routing.module.ts
@@ -4,8 +4,10 @@ import { FormSubmittedSuccessComponent } from './form-submitted-success/form-sub
 import { FormsListComponent } from './forms-list/forms-list.component';
 import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
 import { LoginGuard } from './core/auth/_guards/login-guard.service';
+import { SurveyLoginComponent } from './core/auth/_components/survey-login/survey-login.component';
 
 const routes: Routes = [
+  { path: 'survey-login', component: SurveyLoginComponent },
   { path: 'forms-list', component: FormsListComponent, canActivate: [LoginGuard] },
   { path: 'form-submitted-success', component: FormSubmittedSuccessComponent, canActivate: [LoginGuard] },
   { path: 'form/:formId', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
index 4ef807c3f9..b770f708de 100644
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
@@ -5,7 +5,7 @@ import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
 
 
 @Component({
-  selector: 'app-login',
+  selector: 'login',
   templateUrl: './login.component.html',
   styleUrls: ['./login.component.css']
 })
@@ -28,11 +28,7 @@ export class LoginComponent implements OnInit {
 
     if (await this.authenticationService.isLoggedIn()) {
       this.router.navigate([this.returnUrl]);
-    }
-    // We always want to log in from the "front door". If we log in from path of `/app/<groupId>`, then the global cookie
-    // will be tied to that pathname and not available at other pathnames such as `/csv/` that is looking for authentication.
-    if (window.location.pathname !== '/') { 
-      window.location.pathname = '/'
+      return;
     }
     this.customLoginMarkup.nativeElement.innerHTML = await this.authenticationService.getCustomLoginMarkup()
     this.ready = true
@@ -42,8 +38,8 @@ export class LoginComponent implements OnInit {
 
       if (window.location.origin.startsWith('http://localhost')) {
         // If we are running on localhost, we want to use the local server for authentication 
+        localStorage.setItem(this.user.username, this.user.password);
         this.router.navigate([this.returnUrl]);
-  
       } else if (await this.authenticationService.login(this.user.username, this.user.password)) {
         this.router.navigate([this.returnUrl]);
       } else {
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.css b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.css
new file mode 100644
index 0000000000..e347211a77
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.css
@@ -0,0 +1,33 @@
+mat-card {
+  width: 300px;
+  margin: 30px auto;
+}
+.mat-title {
+  color: var(--primary-color);
+  font-size: 1.5em;
+  font-weight: 400;
+  font-family: Roboto, 'Helvetica Nue', sans-serif;
+}
+#logo {
+  margin-bottom: 15px;
+}
+mat-placeholder i {
+  margin-right: 0.075em;
+}
+
+input.mat-input-element {
+  margin-top: 1em;
+}
+
+.mat-placeholder-required.mat-form-field-required-marker.ng-tns-c6-2
+  .mat-form-field-invalid
+  .mat-form-field-placeholder.mat-accent,
+.mat-form-field-invalid
+  .mat-form-field-placeholder
+  .mat-form-field-required-marker {
+  position: relative;
+  bottom: 1em !important;
+}
+.mat-form-field {
+  width: 100%;
+}
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
new file mode 100644
index 0000000000..2da30e3a12
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
@@ -0,0 +1,22 @@
+<mat-card *ngIf="!ready">
+  Loading...
+</mat-card>
+<mat-card *ngIf="ready">
+    <form role="form" #login='ngForm' novalidate>
+      <img id="logo" src="/logo.png" width="100%">
+      <mat-form-field>
+        <input matInput type="text" required [(ngModel)]="user.accessCode" id="username" name="username">
+        <mat-placeholder>
+          <i class="material-icons app-input-icon">key</i>
+          <span>{{'Access Code'|translate}}</span>
+        </mat-placeholder>
+      </mat-form-field>
+      <br>
+      <br>
+      <button (click)="loginUser()" mat-raised-button color="accent" name="action">{{'LOGIN'|translate}}</button>
+      <span id="err">
+        <small>{{errorMessage}}</small>
+      </span>
+    </form>
+</mat-card>
+<span #customLoginMarkup></span>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
new file mode 100644
index 0000000000..01e461fedf
--- /dev/null
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
@@ -0,0 +1,55 @@
+import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AuthenticationService } from '../../_services/authentication.service';
+import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
+import { AppConfigService } from 'src/app/shared/_services/app-config.service';
+
+
+@Component({
+  selector: 'survey-login',
+  templateUrl: './survey-login.component.html',
+  styleUrls: ['./survey-login.component.css']
+})
+export class SurveyLoginComponent implements OnInit {
+
+  errorMessage = '';
+  returnUrl: string; // stores the value of the url to redirect to after login
+  user = { accessCode: '' };
+  @ViewChild('customLoginMarkup', {static: true}) customLoginMarkup: ElementRef;
+  ready = false
+
+  constructor(
+    private authenticationService: AuthenticationService,
+    private appConfigService: AppConfigService,
+    private route: ActivatedRoute,
+    private router: Router,
+  ) { }
+
+  async ngOnInit() {
+    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || 'forms-list';
+
+    if (await this.authenticationService.isLoggedIn()) {
+      this.router.navigate([this.returnUrl]);
+      return;
+    }
+    this.customLoginMarkup.nativeElement.innerHTML = await this.authenticationService.getCustomLoginMarkup()
+    this.ready = true
+  }
+  async loginUser() {
+    try {
+
+      const appConfig = await this.appConfigService.getAppConfig();
+      const groupId = appConfig['groupId'];
+
+      if (await this.authenticationService.surveyLogin(groupId, this.user.accessCode)) {
+        this.router.navigate([this.returnUrl]);
+      } else {
+        this.errorMessage = _TRANSLATE('Login Unsuccessful');
+      }
+    } catch (error) {
+      this.errorMessage = _TRANSLATE('Login Unsuccessful');
+      console.error(error);
+    }
+  }
+
+}
diff --git a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
index 1b48299f84..b9986cbdef 100644
--- a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
+++ b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
@@ -13,7 +13,7 @@ export class LoginGuard implements CanActivate {
     if (await this.authenticationService.isLoggedIn()) {
       return true;
     }
-    this.router.navigate([route, 'login'], { queryParams: { returnUrl: state.url } });
+    this.router.navigate(['survey-login'], { queryParams: { returnUrl: state.url } });
     return false;
   }
 }
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 90395ebd19..b38084a918 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -13,9 +13,10 @@ export class AuthenticationService {
   constructor(private userService: UserService, private http: HttpClient, private errorHandler: TangyErrorHandler) {
     this.currentUserLoggedIn$ = new Subject();
   }
+
   async login(username: string, password: string) {
     try {
-     const data = await this.http.post('/login', {username, password}, {observe: 'response'}).toPromise();
+      const data = await this.http.post('/login', {username, password}, {observe: 'response'}).toPromise();
       if (data.status === 200) {
         const token = data.body['data']['token'];
         await this.setTokens(token);
@@ -31,192 +32,48 @@ export class AuthenticationService {
       return false;
     }
   }
-  async isLoggedIn():Promise<boolean> {
 
-    if (window.location.origin.startsWith('http://localhost')) {
-      localStorage.setItem('user_id', 'testuser');
+  async surveyLogin(groupId: string, accessCode: string) {
+    try {
+      const data = await this.http.post(`/onlineSurvey/login/${groupId}/${accessCode}`, {groupId, accessCode}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        const token = data.body['data']['token'];
+        await this.setTokens(token);
+        return true;
+      } else {
+        return false;
+      }
+    } catch (error) {
+      console.error(error);
+      localStorage.removeItem('token');
+      localStorage.removeItem('user_id');
+      return false;
+    }
   }
 
+  async isLoggedIn():Promise<boolean> {
     this._currentUserLoggedIn = false;
     this._currentUserLoggedIn = !!localStorage.getItem('user_id');
     this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
     return this._currentUserLoggedIn;
   }
 
-  async validateSession():Promise<boolean> {
-    const status = await this.http.get(`/login/validate/${localStorage.getItem('user_id')}`).toPromise()
-    return status['valid']
-  }
-
-
   async logout() {
-    await localStorage.removeItem('token');
-    await localStorage.removeItem('user_id');
-    await localStorage.removeItem('password');
+    localStorage.removeItem('token');
+    localStorage.removeItem('user_id');
+    localStorage.removeItem('password');
     localStorage.removeItem('permissions');
     document.cookie = "Authorization=;max-age=-1";
     this._currentUserLoggedIn = false;
     this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
   }
 
-  async extendUserSession() {
-    const username = localStorage.getItem('user_id');
-    try {
-      const data = await this.http.post('/extendSession', {username}, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        const token = data.body['data']['token'];
-       await this.setTokens(token);
-        return true;
-      } else {
-        return false;
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  }
-
   async setTokens(token) {
     const jwtData = jwtDecode(token);
     document.cookie = "Authorization=;max-age=-1";
     localStorage.setItem('token', token);
-    localStorage.setItem('user_id', jwtData['username']);
-    localStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
+    localStorage.setItem('user_id', jwtData['accessCode']);
     document.cookie = `Authorization=${token}`;
-
-    const user = await this.userService.getMyUser();
-    window['userProfile'] = user;
-    window['userId'] = user._id;
-    window['username'] = jwtData['username'];
-  }
-  async getPermissionsList() {
-    try {
-      const data = await this.http.get('/permissionsList', {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body;
-      }
-    } catch (error) {
-      console.error(error);
-      return {groupPermissions: [], sitewidePermissions: []}
-    }
-  }
-  async getSitewidePermissionsByUsername(username) {
-    try {
-      const data = await this.http.get(`/sitewidePermissionsByUsername/${username}`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body;
-      }
-    } catch (error) {
-      console.error(error);
-      return {groupPermissions:[], sitewidePermissions:[]}
-    }
-  }
-
-  async getUserGroupPermissionsByGroupName(groupName) {
-    try {
-      const data = await this.http.get(`/users/groupPermissionsByGroupName/${groupName}`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        const token = data.body['data']['token'];
-        await this.setTokens(token);
-        return true;
-      }
-    } catch (error) {
-      console.error(error);
-      return false;
-    }
-  }
-
-  setUserGroupPermissionsByGroupName(groupName) {
-
-  }
-  // TODO FIX this
-  async updateUserPermissions(username, sitewidePermissions) {
-    try {
-      const data = await this.http.
-      post(`/permissions/updateUserSitewidePermissions/${username}`, {sitewidePermissions}, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body;
-      }
-    } catch (error) {
-      console.error(error);
-      return {groupPermissions:[], sitewidePermissions:[]}
-    }
-  }
-
-  async addNewRoleToGroup(groupId, data) {
-    try {
-      const result = await this.http.
-      post(`/permissions/addRoleToGroup/${groupId}`, {data}, {observe: 'response'}).toPromise();
-      if (result.status === 200) {
-        return result.body;
-      }
-    } catch (error) {
-      console.error(error);
-      if (typeof error.status === 'undefined') {
-        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-      }
-      if (error.status === 409) {
-        this.errorHandler.handleError(_TRANSLATE(error.error));
-      }
-    }
-  }
-  async updateRoleInGroup(groupId, role) {
-    try {
-      const result = await this.http.
-      post(`/permissions/updateRoleInGroup/${groupId}`, role, {observe: 'response'}).toPromise();
-      if (result.status === 200) {
-        return result.body;
-      }
-    } catch (error) {
-      console.error(error);
-      if (typeof error.status === 'undefined') {
-        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-      }
-      if (error.status === 409) {
-        this.errorHandler.handleError(_TRANSLATE(error.error));
-      }
-    }
-  }
-
-  async getAllRoles(groupId) {
-    try {
-      const data = await this.http.get(`/rolesByGroupId/${groupId}/roles`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body['data'];
-      }
-    } catch (error) {
-      console.error(error);
-      return [];
-    }
-  }
-  async findRoleByName(groupId, roleName) {
-    try {
-      const data = await this.http.get(`/rolesByGroupId/${groupId}/role/${roleName}`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body['data'];
-      }
-    } catch (error) {
-      console.error(error);
-      return {};
-    }
-  }
-
-  getUserGroupPermissions(groupId) {
-    const allGroupsPermissions = JSON.parse(localStorage.getItem('permissions'))?.groupPermissions ?? [];
-    const groupPermissions = (allGroupsPermissions.find(group => group.groupName === groupId)).permissions;
-    return groupPermissions;
-  }
-  doesUserHaveAPermission(groupId, permission) {
-    const groupPermissions = this.getUserGroupPermissions(groupId);
-    return groupPermissions.includes(permission);
-  }
-  doesUserHaveAllPermissions(groupId, permissions= []) {
-    const groupPermissions = this.getUserGroupPermissions(groupId);
-    return permissions.every(e => groupPermissions.includes(e));
-  }
-
-  doesUserHaveSomePermissions(groupId, permissions) {
-    const groupPermissions = this.getUserGroupPermissions(groupId);
-    return permissions.some(e => groupPermissions.includes(e));
   }
 
   async getCustomLoginMarkup() {
diff --git a/online-survey-app/src/app/core/auth/auth-routing.module.ts b/online-survey-app/src/app/core/auth/auth-routing.module.ts
index 5e01fdbcf7..e0b5fa9b74 100644
--- a/online-survey-app/src/app/core/auth/auth-routing.module.ts
+++ b/online-survey-app/src/app/core/auth/auth-routing.module.ts
@@ -1,6 +1,6 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
-import { LoginComponent } from './_components/login/login.component';
+import { SurveyLoginComponent } from './_components/survey-login/survey-login.component';
 import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
 
 const routes: Routes = [
@@ -9,8 +9,8 @@ const routes: Routes = [
     component: UserRegistrationComponent
   },
   {
-    path: 'login',
-    component: LoginComponent
+    path: 'survey-login',
+    component: SurveyLoginComponent
   }
 ];
 @NgModule({
diff --git a/online-survey-app/src/app/core/auth/auth.module.ts b/online-survey-app/src/app/core/auth/auth.module.ts
index 3c8985c5f6..5e6cf3ba04 100644
--- a/online-survey-app/src/app/core/auth/auth.module.ts
+++ b/online-survey-app/src/app/core/auth/auth.module.ts
@@ -11,6 +11,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { SharedModule } from '../../shared/shared.module';
 import { AuthRoutingModule } from './auth-routing.module';
 import { LoginComponent } from './_components/login/login.component';
+import { SurveyLoginComponent } from './_components/survey-login/survey-login.component';
 import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
 import { LoginGuard } from './_guards/login-guard.service';
 import { AuthenticationService } from './_services/authentication.service';
@@ -37,7 +38,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
         MatCheckboxModule
 
     ],
-  declarations: [UserRegistrationComponent, LoginComponent],
+  declarations: [UserRegistrationComponent, LoginComponent, SurveyLoginComponent],
   providers: [AuthenticationService, LoginGuard, UserService]
 })
 export class AuthModule { }
diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js
index 819405b7fa..230b58217f 100644
--- a/server/src/auth-utils.js
+++ b/server/src/auth-utils.js
@@ -12,6 +12,15 @@ const createLoginJWT = ({ username, permissions }) => {
   return jwt.sign({ username, permissions }, jwtTokenSecret, signingOptions);
 };
 
+const createAccessCodeJWT = ({ accessCode }) => {
+  const signingOptions = {
+    expiresIn,
+    issuer,
+    subject: accessCode,
+  };
+  return jwt.sign({ accessCode }, jwtTokenSecret, signingOptions);
+};
+
 const verifyJWT = (token) => {
   try {
     const jwtPayload = jwt.verify(token, jwtTokenSecret, { issuer });
@@ -32,6 +41,7 @@ const decodeJWT = (token) => {
 
 module.exports = {
   createLoginJWT,
+  createAccessCodeJWT,
   decodeJWT,
   verifyJWT,
 };
diff --git a/server/src/express-app.js b/server/src/express-app.js
index 90dd3eddc4..f0a41ef34e 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -46,7 +46,7 @@ const { extendSession, findUserByUsername,
 const {registerUser,  getUserByUsername, isUserSuperAdmin, isUserAnAdminUser, getGroupsByUser, deleteUser,
    getAllUsers, checkIfUserExistByUsername, findOneUserByUsername,
    findMyUser, updateUser, restoreUser, updateMyUser} = require('./users');
-const {getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
+const {login: surveyLogin, getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
 const { getEventFormData } = require('./case-api')
 log.info('heartbeat')
 setInterval(() => log.info('heartbeat'), 5*60*1000)
@@ -194,6 +194,7 @@ app.post('/onlineSurvey/publish/:groupId/:formId', isAuthenticated, publishSurve
 app.put('/onlineSurvey/unpublish/:groupId/:formId', isAuthenticated, unpublishSurvey);
 app.post('/onlineSurvey/saveResponse/:groupId/:formId', hasSurveyUploadKey, saveSurveyResponse);
 
+app.post('/onlineSurvey/login/:groupId/:accessCode', surveyLogin);
 app.get('/onlineSurvey/getResponse/:groupId/:formResponseId', /* hasSurveyUploadKey,*/ getSurveyResponse);
 app.post('/onlineSurvey/saveResponse/:groupId/:caseEventFormId/:formResponseId', /* hasSurveyUploadKey,*/ saveSurveyResponse);
 /*
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 9ac726d581..26bdd66399 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -2,6 +2,36 @@ const DB = require('./db.js');
 const GROUPS_DB = new DB('groups');
 const { v4: uuidV4 } = require('uuid');
 const createFormKey = () => uuidV4();
+const { createAccessCodeJWT } = require('./auth-utils');
+
+const login = async (req, res) => {
+  try {
+
+    const { groupId, accessCode } = req.params;
+
+    console.log('login', groupId, accessCode);
+
+    const groupDb = new DB(groupId)
+    let options = { key: accessCode, include_docs: true }
+    if (req.params.limit) {
+      options.limit = req.params.limit
+    }
+    if (req.params.skip) {
+      options.skip = req.params.skip
+    }
+    const results = await groupDb.query('responsesByUserProfileShortCode', options);
+    const docs = results.rows.map(row => row.doc)
+    const userProfileDoc = docs.find(doc => doc.form.id === 'user-profile');
+
+    if (userProfileDoc) {
+      const token = createAccessCodeJWT({"accessCode": accessCode});
+      return res.status(200).send({ data: { token } });
+    }
+  } catch (error) {
+    console.error(error);
+    return res.status(401).send({ data: 'Invalid Credentials' });
+  }
+}
 
 const getResponse = async (req, res) => {
   try {
@@ -116,6 +146,7 @@ const unpublishSurvey = async (req, res) => {
 };
 
 module.exports = {
+  login,
   getResponse,
   saveResponse,
   publishSurvey,

From bc72f19d4b1660a406ebfc887c9622a59158c7c0 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 30 May 2024 18:48:10 -0400
Subject: [PATCH 04/59] Fix release online survey script copying of form
 directory

---
 server/src/scripts/release-online-survey-app.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index bcf88972bb..634393bd0a 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -46,7 +46,7 @@ mkdir -p $RELEASE_DIRECTORY/assets/locations
 mkdir -p $RELEASE_DIRECTORY/assets/media
 
 # Copy the form, location list, and media to the release directory
-cp -r $FORM_DIRECTORY $RELEASE_DIRECTORY/assets/form
+cp -r $FORM_DIRECTORY/*.html $RELEASE_DIRECTORY/assets/form/
 cp $LOCATION_LIST_PATH $RELEASE_DIRECTORY/assets/
 cp -r $LOCATION_LISTS_DIRECTORY $RELEASE_DIRECTORY/assets/
 cp -r $MEDIA_DIRECTORY $RELEASE_DIRECTORY/assets/

From aaf3809a6cc0fde80f63f7adf1280bed17e7a13c Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 30 May 2024 18:52:49 -0400
Subject: [PATCH 05/59] Add server-side APIs to create Case, Event and Form

---
 server/src/case-api.js           | 178 ++++++++++++++++++++++++++++++-
 server/src/classes/case.class.js |  91 ++++++++++++++++
 server/src/express-app.js        |  17 ++-
 server/src/online-survey.js      |  12 +--
 4 files changed, 289 insertions(+), 9 deletions(-)
 create mode 100644 server/src/classes/case.class.js

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 3aee6e6cd4..c86bf2ac5e 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -1,6 +1,146 @@
 const DB = require('./db.js')
-const clog = require('tangy-log').clog
 const log = require('tangy-log').log
+const path = require('path')
+const fs = require('fs')
+const { v4: uuidV4 } = require('uuid')
+
+const {Case: Case, CaseEvent: CaseEvent, EventForm: EventForm } = require('./classes/case.class.js')
+
+/* Return the contents of the case-definitions.json file or a sepcified */
+getCaseDefinitions = async (req, res) => {
+  const groupDir = `/tangerine/client/content/groups/${req.params.groupId}`
+  const caseType = 'case-definitions.json'
+  try {
+    const fileName = caseType.endsWith('.json') ? caseType : `${caseType}.json`
+    const filePath = path.join(groupDir, fileName)
+    if (fs.existsSync(filePath)) {
+        const fileContents = fs.readFileSync(filePath, "utf8")
+        return res.send(fileContents)
+    } else {
+        return res.status(500).send('Case definitions file not found')
+    }
+  } catch (err) {
+    log.error(`Error reading case definitions: ${err}`)
+    return res.status(500).send(err)
+  }
+}
+
+getCaseDefinition = async (req, res) => {
+  const groupId = req.params.groupId
+  const caseDefinitionId = req.params.caseDefinitionId
+  try {
+        let caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+        return res.send(caseDefinition)
+  } catch (err) {
+    log.error(`Error reading case definition file from ${caseType}`)
+    return res.status(500).send(err)
+  }
+}
+
+_getCaseDefinition = (groupId, caseDefinitionId) => {
+  let caseDefinition = {};
+
+  const groupDir = `/tangerine/client/content/groups/${groupId}`
+  const fileName = caseDefinitionId.endsWith('.json') ? caseDefinitionId : `${caseDefinitionId}.json`
+  const filePath = path.join(groupDir, fileName)
+  if (fs.existsSync(filePath)) {
+      const caseDefinitionContent = fs.readFileSync(filePath, "utf8")
+      caseDefinition = JSON.parse(caseDefinitionContent)
+  }
+
+  return caseDefinition;
+}
+
+createCase = async (req, res) => {
+  let groupId = req.params.groupId
+  let caseDefinitionId = req.params.caseDefinitionId
+
+  try {
+    const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+    let caseDoc = new Case(groupId, caseDefinitionId, caseDefinition)
+
+    if (Object.keys(req.body).length > 0) {
+      const inputs = req.body
+      caseDoc.addInputs(inputs)
+    }
+
+    const db = new DB(groupId)
+    await db.put(caseDoc);
+    res.send(caseDoc._id);
+  } catch (err) {
+    res.status(500).send(err)
+  }
+}
+
+createCaseEvent = async (req, res) => {
+  let groupId = req.params.groupId
+  let caseId = req.params.caseId
+  let caseDefinitionId = req.params.caseDefinitionId
+  let caseEventDefinitionId = req.params.caseEventDefinitionId
+  
+  const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+  let caseEventDefinition = caseDefinition.eventDefinitions.find((e) => e.id === caseEventDefinitionId)
+  let caseEvent = new CaseEvent(groupId, caseEventDefinition)
+
+  let caseDoc;
+  try {
+    const db = new DB(groupId)
+    caseDoc = await db.get(caseId);
+    caseDoc.events.push(caseEvent);
+    await db.put(caseDoc);
+    res.send(caseEvent._id);
+  } catch (err) {
+    res.status(500).send
+  }
+}
+
+createEventForm = async (req, res) => {
+  let groupId = req.params.groupId
+  let caseId = req.params.caseId
+  let caseEventId = req.params.caseEventId
+  let eventFormDefinitionId = req.params.eventFormDefinitionId
+
+  const caseDefinition = _getCaseDefinition(groupId, caseDefinitionId)
+  let eventFormDefinition;
+  for (const eventDefinition of caseDefinition.eventDefinitions) {
+      eventFormDefinition = eventDefinition.eventFormDefinitions.find((f) => f.id === eventFormDefinitionId)
+      if (eventFormDefinition) break;
+  }
+  
+  let eventForm = new EventForm(groupId, caseId, caseEventId, eventFormDefinition)
+
+  try {
+    const db = new DB(groupId)
+    caseDoc = await db.get(caseId);
+    let caseEvent = caseDoc.events.find((e) => e.id === caseEventId);
+    caseEvent.eventForms.push(eventForm);
+    await db.put(caseDoc);
+    res.send(eventForm._id);
+  } catch (err) {
+    res.status(500).send
+  }
+}
+
+createParticipant = async (req, res) => {
+  const groupId = req.params.groupId
+  const caseId = req.params.caseId
+  const caseRoleId = req.params.caseRoleId
+
+  const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+  const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
+
+  let participant = new Participant(groupId, caseId, caseRole)
+
+  try {
+    const db = new DB(groupId)
+    const caseDoc = await db.get(caseId);
+    caseDoc.participant.push(participant);
+    await db.put(caseDoc);
+    res.send(eventForm._id);
+  } catch (err) {
+    res.status(500).send
+  }
+}
 
 getEventFormData = async (req, res) => {
   const groupDb = new DB(req.params.groupId)
@@ -24,6 +164,42 @@ getEventFormData = async (req, res) => {
   res.send(data)
 }
 
+getCaseEventFormSurveyLinks = async (req, res) => {
+  const caseId = req.params.caseId
+  const groupDb = new DB(req.params.groupId)
+
+  let data = []
+  try {
+    const results = await groupDb.get(caseId)
+    if (results.rows.length > 0) {
+      const caseDoc = results.rows[0].doc
+      const caseDefinition =  _getCaseDefinition(groupId, caseDoc.caseDefinitionId)
+      for (let event of doc.events) {
+        let eventForm = event.eventForms.find((f) => f.id === req.params.eventFormId);
+        if (eventForm) {
+          let formId;
+          for (let eventDefinition of caseDefinition.eventDefinitions) {
+              formId = eventDefinition.find((e) => e.id === eventForm.eventFormDefinitionId).formId
+              if (formId) break;
+            }
+          }
+          const url = `http://localhost/releases/prod/online-survey-apps/group-344fabfe-f892-4a6d-a1da-58616949982f/${formId}/#/caseFormResponse/${caseId}/${eventForm.id}`
+          data.push(url)
+          break;
+        }
+      }
+    }
+  } catch (err) {
+    res.status(500).send(err);
+  }
+  res.send(data)
+}
+
 module.exports = {
+  getCaseDefinitions,
+  getCaseDefinition,
+  createCase,
+  createCaseEvent,
+  createEventForm,
   getEventFormData
 }
\ No newline at end of file
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
new file mode 100644
index 0000000000..2360c65e79
--- /dev/null
+++ b/server/src/classes/case.class.js
@@ -0,0 +1,91 @@
+const uuidV4 = require('uuid').v4
+
+class Case {
+  constructor(groupId, caseDefinitionId, caseDefinition) {
+    this._id = uuidV4();
+    this.type = 'case'
+    this.collection = 'TangyFormResponse'
+    this.caseDefinitionId = caseDefinitionId
+    this.archived = false
+    this.groupId = groupId
+    this.form = { id: caseDefinition.formId }
+    this.items = [{ inputs: [], }],
+    this.participants = []
+    this.disabledEventDefinitionIds = []
+    this.notifications = []
+
+    for (let eventDefinition of caseDefinition.eventDefinitions) {
+      if (eventDefinition.required) {
+        this.events.push(new CaseEvent(this._id, eventDefinition))
+      }
+    }
+  }
+
+  addInputs(inputs) {
+    for (let key in inputs) {
+      const input = {
+        name: key,
+        value: inputs[key]
+      }
+      // insert an unkeyed json object into the inputs array
+      this.items[0].inputs.push(input)
+    }
+  }
+}
+
+class CaseEvent {
+  constructor(caseId, eventDefinition) {
+    this.id = uuidV4();
+    this.caseId = caseId
+    this.name = ''
+    this.caseEventDefinitionId = eventDefinition.id
+    this.complete = false
+    this.inactive = false
+    this.eventForms = []
+
+    for (const eventForm of eventDefinition.eventFormDefinitions) {
+      if (eventForm.required) {
+        this.eventForms.push(new EventForm(caseId, this._id, eventForm))
+      }
+    }
+  }
+
+  addData(data) {
+    this.data = data
+  }
+}
+
+class EventForm {
+  constructor(caseId, caseEventId, eventFormDefinition) {
+    this.id = uuidV4()
+    this.caseId = caseId
+    this.caseEventId = caseEventId
+    this.eventFormDefinitionId = eventFormDefinition.id
+    this.required = eventFormDefinition.required
+    this.participantId = ''
+    this.complete = false
+    this.inactive = false
+  }
+
+  addData(data) {
+    this.data = data
+  }
+}
+
+class Participant {
+  constructor(participantDefinition) {
+    this.id = uuidV4()
+    this.caseRoleId = participantDefinition.id
+    this.name = ''
+    this.inactive = false;
+  }
+
+  addData(data) {
+    this.data = data
+  }
+}
+
+exports.Case = Case
+exports.CaseEvent = CaseEvent
+exports.EventForm = EventForm
+exports.Participant = Participant
\ No newline at end of file
diff --git a/server/src/express-app.js b/server/src/express-app.js
index f0a41ef34e..d6551be8c3 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -47,7 +47,14 @@ const {registerUser,  getUserByUsername, isUserSuperAdmin, isUserAnAdminUser, ge
    getAllUsers, checkIfUserExistByUsername, findOneUserByUsername,
    findMyUser, updateUser, restoreUser, updateMyUser} = require('./users');
 const {login: surveyLogin, getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
-const { getEventFormData } = require('./case-api')
+const {
+  getCaseDefinitions,
+  getCaseDefinition,
+  createCase,
+  createCaseEvent,
+  createEventForm,
+  getEventFormData
+} = require('./case-api')
 log.info('heartbeat')
 setInterval(() => log.info('heartbeat'), 5*60*1000)
 const cookieParser = require('cookie-parser');
@@ -184,6 +191,12 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
  * Case API routes
  */
 
+app.get('/case/getCaseDefinitions/:groupId', getCaseDefinitions);
+app.get('/case/getCaseDefinition/:groupId/:caseDefinitionId', getCaseDefinition);
+app.post('/case/createCase/:groupId/:caseDefinitionId', createCase);
+app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', createCaseEvent);
+app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', createEventForm);
+
 app.get('/case/getEventFormData/:groupId/:eventFormId', getEventFormData);
 
 /**
@@ -196,7 +209,7 @@ app.post('/onlineSurvey/saveResponse/:groupId/:formId', hasSurveyUploadKey, save
 
 app.post('/onlineSurvey/login/:groupId/:accessCode', surveyLogin);
 app.get('/onlineSurvey/getResponse/:groupId/:formResponseId', /* hasSurveyUploadKey,*/ getSurveyResponse);
-app.post('/onlineSurvey/saveResponse/:groupId/:caseEventFormId/:formResponseId', /* hasSurveyUploadKey,*/ saveSurveyResponse);
+app.post('/onlineSurvey/saveResponse/:groupId/:eventFormId/:formResponseId', /* hasSurveyUploadKey,*/ saveSurveyResponse);
 /*
  * More API
  */
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 26bdd66399..58e92f0fc4 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -51,30 +51,30 @@ const getResponse = async (req, res) => {
 
 const saveResponse = async (req, res) => {
   try {
-    const { groupId, formId, caseEventFormId } = req.params;
+    const { groupId, formId, eventFormId } = req.params;
 
-    console.log('saveResponse', groupId, formId, caseEventFormId);
+    console.log('saveResponse', groupId, formId, eventFormId);
 
     const db = new DB(groupId);
     const data = req.body;
     data.formId = formId;
 
-    if (caseEventFormId) {
+    if (eventFormId) {
       try {
-        const results = await db.query("eventForms/eventForms", {key: caseEventFormId, include_docs: true});
+        const results = await db.query("eventForms/eventForms", {key: eventFormId, include_docs: true});
         if (results.rows.length > 0) {
           const caseDoc = results.rows[0].doc;
           console.log('caseId', caseDoc._id)
 
           for (let event of caseDoc.events) {
-            let eventForm = event.eventForms.find((f) => f.id === caseEventFormId);
+            let eventForm = event.eventForms.find((f) => f.id === eventFormId);
             if (eventForm) {
 
               console.log('eventForm', eventForm.id);
 
               data.caseId = caseDoc._id;
               data.caseEventId = event.id;
-              data.caseEventFormId = eventForm.id;
+              data.eventFormId = eventForm.id;
 
               eventForm.formResponseId = data._id;
               eventForm.complete = true

From 9752ad58b80fcae1bf37e5af62bc43bc5c81b9a1 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 31 May 2024 13:33:37 -0400
Subject: [PATCH 06/59] Fix unfinished case-api function

---
 server/src/case-api.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index c86bf2ac5e..08be95ae47 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -173,20 +173,19 @@ getCaseEventFormSurveyLinks = async (req, res) => {
     const results = await groupDb.get(caseId)
     if (results.rows.length > 0) {
       const caseDoc = results.rows[0].doc
-      const caseDefinition =  _getCaseDefinition(groupId, caseDoc.caseDefinitionId)
+      const caseDefinition = _getCaseDefinition(groupId, caseDoc.caseDefinitionId)
       for (let event of doc.events) {
         let eventForm = event.eventForms.find((f) => f.id === req.params.eventFormId);
         if (eventForm) {
           let formId;
           for (let eventDefinition of caseDefinition.eventDefinitions) {
-              formId = eventDefinition.find((e) => e.id === eventForm.eventFormDefinitionId).formId
-              if (formId) break;
-            }
+            formId = eventDefinition.find((e) => e.id === eventForm.eventFormDefinitionId).formId
+            if (formId) break;
           }
-          const url = `http://localhost/releases/prod/online-survey-apps/group-344fabfe-f892-4a6d-a1da-58616949982f/${formId}/#/caseFormResponse/${caseId}/${eventForm.id}`
-          data.push(url)
-          break;
         }
+        const url = `http://localhost/releases/prod/online-survey-apps/group-344fabfe-f892-4a6d-a1da-58616949982f/${formId}/#/caseFormResponse/${caseId}/${eventForm.id}`
+        data.push(url)
+        break;
       }
     }
   } catch (err) {

From a92d271d43e7d9cd2b9c43be24bb13f3a45177b3 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 31 May 2024 16:12:51 -0400
Subject: [PATCH 07/59] Fix bugs in case-api and add user-profile create api

---
 server/src/case-api.js                   |  1 +
 server/src/classes/case.class.js         |  6 +++--
 server/src/classes/user-profile.class.js | 27 ++++++++++++++++++++++
 server/src/express-app.js                |  8 +++++++
 server/src/user-profile.js               | 29 ++++++++++++++++++++++++
 5 files changed, 69 insertions(+), 2 deletions(-)
 create mode 100644 server/src/classes/user-profile.class.js
 create mode 100644 server/src/user-profile.js

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 08be95ae47..7432b9fcb5 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -68,6 +68,7 @@ createCase = async (req, res) => {
     await db.put(caseDoc);
     res.send(caseDoc._id);
   } catch (err) {
+    log.error(`Error creating case: ${err}`)
     res.status(500).send(err)
   }
 }
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index 2360c65e79..13be0959e6 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -8,11 +8,13 @@ class Case {
     this.caseDefinitionId = caseDefinitionId
     this.archived = false
     this.groupId = groupId
+    this.location = {}
     this.form = { id: caseDefinition.formId }
-    this.items = [{ inputs: [], }],
+    this.items = [{ inputs: [] }],
     this.participants = []
     this.disabledEventDefinitionIds = []
     this.notifications = []
+    this.events = []
 
     for (let eventDefinition of caseDefinition.eventDefinitions) {
       if (eventDefinition.required) {
@@ -22,12 +24,12 @@ class Case {
   }
 
   addInputs(inputs) {
+    // transform key, value pairs to 'name' : key, 'value': value pairs
     for (let key in inputs) {
       const input = {
         name: key,
         value: inputs[key]
       }
-      // insert an unkeyed json object into the inputs array
       this.items[0].inputs.push(input)
     }
   }
diff --git a/server/src/classes/user-profile.class.js b/server/src/classes/user-profile.class.js
new file mode 100644
index 0000000000..698815f0c9
--- /dev/null
+++ b/server/src/classes/user-profile.class.js
@@ -0,0 +1,27 @@
+const uuidV4 = require('uuid').v4
+
+class UserProfile {
+  constructor(groupId) {
+    this._id = uuidV4();
+    this.type = 'response'
+    this.collection = 'TangyFormResponse'
+    this.groupId = groupId
+    this.form = { id: "user-profile" }
+    this.items = [{ inputs: [] }]
+    this.location = {};
+  }
+
+  addInputs(inputs) {
+    for (let key in inputs) {
+      const input = {
+        name: key,
+        value: inputs[key]
+      }
+      // insert an unkeyed json object into the inputs array
+      this.items[0].inputs.push(input)
+    }
+  }
+}
+
+
+exports.UserProfile = UserProfile
\ No newline at end of file
diff --git a/server/src/express-app.js b/server/src/express-app.js
index d6551be8c3..f877669ed5 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -20,6 +20,7 @@ const pouchRepStream = require('pouchdb-replication-stream');
 PouchDB.plugin(require('pouchdb-find'));
 PouchDB.plugin(pouchRepStream.plugin);
 PouchDB.adapter('writableStream', pouchRepStream.adapters.writableStream);
+const {DB} = require('./db')
 const pako = require('pako')
 const compression = require('compression')
 const chalk = require('chalk');
@@ -55,6 +56,7 @@ const {
   createEventForm,
   getEventFormData
 } = require('./case-api')
+const { createUserProfile } = require('./user-profile')
 log.info('heartbeat')
 setInterval(() => log.info('heartbeat'), 5*60*1000)
 const cookieParser = require('cookie-parser');
@@ -187,6 +189,12 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
  app.get('/configuration/archiveToDisk', isAuthenticated, archiveToDiskConfig);
  app.get('/configuration/passwordPolicyConfig', isAuthenticated, passwordPolicyConfig);
 
+/**
+ * User Profile API
+ */
+
+app.post('/userProfile/create/:groupId', createUserProfile);
+
 /**
  * Case API routes
  */
diff --git a/server/src/user-profile.js b/server/src/user-profile.js
new file mode 100644
index 0000000000..9f0068b539
--- /dev/null
+++ b/server/src/user-profile.js
@@ -0,0 +1,29 @@
+const DB = require('./db.js')
+const log = require('tangy-log').log
+const path = require('path')
+const fs = require('fs')
+
+const { UserProfile: UserProfile } = require('./classes/user-profile.class.js')
+
+createUserProfile = async (req, res) => {
+  let groupId = req.params.groupId
+
+  try {
+    let userProfile = new UserProfile(groupId)
+
+    if (Object.keys(req.body).length > 0) {
+      const inputs = req.body
+      userProfile.addInputs(inputs)
+    }
+
+    const db = new DB(groupId)
+    await db.put(userProfile);
+    res.send(userProfile._id);
+  } catch (err) {
+    res.status(500).send(err)
+  }
+}
+
+module.exports = {
+  createUserProfile
+}
\ No newline at end of file

From 69fce194df6664d6a9af5b75ea725dfd5f6ca07a Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 31 May 2024 17:29:08 -0400
Subject: [PATCH 08/59] Add app config 'requireAccessCode' for users to lock
 access to online surveys

---
 .../release-online-survey.component.html      | 43 ++++++++++++++-----
 .../release-online-survey.component.ts        |  4 +-
 .../src/app/groups/services/groups.service.ts |  7 +--
 .../core/auth/_guards/login-guard.service.ts  | 17 +++++---
 online-survey-app/src/assets/app-config.json  |  3 +-
 server/src/express-app.js                     |  5 ++-
 server/src/online-survey.js                   |  3 ++
 server/src/releases.js                        | 13 +++++-
 .../src/scripts/release-online-survey-app.sh  | 10 ++++-
 9 files changed, 79 insertions(+), 26 deletions(-)

diff --git a/editor/src/app/groups/release-online-survey/release-online-survey.component.html b/editor/src/app/groups/release-online-survey/release-online-survey.component.html
index 06fa3ad30a..9820cc3daa 100644
--- a/editor/src/app/groups/release-online-survey/release-online-survey.component.html
+++ b/editor/src/app/groups/release-online-survey/release-online-survey.component.html
@@ -3,15 +3,7 @@
   <h2>Instructions: </h2>
 
   <div>
-    In the Unpublished Surveys section, click the
-    <button class="info-button">
-      <i class="material-icons tangy-location-list-icon">published_with_changes</i>
-    </button>
-    button to "Publish" an online survey. It will then be listed in the Published Surveys section.
-  </div>
-
-  <div>
-    In the Published Surveys section, use the link
+    In the <strong>Published Surveys</strong> section, use the link
     <button class="info-button">
       <i class="material-icons tangy-location-list-icon">link</i>
     </button> 
@@ -21,14 +13,40 @@ <h2>Instructions: </h2>
         <i class="material-icons tangy-location-list-icon">unpublished</i>
       </button>
       button to "Un-publish" an online survey.
+      The 
+      <button class="info-button">
+        <i class="material-icons tangy-location-list-icon">lock</i>
+      </button> button appears for surveys that require a Device User and Access Code.
   </div>
+
+  <div>
+    In the <strong>Unpublish Surveys</strong> section, click the
+    <button class="info-button">
+      <i class="material-icons tangy-location-list-icon">published_with_changes</i>
+    </button>
+    button to "Publish" an online survey. It will then be listed in the Published Surveys section.
+    To 'Publish' a survey and require a Device User to provide an Access Code, click the 
+    <button class="info-button">
+      <i class="material-icons tangy-location-list-icon">lock</i>
+    </button> button.
+  </div>
+
+  <div>
+
+  </div>
+
 </div>
 <h2 class="tangy-foreground-secondary">{{'Published Surveys'|translate}}</h2>
 <mat-list class="drag-list">
   <mat-list-item class="drag-item" *ngFor="let form of publishedSurveys; let index=index">
     <span>{{index+1}}</span>
-    <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
+    <span>&nbsp;&nbsp;</span>
     <span class="tangy-spacer" [innerHTML]="form.title|unsanitizeHtml"></span>
+    <span *ngIf="form.locked">
+      <button mat-icon-button class="lock-button">
+        <i class="material-icons mat-32 tangy-location-list-icon">lock</i>
+      </button>
+    </span>
     <span>{{form.updatedOn|date :'medium'}}
     </span>
 
@@ -49,9 +67,12 @@ <h2 class="tangy-foreground-secondary">{{'Unpublished Surveys'|translate}}</h2>
     <span class="tangy-spacer" [innerHTML]="form.title|unsanitizeHtml"></span>
 
     <span *appHasAPermission="let i;group:group._id; permission:'can_manage_forms'">
-      <button mat-icon-button (click)="publishSurvey(form.id, form.title)">
+      <button mat-icon-button (click)="publishSurvey(form.id, form.title, false)">
         <i class="material-icons mat-32 tangy-location-list-icon">published_with_changes</i>
       </button>
+      <button mat-icon-button (click)="publishSurvey(form.id, form.title, true)">
+        <i class="material-icons mat-32 tangy-location-list-icon">lock</i>
+      </button>
     </span>
 
     <span >{{form.updatedOn|date :'medium'}}
diff --git a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
index b045a663b2..bc38a1a000 100644
--- a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
+++ b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
@@ -46,9 +46,9 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
     this.publishedSurveys = surveyData.filter(e => e.published);
     this.unPublishedSurveys = surveyData.filter(e => !e.published);
   }
-  async publishSurvey(formId, appName) {
+  async publishSurvey(formId, appName, locked) {
     try {
-      await this.groupService.publishSurvey(this.groupId, formId, 'prod', appName);
+      await this.groupService.publishSurvey(this.groupId, formId, 'prod', appName, locked);
       await this.getForms();
       this.errorHandler.handleError(_TRANSLATE('Survey Published Successfully.'));
     } catch (error) {
diff --git a/editor/src/app/groups/services/groups.service.ts b/editor/src/app/groups/services/groups.service.ts
index efb25b5809..6e3f4e0db0 100644
--- a/editor/src/app/groups/services/groups.service.ts
+++ b/editor/src/app/groups/services/groups.service.ts
@@ -398,10 +398,11 @@ export class GroupsService {
     }
   }
 
-  async publishSurvey(groupId, formId, releaseType = 'prod', appName) {
+  async publishSurvey(groupId, formId, releaseType = 'prod', appName, locked=false) {
     try {
-      const response = await this.httpClient.post(`/onlineSurvey/publish/${groupId}/${formId}`, {groupId, formId}, {observe: 'response'}).toPromise();
-      await this.httpClient.get(`/editor/release-online-survey-app/${groupId}/${formId}/${releaseType}/${appName}/${response.body['uploadKey']}`).toPromise()
+      const response = await this.httpClient.post(`/onlineSurvey/publish/${groupId}/${formId}`, {groupId, formId, locked}, {observe: 'response'}).toPromise();
+      const uploadKey = response.body['uploadKey']
+      await this.httpClient.post(`/editor/release-online-survey-app/${groupId}/${formId}/${releaseType}/${appName}`, {uploadKey, locked}).toPromise()
     } catch (error) {
       this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
     }
diff --git a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
index b9986cbdef..1f4ac789a3 100644
--- a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
+++ b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
@@ -1,19 +1,26 @@
 import { Injectable } from '@angular/core';
 import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
 import { AuthenticationService } from '../_services/authentication.service';
+import { AppConfigService } from 'src/app/shared/_services/app-config.service';
 
 @Injectable()
 export class LoginGuard implements CanActivate {
 
   constructor(
     private router: Router,
-    private authenticationService: AuthenticationService
+    private authenticationService: AuthenticationService,
+    private appConfigService: AppConfigService
   ) { }
   async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    if (await this.authenticationService.isLoggedIn()) {
-      return true;
+    const config = await this.appConfigService.getAppConfig();
+    if (config['requireAccessCode'] === 'true') {
+      if (await this.authenticationService.isLoggedIn()) {
+        return true;
+      }
+      this.router.navigate(['survey-login'], { queryParams: { returnUrl: state.url } });
+      return false;
     }
-    this.router.navigate(['survey-login'], { queryParams: { returnUrl: state.url } });
-    return false;
+
+    return true;
   }
 }
diff --git a/online-survey-app/src/assets/app-config.json b/online-survey-app/src/assets/app-config.json
index 06c3866df4..58d107cffe 100644
--- a/online-survey-app/src/assets/app-config.json
+++ b/online-survey-app/src/assets/app-config.json
@@ -4,5 +4,6 @@
   "groupId":"GROUP_ID",
   "languageDirection": "ltr",
   "languageCode": "en",
-  "appName":"APP_NAME"
+  "appName":"APP_NAME",
+  "requireAccessCode": "REQUIRE_ACCESS_CODE"
 }
\ No newline at end of file
diff --git a/server/src/express-app.js b/server/src/express-app.js
index f877669ed5..9de24e7952 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -193,7 +193,7 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
  * User Profile API
  */
 
-app.post('/userProfile/create/:groupId', createUserProfile);
+app.post('/userProfile/createUserProfile/:groupId', createUserProfile);
 
 /**
  * Case API routes
@@ -321,8 +321,11 @@ app.post('/editor/release-apk/:group', isAuthenticated, releaseAPK)
 
 app.post('/editor/release-pwa/:group/', isAuthenticated, releasePWA)
 
+// TODO @deprice: This route should be removed.
 app.use('/editor/release-online-survey-app/:groupId/:formId/:releaseType/:appName/:uploadKey/', isAuthenticated, releaseOnlineSurveyApp)
 
+app.post('/editor/release-online-survey-app/:groupId/:formId/:releaseType/:appName/', isAuthenticated, releaseOnlineSurveyApp)
+
 app.use('/editor/unrelease-online-survey-app/:groupId/:formId/:releaseType/', isAuthenticated, unreleaseOnlineSurveyApp)
 
 app.post('/editor/file/save', isAuthenticated, async function (req, res) {
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 58e92f0fc4..6104647ffc 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -3,6 +3,7 @@ const GROUPS_DB = new DB('groups');
 const { v4: uuidV4 } = require('uuid');
 const createFormKey = () => uuidV4();
 const { createAccessCodeJWT } = require('./auth-utils');
+const { log } = require('console');
 
 const login = async (req, res) => {
   try {
@@ -100,6 +101,7 @@ const publishSurvey = async (req, res) => {
   let surveyInfo = {}
   try {
     const { formId, groupId } = req.params;
+    const locked = req.body.locked || false;
     const data = (await GROUPS_DB.get(groupId));
     data.onlineSurveys = data.onlineSurveys ? data.onlineSurveys : [];
     let surveysIndex = data.onlineSurveys.findIndex((e) => e.formId === formId);
@@ -109,6 +111,7 @@ const publishSurvey = async (req, res) => {
       updatedOn: new Date(),
       updatedBy: req.user.name,
       uploadKey: createFormKey(),
+      locked: locked
     };
     if (surveysIndex < 0) {
       data.onlineSurveys = [...data.onlineSurveys, surveyInfo];
diff --git a/server/src/releases.js b/server/src/releases.js
index 744ab6441f..21f8b20acb 100644
--- a/server/src/releases.js
+++ b/server/src/releases.js
@@ -63,14 +63,23 @@ const releasePWA = async (req, res)=>{
 
 const releaseOnlineSurveyApp = async(req, res) => {
 	// @TODO Make sure user is member of group.
+	debugger;
 	const groupId = sanitize(req.params.groupId)
 	const formId = sanitize(req.params.formId)
 	const releaseType = sanitize(req.params.releaseType)
 	const appName = sanitize(req.params.appName)
-	const uploadKey = sanitize(req.params.uploadKey)
+
+	let uploadKey;
+	if (req.params.uploadKey) {
+		uploadKey = sanitize(req.params.uploadKey)
+	} else {
+		uploadKey = sanitize(req.body.uploadKey)
+	}
+	debugger;
+	const requireAccessCode = req.body.locked ? req.body.locked : false
 
 	try {
-		const cmd = `release-online-survey-app ${groupId} ${formId} ${releaseType} "${appName}" ${uploadKey} `
+		const cmd = `release-online-survey-app ${groupId} ${formId} ${releaseType} "${appName}" ${uploadKey} ${requireAccessCode}`
 		log.info(`RELEASING Online survey app: ${cmd}`)
 		await exec(cmd)
 		res.send({ statusCode: 200, data: 'ok' })
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index 634393bd0a..b37034462d 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -5,13 +5,14 @@ FORM_ID="$2"
 RELEASE_TYPE="$3"
 APP_NAME=$(echo "$4" | sed "s/ /_/g") # sanitize spaces in app name
 UPLOAD_KEY="$5"
+REQUIRE_ACCESS_CODE="$6"
 
 if [ "$2" = "--help" ] || [ "$GROUP_ID" = "" ] || [ "$FORM_ID" = "" ] || [ "$RELEASE_TYPE" = "" ]; then
   echo ""
   echo "RELEASE Online Survey App"
   echo "A command for releasing the Online Survey App for a specific form in a group."
   echo ""
-  echo "./release-online-survey-app.sh <groupId> <formId> <releaseType> <appName> <uploadKey>"
+  echo "./release-online-survey-app.sh <groupId> <formId> <releaseType> <appName> <uploadKey> [requireAccessCode]"
   echo ""
   echo "Release type is either qa or prod."
   echo ""
@@ -54,10 +55,17 @@ cp /tangerine/translations/*.json $RELEASE_DIRECTORY/assets/
 
 FORM_UPLOAD_URL="/onlineSurvey/saveResponse/$GROUP_ID/$FORM_ID"
 
+if [[ $REQUIRE_ACCESS_CODE == 'true' ]]; then
+  REQUIRE_ACCESS_CODE="true"
+else
+  REQUIRE_ACCESS_CODE="false"
+fi
+
 # NOTE: App Config does NOT come from the app-config.json file in the release directory
 sed -i -e "s#GROUP_ID#"$GROUP_ID"#g" $RELEASE_DIRECTORY/assets/app-config.json
 sed -i -e "s#FORM_UPLOAD_URL#"$FORM_UPLOAD_URL"#g" $RELEASE_DIRECTORY/assets/app-config.json
 sed -i -e "s#UPLOAD_KEY#"$UPLOAD_KEY"#g" $RELEASE_DIRECTORY/assets/app-config.json
 sed -i -e "s#APP_NAME#"$APP_NAME"#g" $RELEASE_DIRECTORY/assets/app-config.json
+sed -i -e "s#REQUIRE_ACCESS_CODE#"$REQUIRE_ACCESS_CODE"#g" $RELEASE_DIRECTORY/assets/app-config.json
 
 echo "Release with UUID of $UUID to $RELEASE_DIRECTORY with Build ID of $BUILD_ID, channel of $RELEASE_TYPE"

From d85217891c9af59564e84fbe85f1c92afcc38bdd Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 3 Jun 2024 15:17:33 -0400
Subject: [PATCH 09/59] Reorder span items in release online survey component

---
 .../release-online-survey.component.html                      | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/editor/src/app/groups/release-online-survey/release-online-survey.component.html b/editor/src/app/groups/release-online-survey/release-online-survey.component.html
index 9820cc3daa..3f98252b5f 100644
--- a/editor/src/app/groups/release-online-survey/release-online-survey.component.html
+++ b/editor/src/app/groups/release-online-survey/release-online-survey.component.html
@@ -65,7 +65,7 @@ <h2 class="tangy-foreground-secondary">{{'Unpublished Surveys'|translate}}</h2>
     <span>{{index+1}}</span>
     <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
     <span class="tangy-spacer" [innerHTML]="form.title|unsanitizeHtml"></span>
-
+    <span >{{form.updatedOn|date :'medium'}}
     <span *appHasAPermission="let i;group:group._id; permission:'can_manage_forms'">
       <button mat-icon-button (click)="publishSurvey(form.id, form.title, false)">
         <i class="material-icons mat-32 tangy-location-list-icon">published_with_changes</i>
@@ -74,8 +74,6 @@ <h2 class="tangy-foreground-secondary">{{'Unpublished Surveys'|translate}}</h2>
         <i class="material-icons mat-32 tangy-location-list-icon">lock</i>
       </button>
     </span>
-
-    <span >{{form.updatedOn|date :'medium'}}
     </span>
   </mat-list-item>
 </mat-list>

From 1ef7c00d35638a1eee6d497bfbd64b25803232c0 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 3 Jun 2024 15:17:58 -0400
Subject: [PATCH 10/59] Include custom-scripts in online-survey releases

---
 online-survey-app/src/index.html                | 2 ++
 server/src/scripts/release-online-survey-app.sh | 4 ++++
 2 files changed, 6 insertions(+)

diff --git a/online-survey-app/src/index.html b/online-survey-app/src/index.html
index 8b9e161294..64c8e5db67 100644
--- a/online-survey-app/src/index.html
+++ b/online-survey-app/src/index.html
@@ -19,4 +19,6 @@
     }
   </script>
 </body>
+<script type="module" src="./assets/custom-scripts.js" defer></script>
+<script type="module" src="./assets/alternative-scripts.js" defer></script>
 </html>
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index b37034462d..2cce11623b 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -38,6 +38,8 @@ FORM_DIRECTORY="$FORM_CLIENT_DIRECTORY/$FORM_ID"
 LOCATION_LIST_PATH="$FORM_CLIENT_DIRECTORY/location-list.json"
 LOCATION_LISTS_DIRECTORY="$FORM_CLIENT_DIRECTORY/locations"
 MEDIA_DIRECTORY="$FORM_CLIENT_DIRECTORY/media"
+CUSTOM_SCRIPTS="$FORM_CLIENT_DIRECTORY/custom-scripts.js"
+CUSTOM_SCRIPTS_MAP="$FORM_CLIENT_DIRECTORY/custom-scripts.js.map"
 
 # Set up the release dir from the dist
 cp -r /tangerine/online-survey-app/dist/online-survey-app/ $RELEASE_DIRECTORY
@@ -52,6 +54,8 @@ cp $LOCATION_LIST_PATH $RELEASE_DIRECTORY/assets/
 cp -r $LOCATION_LISTS_DIRECTORY $RELEASE_DIRECTORY/assets/
 cp -r $MEDIA_DIRECTORY $RELEASE_DIRECTORY/assets/
 cp /tangerine/translations/*.json $RELEASE_DIRECTORY/assets/
+cp $CUSTOM_SCRIPTS $RELEASE_DIRECTORY/assets/
+cp $CUSTOM_SCRIPTS_MAP $RELEASE_DIRECTORY/assets/
 
 FORM_UPLOAD_URL="/onlineSurvey/saveResponse/$GROUP_ID/$FORM_ID"
 

From c4b6c68c492d2d4124ad5c94e8e410bc10d87f79 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 3 Jun 2024 16:07:41 -0400
Subject: [PATCH 11/59] Online Survey - save case variables to local storage

---
 .../src/app/app-routing.module.ts             |  2 +-
 .../tangy-forms-player.component.ts           | 37 ++++++++++++++-----
 2 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/online-survey-app/src/app/app-routing.module.ts b/online-survey-app/src/app/app-routing.module.ts
index 7370b484d0..d08958145b 100644
--- a/online-survey-app/src/app/app-routing.module.ts
+++ b/online-survey-app/src/app/app-routing.module.ts
@@ -12,7 +12,7 @@ const routes: Routes = [
   { path: 'form-submitted-success', component: FormSubmittedSuccessComponent, canActivate: [LoginGuard] },
   { path: 'form/:formId', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
   { path: 'form/option/:formId/:option', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
-  { path: 'caseFormResponse/:caseEventFormId', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
+  { path: 'case/event/form/:case/:event/:form', component: TangyFormsPlayerComponent, canActivate: [LoginGuard] },
 ];
 
 @NgModule({
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
index b3d3a7eccf..4b2183bb69 100644
--- a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
@@ -13,14 +13,18 @@ export class TangyFormsPlayerComponent implements OnInit {
 
   formId: string;
   formResponseId: string;
-  caseEventFormId: string;
+  caseId: string;
+  caseEventId: string;
+  eventFormId: string;
   
   constructor(private route: ActivatedRoute, private formsService: FormsService, private router: Router, private httpClient:HttpClient
   ) { 
     this.router.events.subscribe(async (event) => {
         this.formId = this.route.snapshot.paramMap.get('formId');
         this.formResponseId = this.route.snapshot.paramMap.get('formResponseId');
-        this.caseEventFormId = this.route.snapshot.paramMap.get('caseEventFormId');
+        this.caseId = this.route.snapshot.paramMap.get('caseId');
+        this.caseEventId = this.route.snapshot.paramMap.get('caseEventId');
+        this.eventFormId = this.route.snapshot.paramMap.get('eventFormId');
     });
   }
 
@@ -29,22 +33,37 @@ export class TangyFormsPlayerComponent implements OnInit {
     this.container.nativeElement.innerHTML = data;
     let tangyForm = this.container.nativeElement.querySelector('tangy-form');
 
-    if (this.caseEventFormId) {
+    if (this.caseId) {
       try {
-        const eventForm = await this.formsService.getEventFormData(this.caseEventFormId);
-        if (eventForm && eventForm.formResponseId) {
-          tangyForm.response = await this.formsService.getFormResponse(eventForm.formResponseId);
+        const caseDoc = await this.formsService.getFormResponse(this.caseId);
+        if (caseDoc) {
+          let inputs = caseDoc.items[0].inputs;
+          if (inputs.length > 0) {
+            window.localStorage.setItem('caseVariables', JSON.stringify(inputs));
+          }
         }
       } catch (error) {
-        //pass
+        console.log('Error loading case variables: ' + error)
+      }
+
+      if (this.eventFormId) {
+        try {
+          // Attempt to load the form response for the event form
+          const eventForm = await this.formsService.getEventFormData(this.eventFormId);
+          if (eventForm && eventForm.formResponseId) {
+            tangyForm.response = await this.formsService.getFormResponse(eventForm.formResponseId);
+          }
+        } catch (error) {
+          //pass
+        }
       }
     }
 
     tangyForm.addEventListener('after-submit', async (event) => {
       event.preventDefault();
       try {
-        if (this.caseEventFormId) {
-          if (await this.formsService.uploadFormResponseForCase(event.target.response, this.caseEventFormId)) {
+        if (this.eventFormId) {
+          if (await this.formsService.uploadFormResponseForCase(event.target.response, this.eventFormId)) {
             this.router.navigate(['/form-submitted-success']);
           } else {
             alert('Form could not be submitted. Please retry');

From 4d8c5b6035bc461a35e174f8d1a30c3c745ab323 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 4 Jun 2024 10:25:06 -0400
Subject: [PATCH 12/59] Add case module to online-survey-app

---
 develop.sh                                    |    1 -
 online-survey-app/package.json                |    2 +
 online-survey-app/src/app/app-context.enum.ts |    4 +
 .../src/app/app-routing.module.ts             |    2 +-
 online-survey-app/src/app/app.component.ts    |    6 +-
 online-survey-app/src/app/app.module.ts       |    8 +-
 online-survey-app/src/app/case/case.module.ts |   25 +
 .../app/case/classes/case-definition.class.ts |   48 +
 .../classes/case-event-definition.class.ts    |   31 +
 .../src/app/case/classes/case-event.class.ts  |   25 +
 .../case/classes/case-participant.class.ts    |    7 +
 .../src/app/case/classes/case-role.class.ts   |    7 +
 .../src/app/case/classes/case.class.ts        |   29 +
 .../classes/event-form-definition.class.ts    |   21 +
 .../src/app/case/classes/event-form.class.ts  |   17 +
 .../app/case/classes/notification.class.ts    |   29 +
 .../src/app/case/classes/query.class.ts       |   46 +
 .../services/case-definitions.service.spec.ts |   13 +
 .../case/services/case-definitions.service.ts |   28 +
 .../case/services/case-event-info.class.ts    |   12 +
 .../app/case/services/case.service.spec.ts    |  292 +++++
 .../src/app/case/services/case.service.ts     | 1101 +++++++++++++++++
 .../app/case/services/cases.service.spec.ts   |  264 ++++
 .../src/app/case/services/cases.service.ts    |   46 +
 .../shared/_services/app-config.service.ts    |   29 +
 .../app/shared/classes/user-database.class.ts |  130 +-
 .../tangy-forms/classes/form-info.class.ts    |   63 +
 .../tangy-forms/classes/form-version.class.ts |    5 +
 .../src/app/tangy-forms/safe-url.pipe.ts      |   13 +
 .../tangy-forms/tangy-form-response.class.ts  |   43 +
 .../src/app/tangy-forms/tangy-form-service.ts |   41 +
 .../src/app/tangy-forms/tangy-form.service.ts |   72 ++
 .../tangy-forms/tangy-forms-info-service.ts   |   38 +
 .../tangy-forms-player.component.css          |    0
 .../tangy-forms-player.component.html         |    0
 .../tangy-forms-player.component.spec.ts      |    0
 .../tangy-forms-player.component.ts           |   49 +-
 .../tangy-forms/tangy-forms-routing.module.ts |   15 +
 .../src/app/tangy-forms/tangy-forms.module.ts |   32 +
 .../src/scripts/release-online-survey-app.sh  |   23 +-
 40 files changed, 2503 insertions(+), 114 deletions(-)
 create mode 100644 online-survey-app/src/app/app-context.enum.ts
 create mode 100644 online-survey-app/src/app/case/case.module.ts
 create mode 100644 online-survey-app/src/app/case/classes/case-definition.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/case-event-definition.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/case-event.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/case-participant.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/case-role.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/case.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/event-form-definition.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/event-form.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/notification.class.ts
 create mode 100644 online-survey-app/src/app/case/classes/query.class.ts
 create mode 100644 online-survey-app/src/app/case/services/case-definitions.service.spec.ts
 create mode 100644 online-survey-app/src/app/case/services/case-definitions.service.ts
 create mode 100644 online-survey-app/src/app/case/services/case-event-info.class.ts
 create mode 100644 online-survey-app/src/app/case/services/case.service.spec.ts
 create mode 100644 online-survey-app/src/app/case/services/case.service.ts
 create mode 100644 online-survey-app/src/app/case/services/cases.service.spec.ts
 create mode 100644 online-survey-app/src/app/case/services/cases.service.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/classes/form-info.class.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/classes/form-version.class.ts
 create mode 100755 online-survey-app/src/app/tangy-forms/safe-url.pipe.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/tangy-form-response.class.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/tangy-form-service.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/tangy-form.service.ts
 create mode 100644 online-survey-app/src/app/tangy-forms/tangy-forms-info-service.ts
 rename online-survey-app/src/app/{ => tangy-forms}/tangy-forms-player/tangy-forms-player.component.css (100%)
 rename online-survey-app/src/app/{ => tangy-forms}/tangy-forms-player/tangy-forms-player.component.html (100%)
 rename online-survey-app/src/app/{ => tangy-forms}/tangy-forms-player/tangy-forms-player.component.spec.ts (100%)
 rename online-survey-app/src/app/{ => tangy-forms}/tangy-forms-player/tangy-forms-player.component.ts (65%)
 create mode 100755 online-survey-app/src/app/tangy-forms/tangy-forms-routing.module.ts
 create mode 100755 online-survey-app/src/app/tangy-forms/tangy-forms.module.ts

diff --git a/develop.sh b/develop.sh
index 7182a9f976..636a36b88b 100755
--- a/develop.sh
+++ b/develop.sh
@@ -75,7 +75,6 @@ fi
 
 if echo "$T_MODULES" | grep mysql; then
   ./mysql-create-dirs.sh
-fi
 
   if echo "$T_USE_MYSQL_CONTAINER" | grep "true"; then
     ./mysql-start-container.sh
diff --git a/online-survey-app/package.json b/online-survey-app/package.json
index feb1c86b68..364098872d 100644
--- a/online-survey-app/package.json
+++ b/online-survey-app/package.json
@@ -24,6 +24,8 @@
     "@ngx-translate/core": "^11.0.1",
     "@ngx-translate/http-loader": "^4.0.0",
     "@webcomponents/webcomponentsjs": "^2.4.4",
+    "axios": "^0.21.4",
+    "fast-json-patch": "^3.1.1",
     "jwt-decode": "^4.0.0",
     "material-design-icons-iconfont": "^6.1.0",
     "redux": "^4.0.5",
diff --git a/online-survey-app/src/app/app-context.enum.ts b/online-survey-app/src/app/app-context.enum.ts
new file mode 100644
index 0000000000..e627f5e55d
--- /dev/null
+++ b/online-survey-app/src/app/app-context.enum.ts
@@ -0,0 +1,4 @@
+export enum AppContext {
+  Editor = 'EDITOR',
+  Client = 'CLIENT'
+}
diff --git a/online-survey-app/src/app/app-routing.module.ts b/online-survey-app/src/app/app-routing.module.ts
index d08958145b..4d774915d8 100644
--- a/online-survey-app/src/app/app-routing.module.ts
+++ b/online-survey-app/src/app/app-routing.module.ts
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
 import { FormSubmittedSuccessComponent } from './form-submitted-success/form-submitted-success.component';
 import { FormsListComponent } from './forms-list/forms-list.component';
-import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
+import { TangyFormsPlayerComponent } from './tangy-forms/tangy-forms-player/tangy-forms-player.component';
 import { LoginGuard } from './core/auth/_guards/login-guard.service';
 import { SurveyLoginComponent } from './core/auth/_components/survey-login/survey-login.component';
 
diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index 3d0d582a40..3d856bcc62 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -1,5 +1,6 @@
 import { Component, OnInit } from '@angular/core';
 import { AppConfigService } from './shared/_services/app-config.service';
+import { CaseService } from './case/services/case.service';
 
 @Component({
   selector: 'app-root',
@@ -9,7 +10,9 @@ import { AppConfigService } from './shared/_services/app-config.service';
 export class AppComponent implements OnInit{
   languageDirection: string;
   appName: string;
-  constructor(private appConfigService: AppConfigService){
+  window: any;
+
+  constructor(private appConfigService: AppConfigService, private caseService: CaseService){
   }
 
   async ngOnInit(): Promise<any>{
@@ -17,6 +20,7 @@ export class AppComponent implements OnInit{
       const appConfig = await this.appConfigService.getAppConfig();
       this.appName = appConfig.appName;
       this.languageDirection = appConfig.languageDirection;
+
     } catch (error) {
       this.appName = '';
       this.languageDirection = 'ltr';
diff --git a/online-survey-app/src/app/app.module.ts b/online-survey-app/src/app/app.module.ts
index 8080a7af5b..2ed7fa567b 100644
--- a/online-survey-app/src/app/app.module.ts
+++ b/online-survey-app/src/app/app.module.ts
@@ -13,17 +13,17 @@ import {MatListModule} from '@angular/material/list';
 import { MatIconModule } from '@angular/material/icon';
 import { MatSnackBarModule } from '@angular/material/snack-bar';
 import { TangySvgLogoComponent } from './shared/tangy-svg-logo/tangy-svg-logo.component';
-import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
+import { TangyFormsModule } from './tangy-forms/tangy-forms.module';
 import { HttpClientModule } from '@angular/common/http';
 import { FormsListComponent } from './forms-list/forms-list.component';
 import { FormSubmittedSuccessComponent } from './form-submitted-success/form-submitted-success.component';
 import { TangyErrorHandler } from './shared/_services/tangy-error-handler.service';
+import { CaseModule } from './case/case.module';
 
 @NgModule({
   declarations: [
     AppComponent,
     TangySvgLogoComponent,
-    TangyFormsPlayerComponent,
     FormsListComponent,
     FormSubmittedSuccessComponent
   ],
@@ -32,6 +32,7 @@ import { TangyErrorHandler } from './shared/_services/tangy-error-handler.servic
     AppRoutingModule,
     BrowserModule,
     BrowserAnimationsModule,
+    CaseModule,
     MatToolbarModule,
     MatMenuModule,
     MatIconModule,
@@ -39,7 +40,8 @@ import { TangyErrorHandler } from './shared/_services/tangy-error-handler.servic
     MatCardModule,
     MatTabsModule,
     MatListModule,
-    MatSnackBarModule
+    MatSnackBarModule,
+    TangyFormsModule
   ],
   schemas: [CUSTOM_ELEMENTS_SCHEMA],
   providers: [TangyErrorHandler],
diff --git a/online-survey-app/src/app/case/case.module.ts b/online-survey-app/src/app/case/case.module.ts
new file mode 100644
index 0000000000..4ade6ed730
--- /dev/null
+++ b/online-survey-app/src/app/case/case.module.ts
@@ -0,0 +1,25 @@
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '../shared/shared.module'
+import { CaseDefinitionsService } from './services/case-definitions.service';
+import { CaseService } from './services/case.service';
+import { TangyFormsModule } from '../tangy-forms/tangy-forms.module';
+
+
+@NgModule({
+  schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
+  exports: [
+  ],
+  imports: [
+    SharedModule,
+    TangyFormsModule,
+    CommonModule
+  ],
+  providers: [
+    CaseDefinitionsService,
+    CaseService
+  ],
+  declarations: [
+  ]
+})
+export class CaseModule { }
diff --git a/online-survey-app/src/app/case/classes/case-definition.class.ts b/online-survey-app/src/app/case/classes/case-definition.class.ts
new file mode 100644
index 0000000000..d3ebea9676
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case-definition.class.ts
@@ -0,0 +1,48 @@
+import { CaseRole } from './case-role.class';
+import { CaseEventDefinition } from './case-event-definition.class'
+
+class CaseDefinition {
+
+  id:string
+  formId:string
+  name:string
+  caseRoles?:Array<CaseRole>
+  description?:string
+  eventDefinitions: Array<CaseEventDefinition> = []
+  templateBreadcrumbText?:string 
+  templateCaseTitle?:string 
+  templateCaseDescription?:string 
+  templateCaseEventListItemIcon?:string 
+  templateCaseEventListItemPrimary?:string 
+  templateCaseEventListItemSecondary?:string 
+  templateEventFormListItemIcon?:string 
+  templateEventFormListItemPrimary?:string 
+  templateEventFormListItemSecondary?:string 
+  startFormOnOpen?: CaseFormPath
+  templateScheduleListItemIcon?:string
+  templateScheduleListItemPrimary?:string
+  templateScheduleListItemSecondary?:string
+  templateIssueTitle?:string
+  templateIssueDescription?:string
+  onCaseOpen?:string
+  onCaseClose?:string
+  constructor(init:any) {
+    Object.assign(this, init);
+    /*
+    this.id = caseDefinitionData.id;
+    this.revision = caseDefinitionData.revision;
+    this.name = caseDefinitionData.name;
+    this.eventDefinitions = caseDefinitionData
+      .eventDefinitions
+      .map(eventDefinition => new CaseEventDefinition(eventDefinition))
+    */
+  }
+
+}
+
+class CaseFormPath {
+  eventId:string
+  eventFormId:string
+}
+
+export { CaseDefinition }
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/case-event-definition.class.ts b/online-survey-app/src/app/case/classes/case-event-definition.class.ts
new file mode 100644
index 0000000000..5729538018
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case-event-definition.class.ts
@@ -0,0 +1,31 @@
+import { EventFormDefinition } from './event-form-definition.class'
+
+class CaseEventDefinition {
+
+  id:string
+  name:string
+  description:string
+  repeatable:boolean
+  required:boolean
+  templateListItemIcon:string
+  templateListItemPrimary:string
+  templateListItemSecondary:string
+  templateCaseEventListItemIcon:string
+  templateCaseEventListItemPrimary:string
+  templateCaseEventListItemSecondary:string
+  templateEventFormListItemIcon:string
+  templateEventFormListItemPrimary:string
+  templateEventFormListItemSecondary:string
+  estimatedTimeFromCaseOpening:number
+  estimatedTimeWindow:number
+  eventFormDefinitions:Array<EventFormDefinition> = []
+  onEventOpen:string
+  onEventClose:string
+  onEventCreate:string
+
+  constructor() {
+  }
+
+}
+
+export { CaseEventDefinition }
diff --git a/online-survey-app/src/app/case/classes/case-event.class.ts b/online-survey-app/src/app/case/classes/case-event.class.ts
new file mode 100644
index 0000000000..454b109469
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case-event.class.ts
@@ -0,0 +1,25 @@
+import { EventForm } from './event-form.class'
+
+class CaseEvent {
+  id?: string
+  caseId: string
+  caseEventDefinitionId:string
+  eventForms: Array<EventForm> = []
+  complete:boolean = false
+  estimate = true
+  estimatedDay: string
+  scheduledDay: string
+  windowStartDay: string
+  windowEndDay: string
+  occurredOnDay: string
+  archived:boolean = false
+  // started date
+  // last updated date
+
+  name: string
+  constructor() {
+
+  }
+}
+
+export { CaseEvent }
diff --git a/online-survey-app/src/app/case/classes/case-participant.class.ts b/online-survey-app/src/app/case/classes/case-participant.class.ts
new file mode 100644
index 0000000000..73b4a4deae
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case-participant.class.ts
@@ -0,0 +1,7 @@
+interface CaseParticipant {
+  id:string
+  caseRoleId:string
+  data:any  
+}
+
+export { CaseParticipant }
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/case-role.class.ts b/online-survey-app/src/app/case/classes/case-role.class.ts
new file mode 100644
index 0000000000..83badf5ec5
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case-role.class.ts
@@ -0,0 +1,7 @@
+
+export interface CaseRole {
+  id:string
+  label:string
+  templateListItem:string
+}
+
diff --git a/online-survey-app/src/app/case/classes/case.class.ts b/online-survey-app/src/app/case/classes/case.class.ts
new file mode 100644
index 0000000000..195ab8dd79
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/case.class.ts
@@ -0,0 +1,29 @@
+import { CaseEvent } from './case-event.class'
+import {TangyFormResponseModel} from 'tangy-form/tangy-form-response-model.js'
+import { CaseParticipant } from './case-participant.class';
+import { Notification } from './notification.class';
+
+class Case extends TangyFormResponseModel {
+  
+  _id:string
+  _rev:string
+  caseDefinitionId:string
+  label:string
+  status:string
+  openedDate:number
+  participants:Array<CaseParticipant> = []
+  disabledEventDefinitionIds: Array<string> = []
+  events: Array<CaseEvent> = []
+  notifications: Array<Notification> = []
+  type:string = 'case'
+  archived:boolean
+  groupId:string
+  
+  constructor(data?:any) {
+    super()
+    Object.assign(this, data)
+  }
+  
+}
+
+export { Case }
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/event-form-definition.class.ts b/online-survey-app/src/app/case/classes/event-form-definition.class.ts
new file mode 100644
index 0000000000..1b0782fddd
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/event-form-definition.class.ts
@@ -0,0 +1,21 @@
+export class EventFormDefinition {
+
+  id:string
+  formId:string
+  name:string
+  autoPopulate:boolean
+  forCaseRole:string
+  templateListItem:string
+  repeatable: boolean
+  required:boolean
+  templateListItemIcon:string
+  templateListItemPrimary:string
+  templateListItemSecondary:string
+  allowDeleteIfFormNotCompleted:string
+  allowDeleteIfFormNotStarted:string
+  onEventFormOpen?:string
+  onEventFormClose?:string
+
+  constructor() {
+  }
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/event-form.class.ts b/online-survey-app/src/app/case/classes/event-form.class.ts
new file mode 100644
index 0000000000..e83d9c57c4
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/event-form.class.ts
@@ -0,0 +1,17 @@
+class EventForm {
+  id:string;
+  participantId:string
+  complete:boolean = false
+  archived?:boolean = false
+  required:boolean
+  caseId:string; 
+  caseEventId:string;
+  eventFormDefinitionId:string;
+  formResponseId:string;
+  data?:any;
+  constructor() {
+
+  }
+}
+
+export { EventForm }
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/notification.class.ts b/online-survey-app/src/app/case/classes/notification.class.ts
new file mode 100644
index 0000000000..cf2140e780
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/notification.class.ts
@@ -0,0 +1,29 @@
+import { AppContext } from './../../app-context.enum';
+export enum NotificationStatus {
+  Open='Open',
+  Closed='Closed'
+}
+
+export enum NotificationType {
+  Critical='Critical',
+  Info='Info',
+}
+
+class Notification {
+  id: string
+  label:string
+  description:string
+  link:string
+  icon:string
+  color:string
+  enforceAttention:boolean
+  persist:boolean
+  status:NotificationStatus 
+  createdOn:number
+  createdAppContext:AppContext
+  constructor(data?:any) {
+    Object.assign(this, data)
+  }
+}
+
+export { Notification }
\ No newline at end of file
diff --git a/online-survey-app/src/app/case/classes/query.class.ts b/online-survey-app/src/app/case/classes/query.class.ts
new file mode 100644
index 0000000000..34d25659f2
--- /dev/null
+++ b/online-survey-app/src/app/case/classes/query.class.ts
@@ -0,0 +1,46 @@
+class Query {
+  queryId: string;
+
+  associatedCaseId: string;
+  associatedCaseType: string;
+  associatedEventId: string;
+  associatedFormId: string;
+  associatedCaseName: string;
+  associatedEventName: string;
+  associatedFormName: string;
+  associatedFormLink: string;
+  associatedVariable: string;
+
+  queryTypeId: string;
+  queryLink: string;
+  queryText: string;
+  queryResponse: string;
+  queryStatus: string;
+  queryDate: string;
+  queryResponseDate: string;
+
+  constructor(queryId, associatedCaseId, associatedEventId, associatedFormId, associatedCaseName,
+    associatedEventName, associatedFormName, associatedFormLink, associatedCaseType, queryLink, queryDate, queryText,
+    queryStatus, queryResponse?, queryResponseDate?, associatedVariable?, queryTypeId?) {
+
+    this.queryId = queryId;
+    this.associatedCaseId = associatedCaseId;
+    this.associatedCaseType = associatedCaseType;
+    this.associatedEventId = associatedEventId;
+    this.associatedFormId = associatedFormId;
+    this.associatedEventName = associatedEventName;
+    this.associatedFormName = associatedFormName;
+    this.associatedFormLink = associatedFormLink;
+    this.associatedCaseName = associatedCaseName;
+    this.associatedVariable = associatedVariable;
+    this.queryTypeId = queryTypeId;
+    this.queryLink = queryLink;
+    this.queryDate = queryDate;
+    this.queryText = queryText;
+    this.queryStatus = queryStatus;
+    this.queryResponse = queryResponse;
+    this.queryResponseDate = queryResponseDate;
+  }
+}
+
+export { Query };
diff --git a/online-survey-app/src/app/case/services/case-definitions.service.spec.ts b/online-survey-app/src/app/case/services/case-definitions.service.spec.ts
new file mode 100644
index 0000000000..9bc4576d96
--- /dev/null
+++ b/online-survey-app/src/app/case/services/case-definitions.service.spec.ts
@@ -0,0 +1,13 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CaseDefinitionsService } from './case-definitions.service';
+import { HttpClientModule } from '@angular/common/http';
+
+describe('CaseDefinitionsService', () => {
+  beforeEach(() => TestBed.configureTestingModule({imports: [ HttpClientModule ]}));
+
+  it('should be created', () => {
+    const service: CaseDefinitionsService = TestBed.get(CaseDefinitionsService);
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/online-survey-app/src/app/case/services/case-definitions.service.ts b/online-survey-app/src/app/case/services/case-definitions.service.ts
new file mode 100644
index 0000000000..899c37603b
--- /dev/null
+++ b/online-survey-app/src/app/case/services/case-definitions.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+import { CaseDefinition } from '../classes/case-definition.class';
+import { HttpClient } from '@angular/common/http';
+import {FormInfo} from '../../tangy-forms/classes/form-info.class';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CaseDefinitionsService {
+  caseDefinitionReferences:Array<any>
+  caseDefinitions:Array<CaseDefinition>
+  constructor(
+    private http: HttpClient
+  ) { }
+
+  async load():Promise<any> {
+    this.caseDefinitionReferences = this.caseDefinitionReferences ? this.caseDefinitionReferences : <Array<FormInfo>>await this.http.get('./assets/case-definitions.json').toPromise()
+    if (!this.caseDefinitions) {
+      this.caseDefinitions = []
+      for (const reference of this.caseDefinitionReferences) {
+        const definition = <CaseDefinition>await this.http.get(reference.src).toPromise();
+        this.caseDefinitions.push(definition)
+      }
+    }
+    return this.caseDefinitions;
+  }
+
+}
diff --git a/online-survey-app/src/app/case/services/case-event-info.class.ts b/online-survey-app/src/app/case/services/case-event-info.class.ts
new file mode 100644
index 0000000000..dfafd79688
--- /dev/null
+++ b/online-survey-app/src/app/case/services/case-event-info.class.ts
@@ -0,0 +1,12 @@
+import { Case } from '../classes/case.class';
+import { CaseEvent } from '../classes/case-event.class';
+import { CaseDefinition } from '../classes/case-definition.class';
+
+ class CaseEventInfo extends CaseEvent {
+caseInstance?:Case
+caseDefinition?:CaseDefinition
+constructor() {
+super()
+}
+}
+export {CaseEventInfo}
diff --git a/online-survey-app/src/app/case/services/case.service.spec.ts b/online-survey-app/src/app/case/services/case.service.spec.ts
new file mode 100644
index 0000000000..4ca59f8fdb
--- /dev/null
+++ b/online-survey-app/src/app/case/services/case.service.spec.ts
@@ -0,0 +1,292 @@
+import { AppConfigService } from './../../shared/_services/app-config.service';
+import { CaseRole } from './../classes/case-role.class';
+import { CASE_EVENT_STATUS_COMPLETED, CASE_EVENT_STATUS_IN_PROGRESS } from './../classes/case-event.class';
+import { TestBed } from '@angular/core/testing';
+
+import { CaseService } from './case.service';
+import { CaseDefinitionsService } from './case-definitions.service';
+import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service';
+import { UserService } from 'src/app/shared/_services/user.service';
+// NOTE: For some reason if this is WindowRef from the shared module, this fails to inject.
+import { CaseDefinition } from '../classes/case-definition.class';
+import { EventFormDefinition } from '../classes/event-form-definition.class';
+import { CaseEventDefinition } from '../classes/case-event-definition.class';
+import PouchDB from 'pouchdb';
+import { HttpClient } from '@angular/common/http';
+import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
+import { CaseParticipant } from '../classes/case-participant.class';
+import moment from 'moment/src/moment';
+class MockCaseDefinitionsService {
+  async load() {
+    return <Array<CaseDefinition>>[
+      {
+        'id': 'caseDefinition1',
+        'formId': 'caseDefinition1Form',
+        'name': 'Case Type 1',
+        'description': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
+        'caseRoles': [
+          <CaseRole>{
+            id: 'role1',
+            label: 'Role 1'
+          },
+          <CaseRole>{
+            id: 'role2',
+            label: 'Role 2'
+          }
+        ],
+        'eventDefinitions': <Array<CaseEventDefinition>>[
+          {
+            'id': 'event-definition-screening',
+            'name': 'Screening',
+            'description': 'A screening.',
+            'repeatable': false,
+            'required': true,
+            'estimatedTimeFromCaseOpening': 0,
+            'estimatedTimeWindow': 0,
+            'eventFormDefinitions': [
+              <EventFormDefinition>{
+                'id': 'event-form-1',
+                'formId': 'form1',
+                'forCaseRole': 'role1',
+                'name': 'Form 1',
+                'required': true,
+                'repeatable': false
+              },
+              <EventFormDefinition>{
+                'id': 'event-form-2',
+                'formId': 'form2',
+                'forCaseRole': 'role2',
+                'name': 'Form 2',
+                'required': true,
+                'repeatable': false
+              }
+            ]
+          },
+          {
+            'id': 'event-definition-first-visit',
+            'name': 'First visit',
+            'description': 'The first visit',
+            'repeatable': false,
+            'required': true,
+            'estimatedTimeFromCaseOpening': 15552000000,
+            'estimatedTimeWindow': 2592000000,
+            'eventFormDefinitions': [
+              <EventFormDefinition>{
+                'id': 'event-form-1',
+                'formId': 'form1',
+                'forCaseRole': 'role1',
+                'name': 'Form 1',
+                'required': true,
+                'repeatable': false
+              },
+              <EventFormDefinition>{
+                'id': 'event-form-2',
+                'formId': 'form2',
+                'forCaseRole': 'role1',
+                'name': 'Form 2 (repeatable)',
+                'required': true,
+                'repeatable': true 
+              },
+              <EventFormDefinition>{
+                'id': 'event-form-3',
+                'formId': 'form3',
+                'forCaseRole': 'role1',
+                'name': 'Form 3',
+                'required': false,
+                'repeatable': false 
+              }
+            ]
+          },
+          {
+            'id': 'event-definition-repeatable-event',
+            'name': 'A Repeatable Event',
+            'description': 'A repeatable event.',
+            'repeatable': true,
+            'required': true,
+            'estimatedTimeFromCaseOpening': 0,
+            'estimatedTimeWindow': 0,
+            'eventFormDefinitions': [
+              <EventFormDefinition>{
+                'id': 'event-form-1',
+                'formId': 'form1',
+                'forCaseRole': 'role1',
+                'name': 'Form 1',
+                'required': true,
+                'repeatable': false
+              }
+            ]
+          },
+          {
+            'id': 'event-definition-not-required-event',
+            'name': 'A Event that is not require',
+            'description': 'An event that is not required.',
+            'repeatable': true,
+            'required': false,
+            'estimatedTimeFromCaseOpening': 0,
+            'estimatedTimeWindow': 0,
+            'eventFormDefinitions': [
+              <EventFormDefinition>{
+                'id': 'event-form-1',
+                'formId': 'form1',
+                'forCaseRole': 'role1',
+                'name': 'Form 1',
+                'required': true,
+                'repeatable': false
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+
+class MockTangyFormService {
+
+  response:any
+
+  async getFormMarkup(formId) {
+    return `
+      <tangy-form id='caseDefinition1Form'>
+        <tangy-form-item id='item1'>
+          <tangy-input name='input1'></tangy-input>
+        </tangy-form>
+      </tangy-form>
+    `
+  }
+  async saveResponse(response) {
+    this.response = response
+    ///
+  }
+  async getResponse(id) {
+    return this.response
+    ///
+  }
+}
+
+class MockUserService {
+  getCurrentUser() {
+    return 'test'
+  }
+  getUserDatabase(username) {
+    return new PouchDB('test')
+  }
+}
+
+describe('CaseService', () => {
+
+  let httpClient: HttpClient
+  let httpTestingController: HttpTestingController 
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        HttpClientTestingModule
+      ],
+      providers: [
+        { 
+          provide: CaseDefinitionsService,
+          useClass: MockCaseDefinitionsService
+        },
+        {
+          provide: TangyFormService,
+          useClass: MockTangyFormService
+        },
+        {
+          provide: UserService,
+          useClass: MockUserService
+        },
+        {
+          provide: AppConfigService,
+          useClass: AppConfigService 
+        }
+      ]
+    })
+    httpClient = TestBed.get(HttpClient);
+    httpTestingController = TestBed.get(HttpTestingController);
+  });
+
+  it('should be created', () => {
+    const service: CaseService = TestBed.get(CaseService);
+    expect(service).toBeTruthy();
+  });
+
+  it('should create a case' , async () => {
+    const service: CaseService = TestBed.get(CaseService);
+    await service.create('caseDefinition1')
+    expect(service.case._id).toBeTruthy()
+  })
+
+  it('should set an event ocurred on date', async () => {
+    const service: CaseService = TestBed.get(CaseService)
+    await service.create('caseDefinition1')
+    await service.createEvent('event-definition-first-visit')
+    const timeInMs = new Date().getTime()
+    const date = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    service.setEventOccurredOn(service.case.events[0].id, timeInMs)
+    expect(service.case.events[0].occurredOnDay).toEqual(date)
+  })
+  it('should set an event EstimatedDay date', async () => {
+    const service: CaseService = TestBed.get(CaseService)
+    await service.create('caseDefinition1')
+    await service.createEvent('event-definition-first-visit')
+    const timeInMs = new Date().getTime()
+    const date = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    service.setEventEstimatedDay(service.case.events[0].id, timeInMs)
+    expect(service.case.events[0].estimatedDay).toEqual(date)
+  })
+  it('should set an event ScheduledDay date', async () => {
+    const service: CaseService = TestBed.get(CaseService)
+    await service.create('caseDefinition1')
+    await service.createEvent('event-definition-first-visit')
+    const timeInMs = new Date().getTime()
+    const date = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    service.setEventScheduledDay(service.case.events[0].id, timeInMs)
+    expect(service.case.events[0].scheduledDay).toEqual(date)
+  })
+  it('should set an event Window period', async () => {
+    const service: CaseService = TestBed.get(CaseService)
+    await service.create('caseDefinition1')
+    await service.createEvent('event-definition-first-visit')
+    const windowStartDayTimeInMs = new Date().getTime()
+    const windowEndDayTimeInMs = new Date().getTime()
+    const windowStartDay = moment((new Date(windowStartDayTimeInMs))).format('YYYY-MM-DD')
+    const windowEndDay = moment((new Date(windowEndDayTimeInMs))).format('YYYY-MM-DD')
+    service.setEventWindow(service.case.events[0].id, windowStartDayTimeInMs, windowEndDayTimeInMs)
+    expect(service.case.events[0].windowStartDay).toEqual(windowStartDay)
+    expect(service.case.events[0].windowEndDay).toEqual(windowEndDay)
+  })
+
+  it('should create participant and create forms for existing events', async () => {
+    const service: CaseService = TestBed.get(CaseService)
+    await service.create('caseDefinition1')
+    await service.createEvent('event-definition-screening', true)
+    expect(service.case.events[0].eventForms.length).toEqual(0)
+    const caseParticipant = await service.createParticipant('role1')
+    expect(service.case.participants[0].id).toEqual(caseParticipant.id)
+    expect(service.case.events[0].eventForms.length).toEqual(1)
+  })
+
+  it('should create CaseEvent and also create corresponding required EventForms for Participants', async() => {
+    const service: CaseService = TestBed.get(CaseService);
+    await service.create('caseDefinition1')
+    const caseParticipant = await service.createParticipant('role1')
+    await service.createEvent('event-definition-screening', true)
+    expect(service.case.participants[0].id).toEqual(caseParticipant.id)
+    expect(service.case.events[0].eventForms.length).toEqual(1)
+  })
+
+  it('CaseEvent should have status of comleted when all required forms are completed', async () => {
+    const service: CaseService = TestBed.get(CaseService);
+    await service.create('caseDefinition1')
+    const caseParticipant = await service.createParticipant('role1')
+    const caseParticipant2 = await service.createParticipant('role2')
+    const caseEvent = await service.createEvent('event-definition-screening', true)
+    expect(service.case.events[0].status).toEqual(CASE_EVENT_STATUS_IN_PROGRESS)
+    for (const eventForm of service.case.events[0].eventForms) {
+      service.markEventFormComplete(caseEvent.id, eventForm.id)
+    }
+    expect(service.case.events[0].status).toEqual(CASE_EVENT_STATUS_COMPLETED)
+  })
+
+});
diff --git a/online-survey-app/src/app/case/services/case.service.ts b/online-survey-app/src/app/case/services/case.service.ts
new file mode 100644
index 0000000000..20b084ba35
--- /dev/null
+++ b/online-survey-app/src/app/case/services/case.service.ts
@@ -0,0 +1,1101 @@
+import { AppConfigService, LocationNode } from './../../shared/_services/app-config.service';
+import { EventFormDefinition } from './../classes/event-form-definition.class';
+import { Subject } from 'rxjs';
+import { NotificationStatus, Notification, NotificationType } from './../classes/notification.class';
+// Services.
+import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service';
+import { CaseDefinitionsService } from './case-definitions.service';
+import { HttpClient } from '@angular/common/http';
+import { UserService } from 'src/app/core/auth/_services/user.service';
+// Classes.
+import { TangyFormResponseModel } from 'tangy-form/tangy-form-response-model.js';
+import { Case } from '../classes/case.class'
+import { CaseEvent } from '../classes/case-event.class'
+import { EventForm } from '../classes/event-form.class'
+import { CaseDefinition } from '../classes/case-definition.class';
+import { CaseParticipant } from '../classes/case-participant.class';
+import { Query } from '../classes/query.class'
+// Other.
+import { v4 as UUID } from 'uuid';
+import { Injectable } from '@angular/core';
+import * as moment from 'moment';
+import { AppContext } from 'src/app/app-context.enum';
+import { CaseEventDefinition } from '../classes/case-event-definition.class';
+
+
+@Injectable({
+  providedIn: 'root'
+})
+class CaseService {
+
+  _case:Case
+  caseDefinition:CaseDefinition
+  location:Array<LocationNode>
+
+  // Opening a case confirmation semaphore.
+  openCaseConfirmed = false
+  // Query properties.
+  queryCaseEventDefinitionId: any
+  queryEventFormDefinitionId: any
+  queryFormId: any
+  _shouldSave = true
+
+
+  set case(caseInstance:Case) {
+    const caseInfo:CaseInfo = {
+      caseInstance,
+      caseDefinition: this.caseDefinition
+    }
+    this._case = markQualifyingCaseAsComplete(markQualifyingEventsAsComplete(caseInfo)).caseInstance
+  }
+
+  get case():Case {
+    return this._case
+  }
+
+  caseEvent:CaseEvent
+  caseEventDefinition:CaseEventDefinition
+  eventForm:EventForm
+  eventFormDefinition:EventFormDefinition
+  participant:CaseParticipant
+
+  setContext(caseEventId = '', eventFormId = '') {
+    window['caseInstance'] = this.case
+    this.caseEvent = caseEventId
+      ? this.case
+        .events
+        .find(caseEvent => caseEvent.id === caseEventId)
+      : null
+    window['caseEvent'] = this.caseEvent
+    this.caseEventDefinition = caseEventId
+      ? this
+        .caseDefinition
+        .eventDefinitions
+        .find(caseEventDefinition => caseEventDefinition.id === this.caseEvent.caseEventDefinitionId)
+      : null
+    window['caseEventDefinition'] = this.caseEventDefinition
+    this.eventForm = eventFormId
+      ? this.caseEvent
+        .eventForms
+        .find(eventForm => eventForm.id === eventFormId)
+      : null
+    window['eventForm'] = this.eventForm
+    this.eventFormDefinition = eventFormId
+      ? this.caseEventDefinition
+        .eventFormDefinitions
+        .find(eventFormDefinition => eventFormDefinition.id === this.eventForm.eventFormDefinitionId)
+      : null
+    this.participant = this.eventForm
+      ? this.case.participants.find(participant => participant.id === this.eventForm.participantId)
+      : null
+    window['participant'] = this.participant
+  }
+
+  getCurrentCaseEventId() {
+    return this?.caseEvent?.id
+  }
+  
+  getCurrentEventFormId() {
+    return this?.eventForm?.id
+  }
+
+  constructor(
+    private tangyFormService: TangyFormService,
+    private caseDefinitionsService: CaseDefinitionsService,
+    private userService:UserService,
+    private appConfigService:AppConfigService,
+    private http:HttpClient
+  ) {
+    this.queryCaseEventDefinitionId = 'query-event';
+    this.queryEventFormDefinitionId = 'query-form-event';
+    this.queryFormId = 'query-form';
+  }
+
+  /*
+   * Case API
+   */
+
+  get id() {
+    return this._case._id
+  }
+
+  get participants() {
+    return this._case.participants || []
+  }
+
+  get events() {
+    return this._case.events || []
+  }
+
+  get forms() {
+    return this._case.events.reduce((forms, event) => {
+      return [
+        ...event.eventForms || [],
+        ...forms
+      ]
+    }, [])
+  }
+
+  get groupId() {
+    return this._case.groupId
+  }
+
+  get roleDefinitions() {
+    return this.caseDefinition.caseRoles
+  }
+
+  get caseEventDefinitions() {
+    return this.caseDefinition.eventDefinitions || []
+  }
+
+  get eventFormDefinitions() {
+    return this.caseDefinition.eventDefinitions.reduce((formDefs, eventDef) => {
+      return [
+        ...eventDef.eventFormDefinitions.map(eventFormDef => {
+          return {
+            ...eventFormDef,
+            eventDefinitionId: eventDef.id
+          }
+        }) || [],
+        ...formDefs
+      ]
+    }, [])
+  }
+
+  async create(caseDefinitionId) {
+    this.caseDefinition = <CaseDefinition>(await this.caseDefinitionsService.load())
+      .find(caseDefinition => caseDefinition.id === caseDefinitionId)
+    this.case = new Case({caseDefinitionId, events: [], _id: UUID()})
+    delete this.case._rev
+    const tangyFormContainerEl:any = document.createElement('div')
+    tangyFormContainerEl.innerHTML = await this.tangyFormService.getFormMarkup(this.caseDefinition.formId, null)
+    const tangyFormEl = tangyFormContainerEl.querySelector('tangy-form')
+    tangyFormEl.style.display = 'none'
+    document.body.appendChild(tangyFormContainerEl)
+    this.case.form = tangyFormEl.getProps()
+    this.case.items = []
+    tangyFormEl.querySelectorAll('tangy-form-item').forEach((item) => {
+      this.case.items.push(item.getProps())
+    })
+    tangyFormEl.querySelectorAll('tangy-form-item')[0].submit()
+    this.case.items[0].inputs = tangyFormEl.querySelectorAll('tangy-form-item')[0].inputs
+    tangyFormEl.response = this.case
+    this.case = {...this.case, ...tangyFormEl.response}
+    tangyFormContainerEl.remove()
+    await this.setCase(this.case)
+    this.case.caseDefinitionId = caseDefinitionId;
+    this.case.label = this.caseDefinition.name
+    await this.save()
+  }
+
+  async archive() {
+    const eventForms:Array<EventForm> = this.case.events.reduce((eventForms, event) => {
+      return Array.isArray(event.eventForms)
+        ? [...eventForms, ...event.eventForms]
+        : eventForms
+    }, [])
+    for (let eventForm of eventForms) {
+      if (eventForm.formResponseId) {
+        const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+        if (formResponse) {
+          formResponse.archived = true
+          await this.tangyFormService.saveResponse(formResponse)
+        }
+      }
+    }
+    this.case.archived=true
+    await this.save() 
+  }
+
+  async unarchive() {
+    const eventForms:Array<EventForm> = this.case.events.reduce((eventForms, event) => {
+      return Array.isArray(event.eventForms)
+        ? [...eventForms, ...event.eventForms]
+        : eventForms
+    }, [])
+    for (let eventForm of eventForms) {
+      if (eventForm.formResponseId) {
+        const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+        if (formResponse) {
+          formResponse.archived = false 
+          await this.tangyFormService.saveResponse(formResponse)
+        }
+      }
+    }
+    this.case.archived = false 
+    await this.save() 
+  }
+
+  async delete() {
+    const eventForms:Array<EventForm> = this.case.events.reduce((eventForms, event) => {
+      return Array.isArray(event.eventForms)
+        ? [...eventForms, ...event.eventForms]
+        : eventForms
+    }, [])
+    for (let eventForm of eventForms) {
+      if (eventForm.formResponseId) {
+        const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+        if (formResponse) {
+          const archivedFormResponse = new TangyFormResponseModel(
+            {
+              archived:true,
+              _rev : formResponse._rev,
+              _id : formResponse._id,
+              form : {
+                id: formResponse.form.id,
+                title: formResponse.form.title,
+                tagName: formResponse.form.tagName,
+                complete: formResponse.form.complete
+              },
+              items : [],
+              events : [],
+              location : formResponse.location,
+              type : "response",
+              caseId: formResponse.caseId,
+              eventId: formResponse.eventId,
+              eventFormId: formResponse.eventFormId,
+              participantId: formResponse.participantId,
+              groupId: formResponse.groupId,
+              complete: formResponse.complete,
+              tangerineModifiedOn: new Date().getTime()
+            }
+          )
+          await this.tangyFormService.saveResponse(archivedFormResponse)
+        }
+      }
+    }
+
+    this.case.archived=true
+    // Keeping inputs so that the case show up in searches *on the server*
+    const archivedCase = new Case(
+      {
+        archived:true,
+        _rev : this.case._rev,
+        _id : this.case._id,
+        form : {
+          id: this.case.form.id,
+          tagName: this.case.form.tagName,
+          complete: this.case.form.complete
+        },
+        items : [{}],
+        events : [],
+        location : this.case.location,
+        type : "case",
+        caseDefinitionId: this.case.caseDefinitionId,
+        groupId: this.case['groupId'],
+        complete: this.case.complete,
+        tangerineModifiedOn: new Date().getTime()
+      }
+    )
+    if (this.case.items[0]) {
+      archivedCase.items[0].id = this.case.items[0].id
+      archivedCase.items[0].title = this.case.items[0].title
+      archivedCase.items[0].tagName = this.case.items[0].tagName
+      archivedCase.items[0].inputs = this.case.items[0].inputs
+    }
+    await this.tangyFormService.saveResponse(archivedCase)
+  }
+  
+  async setCase(caseInstance) {
+    // Note the order of setting caseDefinition before case matters because the setter for case expects caseDefinition to be the current one.
+    this.caseDefinition = (await this.caseDefinitionsService.load())
+      .find(caseDefinition => caseDefinition.id === caseInstance.caseDefinitionId)
+    const flatLocationList = await this.appConfigService.getFlatLocationList()
+    this.location = Object.keys(caseInstance.location).map(level => flatLocationList.locations.find(node => node.id === caseInstance.location[level]))
+    this.case = caseInstance
+  }
+
+  async load(id:string) {
+    await this.setCase(new Case(await this.tangyFormService.getResponse(id)))
+    this._shouldSave = true
+  }
+
+  async loadInMemory(caseData:Case) {
+    await this.setCase(new Case(caseData))
+    this._shouldSave = false
+  }
+
+  onChangeLocation$ = new Subject()
+
+  // @Param location: Can be Object where keys are levels and values are IDs of locations or an Array from the Tangy Location input's value.
+  async changeLocation(location:any):Promise<void> {
+    location = Array.isArray(location)
+      ? location.reduce((location, level) => { return {...location, [level.level]: level.value} }, {})
+      : location
+    this.case.location = location
+    const eventForms:Array<EventForm> = this.case.events
+      .reduce((eventForms, event) => {
+        return [...eventForms, ...event.eventForms]
+      }, [])
+    for (let eventForm of eventForms) {
+      if (eventForm.formResponseId) {
+        const response = await this.tangyFormService.getResponse(eventForm.formResponseId)
+        response.location = location
+        await this.tangyFormService.saveResponse(response)
+      }
+    }
+    this.onChangeLocation$.next(location)
+  }
+
+  async getCaseDefinitionByID(id:string) {
+    return <CaseDefinition>await this.http.get(`./assets/${id}.json`)
+      .toPromise()
+  }
+
+  async save() {
+    if (this._shouldSave) {
+      await this.tangyFormService.saveResponse(this.case)
+      await this.setCase(await this.tangyFormService.getResponse(this.case._id))
+    }
+  }
+
+  setVariable(variableName, value) {
+    let input = this.case.items[0].inputs.find(input => input.name === variableName)
+    if (input) {
+      input.value = value
+    } else {
+      this.case.items[0].inputs.push({name: variableName, value: value})
+    }
+  }
+
+  getVariable(variableName) {
+    return this.case.items[0].inputs.find(input => input.name === variableName)
+      ? this.case.items[0].inputs.find(input => input.name === variableName).value
+      : undefined
+  }
+
+  /*
+   * Case Event API
+   */
+
+  createEvent(eventDefinitionId:string) {
+    const caseEventDefinition = this.caseDefinition
+      .eventDefinitions
+      .find(eventDefinition => eventDefinition.id === eventDefinitionId)
+    const caseEvent = <CaseEvent>{
+      id: UUID(),
+      caseId: this.case._id,
+      name: caseEventDefinition.name,
+      complete: false,
+      estimate: true,
+      caseEventDefinitionId: eventDefinitionId,
+      windowEndDay: undefined,
+      windowStartDay: undefined,
+      estimatedDay: undefined,
+      occurredOnDay: undefined,
+      scheduledDay: undefined,
+      eventForms: [],
+      startDate: 0,
+      archived: false
+    }
+    this.case.events.push(caseEvent)
+    for (const caseParticipant of this.case.participants) {
+      for (const eventFormDefinition of caseEventDefinition.eventFormDefinitions) {
+        if (eventFormDefinition.forCaseRole.split(',').map(e=>e.trim()).includes(caseParticipant.caseRoleId)
+          &&
+          (
+            eventFormDefinition.autoPopulate || 
+            (eventFormDefinition.autoPopulate === undefined && eventFormDefinition.required === true)
+          )
+        ) {
+          this.createEventForm(caseEvent.id, eventFormDefinition.id, caseParticipant.id)
+        }
+      }
+    }
+
+    return caseEvent
+  }
+
+  async onCaseEventCreate(caseEvent: CaseEvent) {
+    const caseEventDefinition = this.caseDefinition
+    .eventDefinitions
+    .find(eventDefinition => eventDefinition.id === caseEvent.caseEventDefinitionId)
+
+    await eval(caseEventDefinition.onEventCreate)
+  }
+
+  setEventName(eventId, name:string) {
+    this.case.events = this.case.events.map(event => {
+      return event.id === eventId
+        ? { ...event, ...{ name } }
+        : event
+    })
+  }
+
+  setEventEstimatedDay(eventId, timeInMs: number) {
+    const estimatedDay = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    this.case.events = this.case.events.map(event => {
+      return event.id === eventId
+        ? { ...event, ...{ estimatedDay } }
+        : event
+    })
+  }
+  setEventScheduledDay(eventId, timeInMs: number) {
+    const scheduledDay = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    this.case.events = this.case.events.map(event => {
+      return event.id === eventId
+        ? { ...event, ...{ scheduledDay } }
+        : event
+    })
+  }
+  setEventWindow(eventId: string, windowStartDayTimeInMs: number, windowEndDayTimeInMs: number) {
+    const windowStartDay = moment((new Date(windowStartDayTimeInMs))).format('YYYY-MM-DD')
+    const windowEndDay = moment((new Date(windowEndDayTimeInMs))).format('YYYY-MM-DD')
+    this.case.events = this.case.events.map(event => {
+      return event.id === eventId
+        ? { ...event, ...{ windowStartDay, windowEndDay } }
+        : event
+    })
+  }
+  setEventOccurredOn(eventId, timeInMs: number) {
+    const occurredOnDay = moment((new Date(timeInMs))).format('YYYY-MM-DD')
+    return this.case.events = this.case.events.map(event => {
+      return event.id === eventId
+        ? { ...event, ...{ occurredOnDay } }
+        : event
+    })
+  }
+
+  disableEventDefinition(eventDefinitionId) {
+    if (this.case.disabledEventDefinitionIds.indexOf(eventDefinitionId) === -1) {
+      this.case.disabledEventDefinitionIds.push(eventDefinitionId)
+    }
+  }
+
+  activateCaseEvent(caseEventId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => {
+        return event.id === caseEventId
+          ? {
+            ...event,
+            inactive: false
+          }
+          : event
+      })
+    }
+  }
+
+  deactivateCaseEvent(caseEventId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => {
+        return event.id === caseEventId
+          ? {
+            ...event,
+            inactive: true
+          }
+          : event
+      })
+    }
+  }
+
+  async archiveCaseEvent(caseEventId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent && !caseEvent.archived) {
+      var unarchivedEventForms = caseEvent.eventForms.filter(form => !form.archived)
+      for (var eventForm of unarchivedEventForms) {
+        if (eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse) {
+            formResponse.archived = true
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+        }
+        eventForm.archived = true
+      }
+      caseEvent.archived = true
+      await this.save()
+    }
+  }
+
+  async unarchiveCaseEvent(caseEventId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent && caseEvent.archived) {
+      var archivedEventForms = caseEvent.eventForms.filter(form => form.archived)
+      for (var eventForm of archivedEventForms) {
+        if (eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse) {
+            formResponse.archived = false
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+        }
+        eventForm.archived = false
+      }
+      caseEvent.archived = false
+      await this.save()
+    }
+  }
+
+  /*
+   * Event Form API
+   */
+
+  createEventForm(caseEventId, eventFormDefinitionId, participantId = ''): EventForm {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    const eventFormId = UUID()
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id === caseEventId
+        ? {
+            ...event,
+            eventForms: [
+              ...event.eventForms,
+              <EventForm>{
+                id: eventFormId,
+                complete: false,
+                required: this
+                  .caseDefinition
+                  .eventDefinitions
+                  .find(eventDefinition => eventDefinition.id === caseEvent.caseEventDefinitionId)
+                  .eventFormDefinitions
+                  .find(eventFormDefinition => eventFormDefinition.id === eventFormDefinitionId)
+                  .required,
+                caseId: this.case._id,
+                participantId,
+                caseEventId,
+                eventFormDefinitionId
+              }
+            ]
+        }
+        : event
+      )
+    }
+    return this.case
+      .events
+      .find(event => event.id === caseEvent.id)
+      .eventForms
+      .find(eventForm => eventForm.id === eventFormId)
+  }
+
+  // @TODO Deprecated.
+  startEventForm(caseEventId, eventFormDefinitionId, participantId = ''): EventForm {
+    console.warn('caseService.startEventForm(...) is deprecated. Please use caseService.createEventForm(...) before startEventForm is removed.')
+    return this.createEventForm(caseEventId, eventFormDefinitionId, participantId)
+  }
+
+  deleteEventForm(caseEventId: string, eventFormId: string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => {
+        return {
+          ...event,
+          eventForms: event.id === caseEventId
+            ? event.eventForms.filter(eventForm => eventForm.id !== eventFormId)
+            : event.eventForms
+        }
+      })
+    }
+  }
+
+  setEventFormData(caseEventId:string,eventFormId:string, key:string, value:string) {
+    const index = this
+    .case
+    .events
+    .find(caseEvent => caseEvent.id === caseEventId)
+    .eventForms.findIndex(eventForm => eventForm.id === eventFormId);
+    this
+    .case
+    .events
+    .find(caseEvent => caseEvent.id === caseEventId).eventForms[index].data = {
+      ...this
+      .case
+      .events
+      .find(caseEvent => caseEvent.id === caseEventId).eventForms[index].data, [key]:value};
+  }
+
+  getEventFormData(caseEventId:string,eventFormId:string, key:string,) {
+    const index = this
+    .case
+    .events
+    .find(caseEvent => caseEvent.id === caseEventId)
+    .eventForms.findIndex(eventForm => eventForm.id === eventFormId);
+   return this
+    .case
+    .events
+    .find(caseEvent => caseEvent.id === caseEventId).eventForms[index].data[key] || ''
+  }
+
+  markEventFormRequired(caseEventId:string, eventFormId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id !== caseEventId
+        ? event
+        : {
+          ...event,
+          eventForms: event.eventForms.map(eventForm => eventForm.id !== eventFormId
+            ? eventForm
+            : {
+              ...eventForm,
+              required: true
+            }
+          )
+        }
+      )
+    }
+  }
+
+  markEventFormNotRequired(caseEventId:string, eventFormId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id !== caseEventId
+        ? event
+        : {
+          ...event,
+          eventForms: event.eventForms.map(eventForm => eventForm.id !== eventFormId
+            ? eventForm
+            : {
+              ...eventForm,
+              required: false
+            }
+          )
+        }
+      )
+    }
+  }
+
+  markEventFormComplete(caseEventId:string, eventFormId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id !== caseEventId
+        ? event
+        : {
+          ...event,
+          eventForms: event.eventForms.map(eventForm => eventForm.id !== eventFormId
+            ? eventForm
+            : {
+              ...eventForm,
+              complete: true
+            }
+          )
+        }
+      )
+    }
+  }
+
+  activateEventForm(caseEventId:string, eventFormId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id !== caseEventId
+        ? event
+        : {
+          ...event,
+          eventForms: event.eventForms.map(eventForm => eventForm.id !== eventFormId
+            ? eventForm
+            : {
+              ...eventForm,
+              inactive: false
+            }
+          )
+        }
+      )
+    }
+  }
+
+  deactivateEventForm(caseEventId:string, eventFormId:string) {
+    this.case = {
+      ...this.case,
+      events: this.case.events.map(event => event.id !== caseEventId
+        ? event
+        : {
+          ...event,
+          eventForms: event.eventForms.map(eventForm => eventForm.id !== eventFormId
+            ? eventForm
+            : {
+              ...eventForm,
+              inactive: true
+            }
+          )
+        }
+      )
+    }
+  }
+
+  async archiveEventForm(caseEventId:string, eventFormId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent) {
+      var eventForm = caseEvent.eventForms.find(form => form.id === eventFormId)
+      if (eventForm) {
+        if (eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse && !formResponse.archived) {
+            formResponse.archived = true
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+        }
+        eventForm.archived = true
+        await this.save()
+      }
+    }
+  }
+
+  async unarchiveEventForm(caseEventId:string, eventFormId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent) {
+      var eventForm = caseEvent.eventForms.find(form => form.id === eventFormId)
+      if (eventForm) {
+        if (eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse && formResponse.archived) {
+            formResponse.archived = false
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+        }
+        eventForm.archived = false
+        await this.save()
+      }
+    }
+  }
+
+  /*
+   * Form Response API
+   */
+
+  async archiveFormResponse(caseEventId:string, eventFormId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent) {
+      var eventForm = caseEvent.eventForms.find(form => form.id === eventFormId)
+      if (eventForm && eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse) {
+            formResponse.archived = true
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+          eventForm.archived = true
+          await this.save()
+        }
+    }
+  }
+
+  async unarchiveFormResponse(caseEventId:string, eventFormId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent) {
+      var eventForm = caseEvent.eventForms.find(form => form.id === eventFormId)
+      if (eventForm && eventForm.formResponseId) {
+          const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+          if (formResponse) {
+            formResponse.archived = false
+            await this.tangyFormService.saveResponse(formResponse)
+          }
+          eventForm.archived = false
+          await this.save()
+        }
+    }
+  }
+
+  async deleteFormResponse(caseEventId:string, eventFormId:string) {
+    const caseEvent = this.case.events.find(event => event.id === caseEventId)
+    if (caseEvent) {
+      var eventForm = caseEvent.eventForms.find(form => form.id === eventFormId)
+      if (eventForm && eventForm.formResponseId) {
+        const formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId)
+        if (formResponse) {
+          const archivedFormResponse = new TangyFormResponseModel(
+            {
+              archived:true,
+              _rev : formResponse._rev,
+              _id : formResponse._id,
+              form : {
+                id: formResponse.form.id,
+                title: formResponse.form.title,
+                tagName: formResponse.form.tagName,
+                complete: formResponse.form.complete
+              },
+              items : [],
+              events : [],
+              location : formResponse.location,
+              type : "response",
+              caseId: formResponse.caseId,
+              eventId: formResponse.eventId,
+              eventFormId: formResponse.eventFormId,
+              participantId: formResponse.participantId,
+              groupId: formResponse.groupId,
+              complete: formResponse.complete,
+              tangerineModifiedOn: new Date().getTime()
+            }
+          )
+          await this.tangyFormService.saveResponse(archivedFormResponse)
+        }
+        this.deleteEventForm(caseEventId, eventFormId)
+        await this.save()
+      }
+    }
+  }
+
+  /*
+   * Participant API
+   */
+
+  createParticipant(caseRoleId = ''):CaseParticipant {
+    const id = UUID()
+    const data = {}
+    const caseParticipant = <CaseParticipant>{
+      id,
+      caseRoleId,
+      data
+    }
+    this.case.participants.push(caseParticipant)
+    for (let caseEvent of this.case.events) {
+      const caseEventDefinition = this
+        .caseDefinition
+        .eventDefinitions
+        .find(eventDefinition => eventDefinition.id === caseEvent.caseEventDefinitionId)
+      for (let eventFormDefinition of caseEventDefinition.eventFormDefinitions) {
+        if (eventFormDefinition.forCaseRole.split(',').map(e=>e.trim()).includes(caseRoleId)
+          &&
+          (
+            eventFormDefinition.autoPopulate || 
+            (eventFormDefinition.autoPopulate === undefined && eventFormDefinition.required === true)
+          )
+        ) {
+          this.createEventForm(caseEvent.id, eventFormDefinition.id, caseParticipant.id)
+        }
+      }
+    }
+    return caseParticipant
+  }
+
+  setParticipantData(participantId:string, key:string, value:string) {
+    const index = this.case.participants.findIndex(participant => participant.id === participantId)
+    this.case.participants[index].data[key] = value
+  }
+
+  getParticipantData(participantId:string, key:string) {
+    return this.case.participants.find(participant => participant.id === participantId).data[key]
+  }
+
+  addParticipant(caseParticipant:CaseParticipant) {
+    this.case.participants.push(caseParticipant)
+    for (let caseEvent of this.case.events) {
+      const caseEventDefinition = this
+        .caseDefinition
+        .eventDefinitions
+        .find(eventDefinition => eventDefinition.id === caseEvent.caseEventDefinitionId)
+      for (let eventFormDefinition of caseEventDefinition.eventFormDefinitions) {
+        if (
+          eventFormDefinition.forCaseRole.split(',').map(e=>e.trim()).includes(caseParticipant.caseRoleId)
+          && 
+          (
+            eventFormDefinition.autoPopulate || 
+            (eventFormDefinition.autoPopulate === undefined && eventFormDefinition.required === true)
+          )
+        ) {
+          this.createEventForm(caseEvent.id, eventFormDefinition.id, caseParticipant.id)
+        }
+      }
+    }
+  }
+
+  async activateParticipant(participantId:string) {
+    this.case = {
+      ...this.case,
+      participants: this.case.participants.map(participant => {
+        return participant.id === participantId
+          ? {
+            ...participant,
+            inactive: false 
+          }
+          : participant
+      })
+    }
+  }
+
+  async deactivateParticipant(participantId:string) {
+    this.case = {
+      ...this.case,
+      participants: this.case.participants.map(participant => {
+        return participant.id === participantId
+          ? {
+            ...participant,
+            inactive: true 
+          }
+          : participant
+      })
+    }
+  }
+
+
+  async getParticipantFromAnotherCase(sourceCaseId, sourceParticipantId) {
+    const currCaseId = this.case._id
+
+    await this.load(sourceCaseId)
+    const sourceCase = this.case
+    const sourceParticipant = sourceCase.participants.find(sourceParticipant =>
+      sourceParticipant.id === sourceParticipantId)
+      
+    await this.load(currCaseId)
+
+    return sourceParticipant
+  }
+
+  async deleteParticipantInAnotherCase(sourceCaseId, sourceParticipantId) {
+    const currCaseId = this.case._id
+
+    await this.load(sourceCaseId)
+    this.case.participants = this.case.participants.filter(sourceParticipant =>
+        sourceParticipant.id === sourceParticipantId)
+    await this.save()
+
+    await this.load(currCaseId)
+  }
+
+  async copyParticipantFromAnotherCase(sourceCaseId, sourceParticipantId) {
+    const caseParticipant = await this.getParticipantFromAnotherCase(sourceCaseId, sourceParticipantId)
+    if (caseParticipant !== undefined) {
+      this.addParticipant(caseParticipant)
+    }
+  }
+
+  async moveParticipantFromAnotherCase(sourceCaseId, sourceParticipantId) {
+    const caseParticipant = await this.getParticipantFromAnotherCase(sourceCaseId, sourceParticipantId)
+    if (caseParticipant !== undefined) {
+      this.addParticipant(caseParticipant)
+
+      // Only delete the participant from the other case after adding it to this case is successful
+      await this.deleteParticipantInAnotherCase(sourceCaseId, sourceParticipantId)
+    }
+  }
+
+  async searchForParticipant(username:string, phrase:string, limit = 50, skip = 0, unique = true):Promise<Array<any>> {
+    const db = await window['T'].user.getUserDatabase(username)
+    const result = await db.query(
+      'participantSearch',
+      phrase
+        ? { 
+          startkey: `${phrase}`.toLocaleLowerCase(),
+          endkey: `${phrase}\uffff`.toLocaleLowerCase(),
+          include_docs: true,
+          limit,
+          skip
+        }
+        : {
+          include_docs: true,
+          limit,
+          skip
+        } 
+    )
+    const searchResults = result.rows.map(row => {
+      return {
+        ...row.value,
+        case: row.doc,
+        participant: row.doc.participants.find(p => p.id === row.value.participantId)
+      }
+    })
+    // Deduplicate the search results since the same case may match on multiple variables.
+    return unique
+      ? searchResults.reduce((uniqueResults, result) => {
+        return uniqueResults.find(x => x.participantId === result.participantId)
+          ? uniqueResults
+          : [ ...uniqueResults, result ]
+      }, [])
+      : searchResults
+  }
+
+  /*
+   * Notification API
+   */
+
+  createNotification (label = '', description = '', link = '', icon = 'notification_important', color = '#CCC', persist = false, enforceAttention = false ) {
+    const notification = <Notification>{
+      id: UUID(),
+      status: NotificationStatus.Open,
+      createdAppContext: AppContext.Client,
+      createdOn: Date.now(),
+      label,
+      description,
+      link,
+      icon,
+      color,
+      enforceAttention,
+      persist
+   }
+    this.case.notifications.push(notification)
+  }
+
+  async openNotification(notificationId:string) {
+    this.case.notifications = this.case.notifications.map(notification => {
+      return notification.id === notificationId
+        ? <Notification>{
+          ...notification,
+          status: NotificationStatus.Open
+        }
+        : notification
+    })
+  }
+
+  async closeNotification(notificationId:string) {
+    this.case.notifications = this.case.notifications.map(notification => {
+      return notification.id === notificationId
+        ? <Notification>{
+          ...notification,
+          status: NotificationStatus.Closed
+        }
+        : notification
+    })
+  }
+}
+
+interface CaseInfo {
+  caseInstance: Case
+  caseDefinition:CaseDefinition
+}
+
+// @TODO We should revisit this logic. Not sure it's what we want.
+export const markQualifyingCaseAsComplete = ({caseInstance, caseDefinition}:CaseInfo):CaseInfo => {
+  // Check to see if all required Events are complete in Case. If so, mark Case complete.
+  let numberOfCaseEventsRequired = caseDefinition
+    .eventDefinitions
+    .reduce((acc, definition) => definition.required ? acc + 1 : acc, 0)
+  let numberOfUniqueCompleteCaseEvents = caseInstance
+    .events
+    .reduce((acc, instance) => instance.complete === true
+        ? Array.from(new Set([...acc, instance.caseEventDefinitionId]))
+        : acc
+      , [])
+      .length
+  caseInstance
+    .complete = numberOfCaseEventsRequired === numberOfUniqueCompleteCaseEvents ? true : false
+  return { caseInstance, caseDefinition }
+}
+
+export const markQualifyingEventsAsComplete = ({caseInstance, caseDefinition}:CaseInfo):CaseInfo => {
+  return {
+    caseInstance: {
+      ...caseInstance,
+      events: caseInstance.events.map(event => {
+        return {
+          ...event,
+          complete: !caseDefinition
+            .eventDefinitions
+            .find(eventDefinition => eventDefinition.id === event.caseEventDefinitionId)
+            .eventFormDefinitions
+            .some(eventFormDefinition => {
+              // 1. Is required and has no Event Form instances.
+              return (
+                  eventFormDefinition.required === true &&
+                  !event.eventForms.some(eventForm => eventForm.eventFormDefinitionId === eventFormDefinition.id)
+                ) ||
+                // 2. Is required and at least one Event Form instance is not complete.
+                (
+                  eventFormDefinition.required === true &&
+                  event.eventForms
+                    .filter(eventForm => eventForm.eventFormDefinitionId === eventFormDefinition.id)
+                    .some(eventForm => !eventForm.complete)
+                ) ||
+                // 3. Is not required and has at least one Event Form instance that is both incomplete and required.
+                (
+                  eventFormDefinition.required === false &&
+                  event.eventForms
+                    .filter(eventForm => eventForm.eventFormDefinitionId === eventFormDefinition.id)
+                    .some(eventForm => !eventForm.complete && eventForm.required)
+                )
+            })
+        }
+      })
+    },
+    caseDefinition
+  }
+}
+
+export { CaseService };
diff --git a/online-survey-app/src/app/case/services/cases.service.spec.ts b/online-survey-app/src/app/case/services/cases.service.spec.ts
new file mode 100644
index 0000000000..1ccaee9023
--- /dev/null
+++ b/online-survey-app/src/app/case/services/cases.service.spec.ts
@@ -0,0 +1,264 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CasesService } from './cases.service';
+import { UserService } from 'src/app/shared/_services/user.service';
+import { HttpClientModule } from '@angular/common/http';
+import { CaseEventInfo } from './case-event-info.class';
+import { CASE_EVENT_STATUS_IN_PROGRESS } from '../classes/case-event.class';
+
+class MockPouchDB {
+  allDocs(options) {
+    return {
+      rows: [
+        {
+          _id: 'doc1',
+          doc: {
+            _id: 'doc1',
+            collection: 'TangyFormResponse',
+            type: 'case',
+            events: [
+              // Estimated lower bound overlap.
+              {
+                id: 'e1',
+                estimate: true,
+                dateStart: 1,
+                dateEnd: 10
+              },
+              // Estimated in bounds.
+              {
+                id: 'e2',
+                estimate: true,
+                dateStart: 9,
+                dateEnd: 11
+              },
+              // Estimated upper bounds overlap.
+              {
+                id: 'e3',
+                estimate: true,
+                dateStart: 10,
+                dateEnd: 20
+              },
+              // Estimated full overlap.
+              {
+                id: 'e4',
+                estimate: true,
+                dateStart: 1,
+                dateEnd: 20
+              },
+              // Estimated out of bounds.
+              {
+                id: 'e5',
+                estimate: true,
+                dateStart: 20,
+                dateEnd: 30
+              },
+              // Scheduled in bounds.
+              {
+                id: 'e6',
+                estimate: false,
+                dateStart: 10,
+                dateEnd: 10 
+              },
+              // Scheduled out of bounds.
+              {
+                id: 'e7',
+                estimate: false,
+                dateStart: 30,
+                dateEnd: 30 
+              },
+            ]
+          }
+        }
+      ]
+    }
+  }
+}
+
+class MockUserService {
+  getCurrentUser() {
+    return 'test'
+  }
+  getUserDatabase(username) {
+    return new MockPouchDB()
+  }
+}
+
+const REFERENCE_TIME = '2019-08-13'
+const REFERENCE_TIME_2 = '2019-12-31'
+
+class MockCasesService {
+  
+  async getEventsByDate(username: string, dateStart, dateEnd, excludeEstimates = false): Promise<Array<CaseEventInfo>> {
+    const caseDefinition= {
+      'id': 'c1',
+      'formId': 'mother-manifest-3a453b',
+      'name': 'Pregnant Woman',
+      'description': 'Select this case type to screen, consent and enroll a pregnant womans.',
+      'templateCaseTitle': '${getVariable("participant_id") ? `${getVariable("firstname")} ${getVariable("surname")}` : "..."}',
+      'templateCaseDescription': '${getVariable("participant_id") ? `<b>Participant ID</b>: ${getVariable("participant_id")} <b>Village</b>: ${getVariable("village")} <b>Head of household</b>: ${getVariable("headofhouse")}` : "..."}',
+      'templateScheduleListItemIcon': '${caseEventInfo.complete ? "event_available" : "event_note"}',
+      'templateScheduleListItemPrimary': '<span>${caseEventDefinition.name}</span>',
+      'templateScheduleListItemSecondary': '<span>${caseInstance.label}</span>',
+    
+      'templateCaseEventListItemIcon': '${caseEventInfo.complete ? "event_available" : "event_note"}',
+      'templateCaseEventListItemPrimary': '<span>${caseEventDefinition.name}</span>',
+      'templateCaseEventListItemSecondary': '${TRANSLATE("Scheduled")}: ${formatDate(caseEventInfo.dateStart,"dddd, MMMM Do YYYY, h:mm:ss a")}',
+      'templateEventFormListItemIcon': '',
+      'templateEventFormListItemPrimary': '<span>${eventFormDefinition.name}</span>',
+      'templateEventFormListItemSecondary': 'Workflow: ${getValue("workflow_state")} Status: ${!eventForm.complete ? "Incomplete" : "Complete"}',
+      'startFormOnOpen': {
+        'eventId': 'c1',
+        'eventFormId': 'event-form-definition-ece26e'
+      },
+      'eventDefinitions': [
+        {
+          'id': 'c1',
+          'name': 'ANC-Enrollment',
+          'description': '',
+          'repeatable': false,
+          'estimatedTimeFromCaseOpening': 0,
+          'estimatedTimeWindow': 0,
+          'required': true,
+          'eventFormDefinitions': [
+            {
+              'id': 'event-form-definition-ece26e',
+              'formId': 's0-participant-information-f254b9',
+              'name': 'S01 - Screening and Enrollment',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-574497',
+              'formId': 's02-drug-administration-99380a',
+              'name': 'S02 - AZ dosing',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-c94289',
+              'formId': 's03-demographic-characteristics-c09a84',
+              'name': 'S03 - Demographic information',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-a5301b',
+              'formId': 's04-current-pregnancy-record-and-pregnancy-history-9858d6',
+              'name': 'S04 - Maternal Clinical history',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-c58ef3',
+              'formId': 's05-maternal-physical-exam-1c983b',
+              'name': 'S05 - Physical Exam',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-z830kj',
+              'formId': 's06-current-anc-visit-68fc78',
+              'name': 'S06 - Current ANC visit',
+              'required': true,
+              'repeatable': false
+            },
+            {
+              'id': 'event-form-definition-z452kj',
+              'formId': 's07-visit-information-82328c',
+              'name': 'S07 - Visit Close Out',
+              'required': true,
+              'repeatable': false
+            }
+          ]
+        },
+      ]
+    }
+    return [
+      <CaseEventInfo>{
+        id: 'e1',
+        caseId: 'response1',
+        caseEventDefinitionId: 'c1',
+        status: CASE_EVENT_STATUS_IN_PROGRESS,
+        eventForms: [],
+        estimate: false,
+        scheduledDay: REFERENCE_TIME,
+        occurredOnDay: REFERENCE_TIME,
+        estimatedDay: REFERENCE_TIME,
+        windowStartDay: REFERENCE_TIME,
+        windowEndDay: REFERENCE_TIME_2,
+        caseDefinition,
+        caseInstance: {
+          caseDefinitionId: 'c1',
+          type: 'case',
+          label: 'Pregnant Woman',
+          collection: 'TangyFormResponse',
+          events:<any> [
+            {
+              'id': '0ff22322-7734-4cc1-957d-dd25416a9413',
+              'caseId': 'ff621529-d26b-42cc-a9a1-254179e75622',
+              'status': 'in-progress',
+              'name': 'ANC-Enrollment',
+              'estimate': true,
+              'caseEventDefinitionId': 'c1',
+              'scheduledDay': REFERENCE_TIME,
+              'occurredOnDay': REFERENCE_TIME,
+              'estimatedDay': REFERENCE_TIME,
+              'windowStartDay': REFERENCE_TIME,
+              'windowEndDay': REFERENCE_TIME_2,
+              'eventForms': [
+                {
+                  'id': 'fb64d705-ee99-4a27-bbad-8349a0e8767d',
+                  'complete': false,
+                  'caseId': 'ff621529-d26b-42cc-a9a1-254179e75622',
+                  'caseEventId': '0ff22322-7734-4cc1-957d-dd25416a9413',
+                  'eventFormDefinitionId': 'event-form-definition-ece26e',
+                  'formResponseId': '487517f7-0bec-4363-b22c-0c20090fe284'
+                },
+                {
+                  'id': '83c502a2-8c82-4ed8-ad05-e8263cd56a0b',
+                  'complete': false,
+                  'caseId': 'ff621529-d26b-42cc-a9a1-254179e75622',
+                  'caseEventId': '0ff22322-7734-4cc1-957d-dd25416a9413',
+                  'eventFormDefinitionId': 'event-form-definition-574497',
+                  'formResponseId': '3cd53c20-d213-4e8b-bf90-c7091bf95ad6'
+                },
+              ],
+              'startDate': 0
+            }
+          ]
+        }
+      }
+]}
+}
+
+describe('CasesService', () => {
+
+  beforeEach(() => TestBed.configureTestingModule({
+    imports:[HttpClientModule],
+    providers: [
+      {provide:CasesService, useClass:MockCasesService},
+      {
+        provide: UserService,
+        useClass: MockUserService
+      }
+    ]
+  }));
+
+  it('should be created', () => {
+    const service:CasesService = TestBed.get(CasesService);
+    expect(service).toBeTruthy();
+  });
+
+  it('should be give events by date', async() => {
+    const service:CasesService = TestBed.get(CasesService);
+    const result = await service.getEventsByDate(5, 15, false)
+    expect(result.length).toEqual(1)
+  })
+
+  it('should be give events by date with estimates excluded', async() => {
+    const service:CasesService = TestBed.get(CasesService);
+    const result = await service.getEventsByDate(5, 15, true)
+    expect(result.length).toEqual(1)
+  })
+
+});
diff --git a/online-survey-app/src/app/case/services/cases.service.ts b/online-survey-app/src/app/case/services/cases.service.ts
new file mode 100644
index 0000000000..3286d49955
--- /dev/null
+++ b/online-survey-app/src/app/case/services/cases.service.ts
@@ -0,0 +1,46 @@
+import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service';
+import { Injectable } from '@angular/core';
+import { CaseEventInfo } from './case-event-info.class';
+import { CaseService } from './case.service';
+import * as moment from 'moment';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CasesService {
+
+  constructor(
+    private caseService: CaseService,
+    private tangyFormService:TangyFormService
+  ) { }
+
+  async getEventsByDate(dateStart, dateEnd, excludeEstimates = false): Promise<Array<CaseEventInfo>> {
+    const allResponses = await this.tangyFormService.getAllResponses()
+    const docs = <Array<CaseEventInfo>>(allResponses)
+      .filter(doc => doc.collection === 'TangyFormResponse' && doc.type === 'case')
+      .reduce((acc, caseDoc) => [...acc, ...caseDoc.events
+        .map(event => {
+          return this.caseService.getCaseDefinitionByID(caseDoc.caseDefinitionId).then(caseDefinition => {
+            return { ...event, caseInstance: caseDoc, caseDefinition }
+          })
+        })], <Array<CaseEventInfo>>[]);
+    return Promise.all(docs).
+    then(data => data.filter(eventInfo => this.doesOverlap(dateStart, dateEnd, eventInfo) && !(excludeEstimates && eventInfo.estimate))
+      .sort(function (a, b) {
+        const dateA = new Date(a.occurredOnDay || a.scheduledDay || a.estimatedDay || a.windowStartDay).getTime()
+        const dateB = new Date(b.occurredOnDay || b.scheduledDay || b.estimatedDay || a.windowEndDay).getTime()
+        return dateA - dateB;
+      }))
+  }
+
+  private doesOverlap(dateStart, dateEnd, eventInfo: CaseEventInfo): boolean {
+    // Only show items on schedule view if the following dates are set on the event
+    if (!(eventInfo.scheduledDay || eventInfo.estimatedDay || eventInfo.windowStartDay)){
+      return false;
+    }
+    dateStart = moment(new Date(dateStart)).format('YYYY-MM-DD')
+    dateEnd = moment(new Date(dateEnd)).format('YYYY-MM-DD')
+    return moment(eventInfo.scheduledDay || eventInfo.estimatedDay || eventInfo.windowStartDay).isBetween(dateStart, dateEnd, 'days', '[]')
+    || moment(eventInfo.scheduledDay || eventInfo.estimatedDay ||eventInfo.windowEndDay).isBetween(dateStart, dateEnd, 'days', '[]')
+  }
+}
diff --git a/online-survey-app/src/app/shared/_services/app-config.service.ts b/online-survey-app/src/app/shared/_services/app-config.service.ts
index d74c53c1a9..5d11d28a1b 100644
--- a/online-survey-app/src/app/shared/_services/app-config.service.ts
+++ b/online-survey-app/src/app/shared/_services/app-config.service.ts
@@ -1,12 +1,35 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { AppConfig } from '../classes/app-config';
+import { Loc } from 'tangy-form/util/loc.js'
+
+export interface LocationNode {
+  id:string
+  level:string
+  label:string
+  data:any
+}
+
+export interface LocationList {
+  locations:any
+  locationsLevels:Array<string>
+  metadata:any
+}
+
+export interface FlatLocationList {
+  locations:Array<LocationNode>
+  locationsLevels:Array<string>
+  metadata:any
+}
 
 @Injectable({
   providedIn: 'root'
 })
 export class AppConfigService {
 
+  locationList:LocationList
+  flatLocationList:FlatLocationList
+
   constructor(private httpClient: HttpClient) { }
   async getAppConfig(): Promise<Partial<AppConfig>> {
     try {
@@ -21,4 +44,10 @@ export class AppConfigService {
   async getAppName(): Promise<string>{
     return (await this.getAppConfig()).appName;
   }
+
+  async getFlatLocationList() {
+    this.locationList = this.locationList || <LocationList>await this.httpClient.get('./assets/location-list.json').toPromise();
+    this.flatLocationList = this.flatLocationList || <FlatLocationList>Loc.flatten(JSON.parse(JSON.stringify(this.locationList)))
+    return this.flatLocationList
+  }
 }
diff --git a/online-survey-app/src/app/shared/classes/user-database.class.ts b/online-survey-app/src/app/shared/classes/user-database.class.ts
index 50b0d34932..38e681573c 100644
--- a/online-survey-app/src/app/shared/classes/user-database.class.ts
+++ b/online-survey-app/src/app/shared/classes/user-database.class.ts
@@ -1,8 +1,6 @@
-import {_TRANSLATE} from '../translation-marker';
-const SHARED_USER_DATABASE_NAME = 'shared-user-database';
-import PouchDB from 'pouchdb';
-import { DB } from '../_factories/db.factory';
-import * as jsonpatch from "fast-json-patch";
+import axios from 'axios'
+import * as jsonpatch from "fast-json-patch"
+
 
 export class UserDatabase {
 
@@ -13,65 +11,35 @@ export class UserDatabase {
   buildId:string;
   buildChannel:string;
   groupId:string;
-  db: PouchDB;
-  attachHistoryToDocs:boolean
+  attachHistoryToDocs:boolean = undefined
 
-  constructor(username: string, userId: string, key:string = '', deviceId: string, shared = false, buildId = '', buildChannel = '', groupId = '', attachHistoryToDocs = false) {
+  constructor(userId: string, groupId = '') {
     this.userId = userId
-    this.username = username
-    this.name = username
-    this.deviceId = deviceId
-    this.buildId = buildId
-    this.buildChannel = buildChannel
+    this.username = userId
+    this.name = userId
+    this.deviceId = 'SURVEY' 
+    this.buildId = 'SURVEY' 
+    this.buildChannel = 'SURVEY' 
     this.groupId = groupId 
-    this.attachHistoryToDocs = attachHistoryToDocs 
-    if (shared) {
-      this.db = DB(SHARED_USER_DATABASE_NAME, key)
-    } else {
-      this.db = DB(username, key)
-    }
   }
 
-  async synced(doc) {
-    return await this.db.put({
-      ...doc,
-      tangerineSyncedOn: Date.now()
-    });
-  }
-
-  async get(_id) {
-    const doc = await this.db.get(_id);
-    // @TODO Temporary workaround for CryptoPouch bug where it doesn't include the _rev when getting a doc.
-    if (this.db.cryptoPouchIsEnabled) {
-      const tmpDb = new PouchDB(this.db.name)
-      const encryptedDoc = await tmpDb.get(_id)
-      doc._rev = encryptedDoc._rev
-    }
-    return doc
+  async get(id) {
+    const token = localStorage.getItem('token');
+    return (<any>await axios.get(`/group-responses/read/${this.groupId}/${id}`, { headers: { authorization: token }})).data
   }
 
   async put(doc) {
-    const newDoc = {
-      ...doc,
-      tangerineModifiedByUserId: this.userId,
-      tangerineModifiedByDeviceId: this.deviceId,
-      tangerineModifiedOn: Date.now(),
-      buildId: this.buildId,
-      deviceId: this.deviceId,
-      groupId: this.groupId,
-      buildChannel: this.buildChannel,
-      // Backwards compatibility for sync protocol 1. 
-      lastModified: Date.now()
-    }
-    return await this.db.put({
-      ...newDoc,
-      ...this.attachHistoryToDocs
-        ? { history: await this._calculateHistory(newDoc) }
-        : { }
-    });
+    return await this.post(doc)
   }
 
   async post(doc) {
+    const token = localStorage.getItem('token');
+    if (this.attachHistoryToDocs === undefined) {
+      const appConfig = (<any>await axios.get('./assets/app-config.json', { headers: { authorization: token }})).data
+      this.attachHistoryToDocs = appConfig['attachHistoryToDocs']
+        ? true
+        : false
+    }
     const newDoc = {
       ...doc,
       tangerineModifiedByUserId: this.userId,
@@ -84,50 +52,32 @@ export class UserDatabase {
       // Backwards compatibility for sync protocol 1. 
       lastModified: Date.now()
     }
-    return await this.db.post({
-      ...newDoc,
-      ...this.attachHistoryToDocs
-        ? { history: await this._calculateHistory(newDoc) }
-        : { } 
-    });
-  }
-
-  remove(doc) {
-    return this.db.remove(doc);
-  }
-
-  query(queryName: string, options = {}) {
-    return this.db.query(queryName, options);
-  }
-
-  destroy() {
-    return this.db.destroy();
-  }
-
-  changes(options) {
-    return this.db.changes(options);
-  }
-
-  allDocs(options) {
-    return this.db.allDocs(options);
-  }
-
-  sync(remoteDb, options) {
-    return this.db.sync(remoteDb, options);
-  }
-
-  upsert(docId, callback) {
-    return this.db.upsert(docId, callback);
+    return (<any>await axios.post(`/group-responses/update/${this.groupId}`, {
+      response: {
+        ...newDoc,
+        ...this.attachHistoryToDocs
+          ? { history: await this._calculateHistory(newDoc) }
+          : { }
+      }
+    },
+    {
+      headers: {
+        authorization: token
+      }
+    }
+    )).data;
   }
 
-  compact() {
-    return this.db.compact();
+  async remove(doc) {
+    // This is not implemented...
+    const token = localStorage.getItem('token');
+    return await axios.delete(`/api/${this.groupId}`, doc)
   }
 
   async _calculateHistory(newDoc) {
     let history = []
     try {
-      const currentDoc = await this.db.get(newDoc._id)
+      const currentDoc = await this.get(newDoc._id)
       const entry = {
         lastRev: currentDoc._rev,
         patch: jsonpatch.compare(currentDoc, newDoc).filter(mod => mod.path.substr(0,8) !== '/history')
diff --git a/online-survey-app/src/app/tangy-forms/classes/form-info.class.ts b/online-survey-app/src/app/tangy-forms/classes/form-info.class.ts
new file mode 100644
index 0000000000..1574ef99c6
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/classes/form-info.class.ts
@@ -0,0 +1,63 @@
+import { AppContext } from './../../app-context.enum';
+export class FormInfo {
+  id:string
+  src:string
+  type:string
+  title:string
+  description:string = ''
+  listed = true
+  archived = false
+  templates: Array<FormTemplate> = []
+  searchSettings:FormSearchSettings =  <FormSearchSettings>{
+    shouldIndex: true,
+    variablesToIndex: [],
+    primaryTemplate: '',
+    secondaryTemplate: ''
+  }
+  customSyncSettings:CustomSyncSettings = <CustomSyncSettings>{
+    enabled: false,
+    push: false,
+    pull: false,
+    excludeIncomplete:false
+  }
+  couchdbSyncSettings:CouchdbSyncSettings = <CouchdbSyncSettings>{
+    enabled: false,
+    filterByLocation: false
+  }
+  formVersionId:string
+  formVersions:Array<FormVersion>
+}
+
+export interface FormVersion {
+  id:string
+  src:string
+}
+
+export interface FormTemplate {
+  id:string
+  src:string
+  label:string
+  appContext: AppContext
+}
+
+
+export interface CouchdbSyncSettings {
+  enabled: boolean
+  push: boolean
+  pull: boolean
+  filterByLocation:boolean
+}
+
+export interface CustomSyncSettings {
+  enabled: boolean
+  push:boolean
+  pull:boolean
+  excludeIncomplete:boolean
+}
+
+export interface FormSearchSettings {
+  shouldIndex:boolean
+  variablesToIndex:Array<string>
+  primaryTemplate:string
+  secondaryTemplate:string
+}
diff --git a/online-survey-app/src/app/tangy-forms/classes/form-version.class.ts b/online-survey-app/src/app/tangy-forms/classes/form-version.class.ts
new file mode 100644
index 0000000000..ad077deea5
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/classes/form-version.class.ts
@@ -0,0 +1,5 @@
+export class FormVersion {
+  id: string
+  src: string
+  legacyOriginal: boolean
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/tangy-forms/safe-url.pipe.ts b/online-survey-app/src/app/tangy-forms/safe-url.pipe.ts
new file mode 100755
index 0000000000..3a8240b219
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/safe-url.pipe.ts
@@ -0,0 +1,13 @@
+import { DomSanitizer } from '@angular/platform-browser';
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'safeUrl'
+})
+export class SafeUrlPipe implements PipeTransform {
+
+  constructor(private sanitizer: DomSanitizer) { }
+  transform(url): any {
+    return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+  }
+}
diff --git a/online-survey-app/src/app/tangy-forms/tangy-form-response.class.ts b/online-survey-app/src/app/tangy-forms/tangy-form-response.class.ts
new file mode 100644
index 0000000000..c5e319b2f4
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-form-response.class.ts
@@ -0,0 +1,43 @@
+export class TangyFormResponse {
+  _id:string;
+  _rev:string;
+  collection = 'TangyFormResponse'
+  complete = false
+  formType = 'form'
+  form:TangyForm;
+  items:Array<any>;
+
+  get variablesArray() {
+    // Reduce to an array.
+    return this.items.reduce((inputsArray, item) => {
+      item.inputs.forEach(input => {
+        if (input.tagName === 'TANGY-CARDS') {
+          input.value.forEach(card => card.value.forEach(input => inputsArray.push(input)))
+        } else {
+          inputsArray.push(input)
+        }
+      })
+      return inputsArray
+    }, [])
+  }
+
+  get variables() {
+    // Reduce to an object keyed on input.name. If multiple inputs with the same name, put them in an array.
+    return this.variablesArray.reduce((inputsObject, input) => {
+      if (inputsObject.hasOwnProperty(input.name)) {
+        if (Array.isArray(inputsObject[input.name])) {
+          inputsObject[input.name] = inputsObject[input.name].push(input)
+        } else {
+          inputsObject[input.name] = [input, inputsObject[input.name]]
+        }
+      } else {
+        inputsObject[input.name] = input
+      }
+      return inputsObject
+    }, {})
+  }
+}
+
+export class TangyForm {
+  id:string
+}
\ No newline at end of file
diff --git a/online-survey-app/src/app/tangy-forms/tangy-form-service.ts b/online-survey-app/src/app/tangy-forms/tangy-form-service.ts
new file mode 100644
index 0000000000..189ab3690e
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-form-service.ts
@@ -0,0 +1,41 @@
+import {Injectable} from '@angular/core';
+// A dummy function so TS does not complain about our use of emit in our pouchdb queries.
+const emit = (key, value) => {
+  return true;
+}
+
+@Injectable()
+export class TangyFormService {
+
+  db:any;
+  databaseName: String;
+
+  constructor() {
+    this.databaseName = 'tangy-forms'
+  }
+
+  // Would be nice if this was queue based so if two saves get called at the same time, the differentials are sequentials updated
+  // into the database. Using a getter and setter for property fields, this would be one way to queue.
+  async saveResponse(responseDoc) {
+    let r
+    if (!responseDoc._id) {
+      r = await this.db.post(responseDoc)
+    }
+    else {
+      r = await this.db.put(responseDoc)
+    }
+    return await this.db.get(r.id)
+
+  }
+
+  async getResponse(responseId) {
+    try {
+      let doc = await this.db.get(responseId)
+      return doc
+    } catch (e) {
+      return false
+    }
+  }
+
+
+}
diff --git a/online-survey-app/src/app/tangy-forms/tangy-form.service.ts b/online-survey-app/src/app/tangy-forms/tangy-form.service.ts
new file mode 100644
index 0000000000..f4a48e1860
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-form.service.ts
@@ -0,0 +1,72 @@
+import { UserDatabase } from '../shared/classes/user-database.class';
+import { HttpClient } from '@angular/common/http';
+import {Injectable} from '@angular/core';
+import {FormVersion} from "./classes/form-version.class";
+import {TangyFormsInfoService} from "./tangy-forms-info-service";
+import {FormInfo} from "./classes/form-info.class";
+
+@Injectable()
+export class TangyFormService {
+
+  db:any;
+  databaseName: String;
+  groupId:string
+  userId:string
+  formsMarkup: Array<any> = []
+  constructor(
+    private httpClient:HttpClient,
+    private tangyFormsInfoService: TangyFormsInfoService
+  ) {
+    this.databaseName = 'tangy-forms'
+  }
+
+  initialize(groupId) {
+    this.userId = localStorage.getItem('user_id') || 'Survey'
+    this.db = new UserDatabase(this.userId, groupId)
+  }
+
+  // Would be nice if this was queue based so if two saves get called at the same time, the differentials are sequentials updated
+  // into the database. Using a getter and setter for property fields, this would be one way to queue.
+  async saveResponse(response) {
+    try {
+      const doc = <any>await this.db.post(response)
+      return doc
+    } catch (e) {
+      return false
+    }
+  }
+
+  async getResponse(responseId) {
+    try {
+      const doc = <any>await this.db.get(responseId)
+      return doc
+    } catch (e) {
+      return false
+    }
+  }
+
+  async getAllResponses() {
+    return []
+  }
+
+  async getResponsesByFormId(formId:string, limit:number = 99999999, skip:number = 0) {
+    return <Array<any>>await this.httpClient.get(`/api/${this.groupId}/responsesByFormId/${formId}/${limit}/${skip}`).toPromise()
+    /*
+    return Array<TangyFormResponseModel>(<Array<any>>await this.httpClient.get(`/api/${groupId}/responsesByFormId/${formId}/${limit}/${skip}`).toPromise())
+      .map((doc) => new TangyFormResponseModel(doc))
+    */
+  }
+
+  /**
+   * Gets markup for a form. If displaying a formResponse, populate the revision in order to display the correct form version.
+   * @param formId
+   * @param formVersionId - Uses this value to lookup the correct version to display. It is null if creating a new response.
+   */
+  async getFormMarkup(formId, formVersionId:string = '') {
+    let formMarkup:any
+    let src: string = await this.tangyFormsInfoService.getFormSrc(formId, formVersionId)
+    formMarkup = await this.httpClient.get(src, {responseType: 'text'}).toPromise()
+    return formMarkup
+  }
+
+}
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-info-service.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-info-service.ts
new file mode 100644
index 0000000000..787cc6bff0
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-info-service.ts
@@ -0,0 +1,38 @@
+import { FormInfo } from './classes/form-info.class';
+import { Inject, Injectable } from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TangyFormsInfoService {
+  formsInfo: Array<FormInfo>
+  formsMarkup: Array<any> = []
+  constructor(
+    private http: HttpClient
+  ) { }
+
+  async getFormsInfo() {
+    this.formsInfo = this.formsInfo ? this.formsInfo : <Array<FormInfo>>await this.http.get('./assets/forms.json').toPromise()
+    return this.formsInfo
+  }
+
+  async getFormInfo(id:string):Promise<FormInfo> {
+    return (await this.getFormsInfo()).find(formInfo => formInfo.id === id)
+  }
+
+  async getFormTemplateMarkup(formId:string, formTemplateId:string):Promise<string> {
+    const formInfo = await this.getFormInfo(formId)
+    const formTemplate = formInfo.templates.find(formTemplate => formTemplate.id === formTemplateId)
+    const formTemplateMarkup = await this.http.get(formTemplate.src, { responseType: 'text' }).toPromise()
+    return formTemplateMarkup
+  }
+
+  async getFormSrc(formId, formVersionId:string = '') {
+    const formInfo = await this.getFormInfo(formId)
+    return formVersionId 
+      ? formInfo.formVersions.find(formVersion => formVersion.id === formVersionId).src
+      : formInfo.src
+  }
+
+}
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.css b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.css
similarity index 100%
rename from online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.css
rename to online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.css
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.html b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.html
similarity index 100%
rename from online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.html
rename to online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.html
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.spec.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.spec.ts
similarity index 100%
rename from online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.spec.ts
rename to online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.spec.ts
diff --git a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
similarity index 65%
rename from online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
rename to online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 4b2183bb69..6c599b0308 100644
--- a/online-survey-app/src/app/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -1,7 +1,9 @@
 import { HttpClient } from '@angular/common/http';
 import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { FormsService } from '../shared/_services/forms-service.service';
+import { FormsService } from 'src/app/shared/_services/forms-service.service';
+import { CaseService } from 'src/app/case/services/case.service';
+import { TangyFormService } from '../tangy-form.service';
 
 @Component({
   selector: 'app-tangy-forms-player',
@@ -16,34 +18,39 @@ export class TangyFormsPlayerComponent implements OnInit {
   caseId: string;
   caseEventId: string;
   eventFormId: string;
+  window: any;
   
-  constructor(private route: ActivatedRoute, private formsService: FormsService, private router: Router, private httpClient:HttpClient
+  constructor(
+    private route: ActivatedRoute, 
+    private formsService: FormsService, 
+    private router: Router, 
+    private httpClient:HttpClient,
+    private caseService: CaseService,
+    private tangyFormService: TangyFormService
   ) { 
     this.router.events.subscribe(async (event) => {
         this.formId = this.route.snapshot.paramMap.get('formId');
         this.formResponseId = this.route.snapshot.paramMap.get('formResponseId');
-        this.caseId = this.route.snapshot.paramMap.get('caseId');
-        this.caseEventId = this.route.snapshot.paramMap.get('caseEventId');
-        this.eventFormId = this.route.snapshot.paramMap.get('eventFormId');
+        this.caseId = this.route.snapshot.paramMap.get('case');
+        this.caseEventId = this.route.snapshot.paramMap.get('event');
+        this.eventFormId = this.route.snapshot.paramMap.get('form');
     });
   }
 
   async ngOnInit(): Promise<any> {
-    const data = await this.httpClient.get('./assets/form/form.html', {responseType: 'text'}).toPromise();
-    this.container.nativeElement.innerHTML = data;
-    let tangyForm = this.container.nativeElement.querySelector('tangy-form');
+    this.tangyFormService.initialize(window.location.pathname.split('/')[4]);
+    this.window = window;
+    this.window.T = {
+      case: this.caseService,
+      tangyForms: this.tangyFormService
+    }
 
+    let formResponse;
     if (this.caseId) {
       try {
-        const caseDoc = await this.formsService.getFormResponse(this.caseId);
-        if (caseDoc) {
-          let inputs = caseDoc.items[0].inputs;
-          if (inputs.length > 0) {
-            window.localStorage.setItem('caseVariables', JSON.stringify(inputs));
-          }
-        }
+        await this.caseService.load(this.caseId);
       } catch (error) {
-        console.log('Error loading case variables: ' + error)
+        console.log('Error loading case: ' + error)
       }
 
       if (this.eventFormId) {
@@ -51,7 +58,7 @@ export class TangyFormsPlayerComponent implements OnInit {
           // Attempt to load the form response for the event form
           const eventForm = await this.formsService.getEventFormData(this.eventFormId);
           if (eventForm && eventForm.formResponseId) {
-            tangyForm.response = await this.formsService.getFormResponse(eventForm.formResponseId);
+            formResponse = await this.formsService.getFormResponse(eventForm.formResponseId);
           }
         } catch (error) {
           //pass
@@ -59,6 +66,14 @@ export class TangyFormsPlayerComponent implements OnInit {
       }
     }
 
+    const data = await this.httpClient.get('./assets/form/form.html', {responseType: 'text'}).toPromise();
+    this.container.nativeElement.innerHTML = data;
+    let tangyForm = this.container.nativeElement.querySelector('tangy-form');
+
+    if (formResponse) {
+      tangyForm.response = formResponse;
+    }
+
     tangyForm.addEventListener('after-submit', async (event) => {
       event.preventDefault();
       try {
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-routing.module.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-routing.module.ts
new file mode 100755
index 0000000000..4bcb8e4c79
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-routing.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
+
+const routes: Routes = [{
+  path: 'tangy-form-player',
+  component: TangyFormsPlayerComponent
+}
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class TangyFormsRoutingModule { }
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms.module.ts b/online-survey-app/src/app/tangy-forms/tangy-forms.module.ts
new file mode 100755
index 0000000000..fd498a5b74
--- /dev/null
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms.module.ts
@@ -0,0 +1,32 @@
+import { TangyFormsInfoService } from './tangy-forms-info-service';
+import { TangyFormService } from './tangy-form.service';
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { TangyFormsRoutingModule } from './tangy-forms-routing.module';
+import { TangyFormsPlayerComponent } from './tangy-forms-player/tangy-forms-player.component';
+import { MatButtonModule } from "@angular/material/button";
+import { MatChipsModule } from '@angular/material/chips';
+import { MatMenuModule } from "@angular/material/menu";
+import { MatTabsModule } from "@angular/material/tabs";
+import {SharedModule} from "../shared/shared.module";
+
+@NgModule({
+  schemas: [CUSTOM_ELEMENTS_SCHEMA],
+  exports: [TangyFormsPlayerComponent],
+  imports: [
+    CommonModule,
+    MatTabsModule,
+    MatMenuModule,
+    MatButtonModule,
+    MatChipsModule,
+    SharedModule,
+    TangyFormsRoutingModule
+  ],
+  providers: [
+    TangyFormsInfoService,
+    TangyFormService
+  ],
+  declarations: [TangyFormsPlayerComponent]
+})
+export class TangyFormsModule { }
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index 2cce11623b..18d269db4a 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -33,7 +33,7 @@ mkdir -p $GROUP_DIRECTORY
 RELEASE_DIRECTORY="$GROUP_DIRECTORY/$FORM_ID"
 rm -r $RELEASE_DIRECTORY
 
-FORM_CLIENT_DIRECTORY="/tangerine/groups/$GROUP_ID/client/"
+FORM_CLIENT_DIRECTORY="/tangerine/groups/$GROUP_ID/client"
 FORM_DIRECTORY="$FORM_CLIENT_DIRECTORY/$FORM_ID"
 LOCATION_LIST_PATH="$FORM_CLIENT_DIRECTORY/location-list.json"
 LOCATION_LISTS_DIRECTORY="$FORM_CLIENT_DIRECTORY/locations"
@@ -45,6 +45,8 @@ CUSTOM_SCRIPTS_MAP="$FORM_CLIENT_DIRECTORY/custom-scripts.js.map"
 cp -r /tangerine/online-survey-app/dist/online-survey-app/ $RELEASE_DIRECTORY
 
 # Ensure the release directories exists
+mkdir -p $RELEASE_DIRECTORY/assets
+mkdir -p $RELEASE_DIRECTORY/assets/form
 mkdir -p $RELEASE_DIRECTORY/assets/locations
 mkdir -p $RELEASE_DIRECTORY/assets/media
 
@@ -57,6 +59,25 @@ cp /tangerine/translations/*.json $RELEASE_DIRECTORY/assets/
 cp $CUSTOM_SCRIPTS $RELEASE_DIRECTORY/assets/
 cp $CUSTOM_SCRIPTS_MAP $RELEASE_DIRECTORY/assets/
 
+CASE_DEFINITIONS="$FORM_CLIENT_DIRECTORY/case-definitions.json"
+echo "CASE_DEFINITIONS: $CASE_DEFINITIONS"
+if [ -f "$CASE_DEFINITIONS" ]; then
+  echo "Found case definitions"
+  cp $CASE_DEFINITIONS $RELEASE_DIRECTORY/assets/
+  # read the case definitions and add the files to the release directory
+  while IFS= read -r line
+  do
+    # if the line contains "src" then it is a file to copy
+    if [[ $line != *"src"* ]]; then
+      continue
+    fi
+    # line is like: "src": "./assets/parent-of-child-6-7-case.json"
+    # strip the file name
+    line=$(echo $line | sed 's/"src": ".\/assets\///g' | sed 's/"//g')
+    cp $FORM_CLIENT_DIRECTORY/$line $RELEASE_DIRECTORY/assets/
+  done < $CASE_DEFINITIONS
+fi
+
 FORM_UPLOAD_URL="/onlineSurvey/saveResponse/$GROUP_ID/$FORM_ID"
 
 if [[ $REQUIRE_ACCESS_CODE == 'true' ]]; then

From 69a510846a072bb312652832cd67f6027b1801ef Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 4 Jun 2024 13:56:12 -0400
Subject: [PATCH 13/59] Allow online survey link for case event form;
 online-survey custom-login-markup

---
 .../classes/event-form-definition.class.ts    |  1 +
 .../event-form-list-item.component.html       |  3 +++
 .../event-form-list-item.component.ts         | 15 ++++++++++++--
 .../survey-login/survey-login.component.html  |  5 ++---
 .../survey-login/survey-login.component.ts    | 20 +++++++++++++++++--
 .../auth/_services/authentication.service.ts  |  5 ++++-
 server/src/auth-utils.js                      | 10 ----------
 server/src/online-survey.js                   |  7 ++++---
 .../src/scripts/release-online-survey-app.sh  |  2 ++
 9 files changed, 47 insertions(+), 21 deletions(-)

diff --git a/editor/src/app/case/classes/event-form-definition.class.ts b/editor/src/app/case/classes/event-form-definition.class.ts
index 1b0782fddd..31904304bf 100644
--- a/editor/src/app/case/classes/event-form-definition.class.ts
+++ b/editor/src/app/case/classes/event-form-definition.class.ts
@@ -15,6 +15,7 @@ export class EventFormDefinition {
   allowDeleteIfFormNotStarted:string
   onEventFormOpen?:string
   onEventFormClose?:string
+  allowOnline?:boolean
 
   constructor() {
   }
diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
index 60a43e393d..bbe69c67fa 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
@@ -5,6 +5,9 @@
 		<div [innerHTML]="renderedTemplateListItemPrimary|unsanitizeHtml"></div>
 		<div [innerHTML]="renderedTemplateListItemSecondary|unsanitizeHtml" secondary></div>
 	</div>
+	<span *ngIf="!eventFormArchived && !eventForm.formResponseId && canLinkToOnlineSurvey">
+    <span class="link"><a href="/releases/prod/online-survey-apps/{{groupId}}/{{eventFormDefinition.formId}}/#/case/event/form/{{case._id}}/{{caseEvent.id}}/{{eventForm.id}}" target="_new"> <i class="material-icons mat-32 tangy-location-list-icon">link</i></a></span>
+	</span>
 	<span *ngIf="canUserDeleteForms">
 		<button (click)="onDeleteFormClick(); $event.stopPropagation()" class="tangy-small-list-icon">
 			<mwc-icon>delete</mwc-icon>
diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 8739566613..9b27b36fa8 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -13,7 +13,7 @@ import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service';
 import { CaseService } from '../../services/case.service';
 import { AppConfigService } from 'src/app/shared/_services/app-config.service';
 import { t } from 'tangy-form/util/t.js'
-
+import { GroupsService } from 'src/app/groups/services/groups.service';
 
 
 @Component({
@@ -47,13 +47,15 @@ export class EventFormListItemComponent implements OnInit {
   canUserDeleteForms: boolean;
   groupId:string;
   eventFormArchived: boolean = false;
+  canLinkToOnlineSurvey: boolean = false;
   response:any
 
   constructor(
     private formService: TangyFormService,
     private ref: ChangeDetectorRef,
     private router:Router,
-    private caseService: CaseService
+    private caseService: CaseService,
+    private groupsService: GroupsService
   ) {
     ref.detach();
   }
@@ -61,6 +63,15 @@ export class EventFormListItemComponent implements OnInit {
   async ngOnInit() {
     this.groupId = window.location.pathname.split('/')[2]
 
+    const group = await this.groupsService.getGroupInfo(this.groupId);
+    const groupOnlineSurveys = group?.onlineSurveys ?? [];
+
+    this.canLinkToOnlineSurvey = groupOnlineSurveys.some(survey => 
+      survey.published && 
+      survey.formId === this.eventFormDefinition.formId && 
+      this.eventFormDefinition.allowOnline
+    );
+
     this.canUserDeleteForms = ((this.eventFormDefinition.allowDeleteIfFormNotCompleted && !this.eventForm.complete)
     || (this.eventFormDefinition.allowDeleteIfFormNotStarted && !this.eventForm.formResponseId));
     const response = await this.formService.getResponse(this.eventForm.formResponseId);
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
index 2da30e3a12..b679b87125 100644
--- a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.html
@@ -3,7 +3,7 @@
 </mat-card>
 <mat-card *ngIf="ready">
     <form role="form" #login='ngForm' novalidate>
-      <img id="logo" src="/logo.png" width="100%">
+      <span #customLoginMarkup></span>
       <mat-form-field>
         <input matInput type="text" required [(ngModel)]="user.accessCode" id="username" name="username">
         <mat-placeholder>
@@ -18,5 +18,4 @@
         <small>{{errorMessage}}</small>
       </span>
     </form>
-</mat-card>
-<span #customLoginMarkup></span>
\ No newline at end of file
+</mat-card>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
index 01e461fedf..15f8194fd8 100644
--- a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
@@ -15,7 +15,7 @@ export class SurveyLoginComponent implements OnInit {
   errorMessage = '';
   returnUrl: string; // stores the value of the url to redirect to after login
   user = { accessCode: '' };
-  @ViewChild('customLoginMarkup', {static: true}) customLoginMarkup: ElementRef;
+  @ViewChild('customLoginMarkup', {static: false}) customLoginMarkup: ElementRef;
   ready = false
 
   constructor(
@@ -32,9 +32,25 @@ export class SurveyLoginComponent implements OnInit {
       this.router.navigate([this.returnUrl]);
       return;
     }
-    this.customLoginMarkup.nativeElement.innerHTML = await this.authenticationService.getCustomLoginMarkup()
     this.ready = true
+
+    await this.renderCustomLoginMarkup();
   }
+
+  async renderCustomLoginMarkup() {
+    let customLoginMarkup = '<img id="logo" src="/logo.png" width="100%">';
+    try {
+      const markup = await this.authenticationService.getCustomLoginMarkup();
+      if (markup) {
+        customLoginMarkup = markup;
+      }
+    } catch (error) {
+      //pass
+    }
+
+    this.customLoginMarkup.nativeElement.innerHTML = customLoginMarkup
+  }
+
   async loginUser() {
     try {
 
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index b38084a918..2f11a4ecbd 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -47,6 +47,8 @@ export class AuthenticationService {
       console.error(error);
       localStorage.removeItem('token');
       localStorage.removeItem('user_id');
+      localStorage.removeItem('password');
+      localStorage.removeItem('permissions');
       return false;
     }
   }
@@ -73,12 +75,13 @@ export class AuthenticationService {
     document.cookie = "Authorization=;max-age=-1";
     localStorage.setItem('token', token);
     localStorage.setItem('user_id', jwtData['accessCode']);
+    localStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
     document.cookie = `Authorization=${token}`;
   }
 
   async getCustomLoginMarkup() {
     try {
-      return <string>await this.http.get('/custom-login-markup', {responseType: 'text'}).toPromise()
+      return <string>await this.http.get('./assets/custom-login-markup.html', {responseType: 'text'}).toPromise()
     } catch (error) {
       console.error('No custom-login-markup found');
       return '';
diff --git a/server/src/auth-utils.js b/server/src/auth-utils.js
index 230b58217f..819405b7fa 100644
--- a/server/src/auth-utils.js
+++ b/server/src/auth-utils.js
@@ -12,15 +12,6 @@ const createLoginJWT = ({ username, permissions }) => {
   return jwt.sign({ username, permissions }, jwtTokenSecret, signingOptions);
 };
 
-const createAccessCodeJWT = ({ accessCode }) => {
-  const signingOptions = {
-    expiresIn,
-    issuer,
-    subject: accessCode,
-  };
-  return jwt.sign({ accessCode }, jwtTokenSecret, signingOptions);
-};
-
 const verifyJWT = (token) => {
   try {
     const jwtPayload = jwt.verify(token, jwtTokenSecret, { issuer });
@@ -41,7 +32,6 @@ const decodeJWT = (token) => {
 
 module.exports = {
   createLoginJWT,
-  createAccessCodeJWT,
   decodeJWT,
   verifyJWT,
 };
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 6104647ffc..eb46011ad9 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -2,8 +2,7 @@ const DB = require('./db.js');
 const GROUPS_DB = new DB('groups');
 const { v4: uuidV4 } = require('uuid');
 const createFormKey = () => uuidV4();
-const { createAccessCodeJWT } = require('./auth-utils');
-const { log } = require('console');
+const { createLoginJWT } = require('./auth-utils');
 
 const login = async (req, res) => {
   try {
@@ -25,7 +24,9 @@ const login = async (req, res) => {
     const userProfileDoc = docs.find(doc => doc.form.id === 'user-profile');
 
     if (userProfileDoc) {
-      const token = createAccessCodeJWT({"accessCode": accessCode});
+      debugger;
+      let permissions = {groupPermissions: [], sitewidePermissions: []};
+      const token = createLoginJWT({ "username": accessCode, permissions });
       return res.status(200).send({ data: { token } });
     }
   } catch (error) {
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index 18d269db4a..091901ce57 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -40,6 +40,7 @@ LOCATION_LISTS_DIRECTORY="$FORM_CLIENT_DIRECTORY/locations"
 MEDIA_DIRECTORY="$FORM_CLIENT_DIRECTORY/media"
 CUSTOM_SCRIPTS="$FORM_CLIENT_DIRECTORY/custom-scripts.js"
 CUSTOM_SCRIPTS_MAP="$FORM_CLIENT_DIRECTORY/custom-scripts.js.map"
+CUSTOM_LOGIN_MARKUP="$FORM_CLIENT_DIRECTORY/custom-login-markup.html"
 
 # Set up the release dir from the dist
 cp -r /tangerine/online-survey-app/dist/online-survey-app/ $RELEASE_DIRECTORY
@@ -58,6 +59,7 @@ cp -r $MEDIA_DIRECTORY $RELEASE_DIRECTORY/assets/
 cp /tangerine/translations/*.json $RELEASE_DIRECTORY/assets/
 cp $CUSTOM_SCRIPTS $RELEASE_DIRECTORY/assets/
 cp $CUSTOM_SCRIPTS_MAP $RELEASE_DIRECTORY/assets/
+cp $CUSTOM_LOGIN_MARKUP $RELEASE_DIRECTORY/assets/
 
 CASE_DEFINITIONS="$FORM_CLIENT_DIRECTORY/case-definitions.json"
 echo "CASE_DEFINITIONS: $CASE_DEFINITIONS"

From 0b075e27533063f6fb1335767228f1d11153088b Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 4 Jun 2024 18:02:30 -0400
Subject: [PATCH 14/59] Ensure auth works for case in online surveys

---
 .../auth/_services/authentication.service.ts  |  2 +-
 .../shared/_services/forms-service.service.ts | 53 +-------------
 .../tangy-forms-player.component.ts           | 41 +++++++----
 server/src/case-api.js                        | 72 ++++++++++++++-----
 server/src/classes/case.class.js              | 19 ++++-
 server/src/express-app.js                     | 21 +++---
 server/src/online-survey.js                   | 34 +--------
 7 files changed, 110 insertions(+), 132 deletions(-)

diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 2f11a4ecbd..8aca5f2adb 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -74,7 +74,7 @@ export class AuthenticationService {
     const jwtData = jwtDecode(token);
     document.cookie = "Authorization=;max-age=-1";
     localStorage.setItem('token', token);
-    localStorage.setItem('user_id', jwtData['accessCode']);
+    localStorage.setItem('user_id', jwtData['username']);
     localStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
     document.cookie = `Authorization=${token}`;
   }
diff --git a/online-survey-app/src/app/shared/_services/forms-service.service.ts b/online-survey-app/src/app/shared/_services/forms-service.service.ts
index ee4ddb17ff..3c022c0ff1 100644
--- a/online-survey-app/src/app/shared/_services/forms-service.service.ts
+++ b/online-survey-app/src/app/shared/_services/forms-service.service.ts
@@ -2,7 +2,6 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Form } from '../classes/form';
 import { AppConfigService } from './app-config.service';
-import { AuthenticationService } from 'src/app/core/auth/_services/authentication.service';
 
 @Injectable({
   providedIn: 'root'
@@ -11,8 +10,7 @@ export class FormsService {
 
   constructor(
     private httpClient: HttpClient, 
-    private appConfigService: AppConfigService,
-    private authenticationService: AuthenticationService
+    private appConfigService: AppConfigService
   ) { }
 
   async getForms(): Promise<Form[]> {
@@ -40,36 +38,6 @@ export class FormsService {
     }
   }
 
-  async getFormResponse(formResponseId: string): Promise<any> {
-    try {
-      const config = await this.appConfigService.getAppConfig();
-
-      const headers = new HttpHeaders();
-      headers.set('formUploadToken', config.uploadKey);
-      const url = `/onlineSurvey/getResponse/${config.groupId}/${formResponseId}`;
-      const data = await this.httpClient.get(url, {headers}).toPromise();
-
-      return data;
-    } catch (error) {
-      console.error(error);
-    }
-  }
-
-  async getEventFormData(eventFormId: string): Promise<any> {
-    try {
-      const config = await this.appConfigService.getAppConfig();
-
-      const headers = new HttpHeaders();
-      headers.set('formUploadToken', config.uploadKey);
-      const url = `/case/getEventFormData/${config.groupId}/${eventFormId}`;
-      const data = await this.httpClient.get(url, {headers}).toPromise();
-
-      return data;
-    } catch (error) {
-      console.error(error);
-    }
-  }
-
   async uploadFormResponse(formResponse): Promise<boolean>{
     try {
       const config = await this.appConfigService.getAppConfig();
@@ -87,23 +55,4 @@ export class FormsService {
       return false;
     }
   }
-
-  async uploadFormResponseForCase(formResponse, eventFormId): Promise<boolean>{
-    try {
-      const config = await this.appConfigService.getAppConfig();
-
-      // Set the groupId or it will be missing from the form
-      // TODO: Move this logic to tangy-form so it happens for all responses
-      formResponse.groupId = config.groupId
-
-      const headers = new HttpHeaders();
-      const formUploadURL = `/onlineSurvey/saveResponse/${config.groupId}/${eventFormId}/${formResponse._id}`;
-      headers.set('formUploadToken', config.uploadKey);
-      const data = await this.httpClient.post(formUploadURL, formResponse, {headers, observe: 'response'}).toPromise();
-      return data.status === 200;
-    } catch (error) {
-      console.error(error);
-      return false;
-    }
-  }
 }
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 6c599b0308..4aed33dc0e 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -45,6 +45,7 @@ export class TangyFormsPlayerComponent implements OnInit {
       tangyForms: this.tangyFormService
     }
 
+    // Loading the formResponse must happen before rendering the innerHTML
     let formResponse;
     if (this.caseId) {
       try {
@@ -56,9 +57,12 @@ export class TangyFormsPlayerComponent implements OnInit {
       if (this.eventFormId) {
         try {
           // Attempt to load the form response for the event form
-          const eventForm = await this.formsService.getEventFormData(this.eventFormId);
-          if (eventForm && eventForm.formResponseId) {
-            formResponse = await this.formsService.getFormResponse(eventForm.formResponseId);
+          const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
+          if (event.id) {
+            const eventForm = event.eventForms.find(eventForm => eventForm.id === this.eventFormId);
+              if (eventForm && eventForm.id === this.eventFormId && eventForm.formResponseId) {
+                formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId);
+            }
           }
         } catch (error) {
           //pass
@@ -76,23 +80,32 @@ export class TangyFormsPlayerComponent implements OnInit {
 
     tangyForm.addEventListener('after-submit', async (event) => {
       event.preventDefault();
+      const formResponse = event.target.response;
       try {
-        if (this.eventFormId) {
-          if (await this.formsService.uploadFormResponseForCase(event.target.response, this.eventFormId)) {
-            this.router.navigate(['/form-submitted-success']);
-          } else {
-            alert('Form could not be submitted. Please retry');
-          }
+        if (await this.formsService.uploadFormResponse(formResponse)) {
+          this.router.navigate(['/form-submitted-success']);
         } else {
-          if (await this.formsService.uploadFormResponse(event.target.response)) {
-            this.router.navigate(['/form-submitted-success']);
-          } else {
-            alert('Form could not be submitted. Please retry');
-          }
+          alert('Form could not be submitted. Please retry');
         }
       } catch (error) {
         console.error(error);
       }
+
+      if (this.eventFormId) {
+        try {
+          const caseEvent = this.caseService.case.events.find(event => event.id === this.caseEventId);
+          if (caseEvent.id) {
+            const eventForm = caseEvent.eventForms.find(eventForm => eventForm.id === this.eventFormId);
+              if (eventForm && eventForm.id === this.eventFormId) {
+                eventForm.formResponseId = formResponse._id
+                eventForm.complete = true
+                await this.caseService.save();
+            }
+          }
+        } catch (error) {
+          console.error(error);
+        }
+      }
     });
   }
 }
diff --git a/server/src/case-api.js b/server/src/case-api.js
index 7432b9fcb5..825486a6a1 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -122,28 +122,39 @@ createEventForm = async (req, res) => {
   }
 }
 
-createParticipant = async (req, res) => {
-  const groupId = req.params.groupId
-  const caseId = req.params.caseId
-  const caseRoleId = req.params.caseRoleId
-
-  const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
-  const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
-
-  let participant = new Participant(groupId, caseId, caseRole)
+updateEventForm = async (req, res) => {
 
-  try {
-    const db = new DB(groupId)
-    const caseDoc = await db.get(caseId);
-    caseDoc.participant.push(participant);
-    await db.put(caseDoc);
-    res.send(eventForm._id);
-  } catch (err) {
-    res.status(500).send
+  let groupId = req.params.groupId
+  let caseId = req.params.caseId
+  let caseEventId = req.params.caseEventId
+  let eventFormId = req.params.eventFormId
+
+  const db = new DB(groupId);
+  const data = req.body;
+
+  if (eventFormId) {
+    let newEventForm = new EventForm(data);
+    try {
+      let caseDoc = await db.get(caseId);
+      if (caseDoc) {
+        let event = caseDoc.events.find((e) => e.id === caseEventId);
+        if (event) {
+          let eventForm = event.eventForms.find((f) => f.id === eventFormId);
+          if (eventForm) {
+            eventForm = newEventForm;
+          } else {
+            event.eventForms.push(eventForm);
+          }
+          await db.put(caseDoc);
+        }
+      }
+    } catch (error) {
+      console.error(error);
+    }
   }
 }
 
-getEventFormData = async (req, res) => {
+readEventForm = async (req, res) => {
   const groupDb = new DB(req.params.groupId)
   let data = {}
   try {
@@ -165,6 +176,27 @@ getEventFormData = async (req, res) => {
   res.send(data)
 }
 
+createParticipant = async (req, res) => {
+  const groupId = req.params.groupId
+  const caseId = req.params.caseId
+  const caseRoleId = req.params.caseRoleId
+
+  const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+  const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
+
+  let participant = new Participant(groupId, caseId, caseRole)
+
+  try {
+    const db = new DB(groupId)
+    const caseDoc = await db.get(caseId);
+    caseDoc.participant.push(participant);
+    await db.put(caseDoc);
+    res.send(eventForm._id);
+  } catch (err) {
+    res.status(500).send
+  }
+}
+
 getCaseEventFormSurveyLinks = async (req, res) => {
   const caseId = req.params.caseId
   const groupDb = new DB(req.params.groupId)
@@ -201,5 +233,7 @@ module.exports = {
   createCase,
   createCaseEvent,
   createEventForm,
-  getEventFormData
+  readEventForm,
+  updateEventForm,
+  createParticipant
 }
\ No newline at end of file
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index 13be0959e6..c9f146936e 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -58,8 +58,23 @@ class CaseEvent {
 }
 
 class EventForm {
-  constructor(caseId, caseEventId, eventFormDefinition) {
-    this.id = uuidV4()
+
+  id;
+  caseId;
+  caseEventId;
+  eventFormDefinitionId;
+  required;
+  participantId;
+  complete;
+  inactive;
+  formResponseId;
+
+  constructor(data) {
+    Object.assign(this, data);
+  }
+
+  constructor(caseId, caseEventId, eventFormDefinition = null) {
+    this.id = id || uuidV4()
     this.caseId = caseId
     this.caseEventId = caseEventId
     this.eventFormDefinitionId = eventFormDefinition.id
diff --git a/server/src/express-app.js b/server/src/express-app.js
index 9de24e7952..595d296ed6 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -54,7 +54,8 @@ const {
   createCase,
   createCaseEvent,
   createEventForm,
-  getEventFormData
+  readEventForm,
+  updateEventForm
 } = require('./case-api')
 const { createUserProfile } = require('./user-profile')
 log.info('heartbeat')
@@ -199,25 +200,23 @@ app.post('/userProfile/createUserProfile/:groupId', createUserProfile);
  * Case API routes
  */
 
-app.get('/case/getCaseDefinitions/:groupId', getCaseDefinitions);
-app.get('/case/getCaseDefinition/:groupId/:caseDefinitionId', getCaseDefinition);
-app.post('/case/createCase/:groupId/:caseDefinitionId', createCase);
-app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', createCaseEvent);
-app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', createEventForm);
-
-app.get('/case/getEventFormData/:groupId/:eventFormId', getEventFormData);
+app.get('/case/getCaseDefinitions/:groupId', isAuthenticated, getCaseDefinitions);
+app.get('/case/getCaseDefinition/:groupId/:caseDefinitionId', isAuthenticated, getCaseDefinition);
+app.post('/case/createCase/:groupId/:caseDefinitionId', isAuthenticated, createCase);
+app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', isAuthenticated, createCaseEvent);
+app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', isAuthenticated, createEventForm);
+app.get('/case/readEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, readEventForm);
+app.post('/case/updateEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, updateEventForm);
 
 /**
  * Online survey routes
  */
 
+app.post('/onlineSurvey/login/:groupId/:accessCode', surveyLogin);
 app.post('/onlineSurvey/publish/:groupId/:formId', isAuthenticated, publishSurvey);
 app.put('/onlineSurvey/unpublish/:groupId/:formId', isAuthenticated, unpublishSurvey);
 app.post('/onlineSurvey/saveResponse/:groupId/:formId', hasSurveyUploadKey, saveSurveyResponse);
 
-app.post('/onlineSurvey/login/:groupId/:accessCode', surveyLogin);
-app.get('/onlineSurvey/getResponse/:groupId/:formResponseId', /* hasSurveyUploadKey,*/ getSurveyResponse);
-app.post('/onlineSurvey/saveResponse/:groupId/:eventFormId/:formResponseId', /* hasSurveyUploadKey,*/ saveSurveyResponse);
 /*
  * More API
  */
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index eb46011ad9..5c1e820b64 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -53,43 +53,11 @@ const getResponse = async (req, res) => {
 
 const saveResponse = async (req, res) => {
   try {
-    const { groupId, formId, eventFormId } = req.params;
-
-    console.log('saveResponse', groupId, formId, eventFormId);
+    const { groupId, formId } = req.params;
 
     const db = new DB(groupId);
     const data = req.body;
     data.formId = formId;
-
-    if (eventFormId) {
-      try {
-        const results = await db.query("eventForms/eventForms", {key: eventFormId, include_docs: true});
-        if (results.rows.length > 0) {
-          const caseDoc = results.rows[0].doc;
-          console.log('caseId', caseDoc._id)
-
-          for (let event of caseDoc.events) {
-            let eventForm = event.eventForms.find((f) => f.id === eventFormId);
-            if (eventForm) {
-
-              console.log('eventForm', eventForm.id);
-
-              data.caseId = caseDoc._id;
-              data.caseEventId = event.id;
-              data.eventFormId = eventForm.id;
-
-              eventForm.formResponseId = data._id;
-              eventForm.complete = true
-              await db.put(caseDoc);
-              break;
-            }
-          }
-        }
-      } catch (error) {
-        console.error(error);
-      }
-    }
-
     await db.post(data);
     return res.status(200).send({ data: 'Response Saved Successfully' });
   } catch (error) {

From c565ceca5f3cf0298060f4ac37376bdcb160d689 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 4 Jun 2024 18:22:29 -0400
Subject: [PATCH 15/59] Editor event form list items get a copy link button

---
 .../event-form-list-item.component.html               |  4 +++-
 .../event-form-list-item.component.ts                 | 11 ++++++++++-
 translations/translation.en.json                      |  1 +
 3 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
index bbe69c67fa..aab0438e61 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
@@ -6,7 +6,9 @@
 		<div [innerHTML]="renderedTemplateListItemSecondary|unsanitizeHtml" secondary></div>
 	</div>
 	<span *ngIf="!eventFormArchived && !eventForm.formResponseId && canLinkToOnlineSurvey">
-    <span class="link"><a href="/releases/prod/online-survey-apps/{{groupId}}/{{eventFormDefinition.formId}}/#/case/event/form/{{case._id}}/{{caseEvent.id}}/{{eventForm.id}}" target="_new"> <i class="material-icons mat-32 tangy-location-list-icon">link</i></a></span>
+		<button (click)="onCopyLinkClick(); $event.stopPropagation()" class="tangy-small-list-icon">
+			<mwc-icon>link</mwc-icon>
+		</button>
 	</span>
 	<span *ngIf="canUserDeleteForms">
 		<button (click)="onDeleteFormClick(); $event.stopPropagation()" class="tangy-small-list-icon">
diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 9b27b36fa8..55dfbd721b 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -14,6 +14,7 @@ import { CaseService } from '../../services/case.service';
 import { AppConfigService } from 'src/app/shared/_services/app-config.service';
 import { t } from 'tangy-form/util/t.js'
 import { GroupsService } from 'src/app/groups/services/groups.service';
+import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
 
 
 @Component({
@@ -55,7 +56,8 @@ export class EventFormListItemComponent implements OnInit {
     private ref: ChangeDetectorRef,
     private router:Router,
     private caseService: CaseService,
-    private groupsService: GroupsService
+    private groupsService: GroupsService,
+    private tangyErrorHandler: TangyErrorHandler
   ) {
     ref.detach();
   }
@@ -142,4 +144,11 @@ export class EventFormListItemComponent implements OnInit {
   onUnarchiveFormClick() {
     this.formUnarchivedEvent.emit(this.eventForm.id);
   }
+
+  onCopyLinkClick() {
+    const url = `${window.location.origin}/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
+    navigator.clipboard.writeText(url);
+
+    this.tangyErrorHandler.handleError('Online Survey link copied to clipboard');
+  }
 }
diff --git a/translations/translation.en.json b/translations/translation.en.json
index 10dbcac72a..8acba54c1a 100644
--- a/translations/translation.en.json
+++ b/translations/translation.en.json
@@ -195,6 +195,7 @@
   "Number of responses uploaded": "Number of responses uploaded",
   "Observations": "Observations",
   "October": "October",
+  "Online Survey link copied to clipboard": "Online Survey link copied to clipboard",
   "Online Sync": "Online Sync",
   "Online Sync is used when there is a reliable Internet connection. If you have no Internet connection, use the P2P tab above to transfer records between nearby devices.": "Online Sync is used when there is a reliable Internet connection. If you have no Internet connection, use the P2P tab above to transfer records between nearby devices.",
   "Open": "Open",

From 036034739fee578c52a72f11b45c7506bc0dda68 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 5 Jun 2024 12:53:16 -0400
Subject: [PATCH 16/59] Add menu to event form list for survey link copy, qr,
 and open

---
 .../event-form-list-item.component.html       | 15 +++++++++++--
 .../event-form-list-item.component.ts         | 21 ++++++++++++++++++-
 2 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
index aab0438e61..505ebb27f5 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
@@ -6,9 +6,20 @@
 		<div [innerHTML]="renderedTemplateListItemSecondary|unsanitizeHtml" secondary></div>
 	</div>
 	<span *ngIf="!eventFormArchived && !eventForm.formResponseId && canLinkToOnlineSurvey">
-		<button (click)="onCopyLinkClick(); $event.stopPropagation()" class="tangy-small-list-icon">
-			<mwc-icon>link</mwc-icon>
+		<button class="tangy-small-list-icon" [matMenuTriggerFor]="linkMenu" (click)="showLinkMenu(); $event.stopPropagation()">
+			<mwc-icon>share</mwc-icon>
 		</button>
+		<mat-menu #linkMenu="matMenu">
+			<button mat-menu-item tangy-small-list-icon (click)="onCopyLinkClick()">
+				<mwc-icon>link</mwc-icon>
+			</button>
+			<button mat-menu-item tangy-small-list-icon (click)="onQRCodeLinkClick()">
+				<mwc-icon>qr_code</mwc-icon>
+			</button>
+			<button mat-menu-item tangy-small-list-icon (click)="$event.stopPropagation()">
+				<a href="{{surveyLinkUrl}}" target="_new"><mwc-icon>open_in_new</mwc-icon></a>
+			</button>
+		</mat-menu>
 	</span>
 	<span *ngIf="canUserDeleteForms">
 		<button (click)="onDeleteFormClick(); $event.stopPropagation()" class="tangy-small-list-icon">
diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 55dfbd721b..46e16b0579 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -15,7 +15,7 @@ import { AppConfigService } from 'src/app/shared/_services/app-config.service';
 import { t } from 'tangy-form/util/t.js'
 import { GroupsService } from 'src/app/groups/services/groups.service';
 import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-
+import * as qrcode from 'qrcode-generator-es6';
 
 @Component({
   selector: 'app-event-form-list-item',
@@ -50,6 +50,7 @@ export class EventFormListItemComponent implements OnInit {
   eventFormArchived: boolean = false;
   canLinkToOnlineSurvey: boolean = false;
   response:any
+  surveyLinkUrl:string;
 
   constructor(
     private formService: TangyFormService,
@@ -74,6 +75,8 @@ export class EventFormListItemComponent implements OnInit {
       this.eventFormDefinition.allowOnline
     );
 
+    this.surveyLinkUrl = `/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
+
     this.canUserDeleteForms = ((this.eventFormDefinition.allowDeleteIfFormNotCompleted && !this.eventForm.complete)
     || (this.eventFormDefinition.allowDeleteIfFormNotStarted && !this.eventForm.formResponseId));
     const response = await this.formService.getResponse(this.eventForm.formResponseId);
@@ -145,10 +148,26 @@ export class EventFormListItemComponent implements OnInit {
     this.formUnarchivedEvent.emit(this.eventForm.id);
   }
 
+  showLinkMenu() {
+    this.ref.detectChanges()
+  }
+
   onCopyLinkClick() {
     const url = `${window.location.origin}/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
     navigator.clipboard.writeText(url);
 
     this.tangyErrorHandler.handleError('Online Survey link copied to clipboard');
   }
+
+  onQRCodeLinkClick() {
+    const url = `${window.location.origin}${this.surveyLinkUrl}`;
+
+    const qr = new qrcode.default(0, 'H')
+    qr.addData(`${url}`)
+    qr.make()
+    window['dialog'].innerHTML = `<div style="width:${Math.round((window.innerWidth > window.innerHeight ? window.innerHeight : window.innerWidth) *.6)}px" id="qr"></div>`
+    window['dialog'].open()
+    window['dialog'].querySelector('#qr').innerHTML = qr.createSvgTag({cellSize:500, margin:0,cellColor:(c, r) =>''})
+  }
+
 }

From 1e0077cd3a5639b6dcd071722b7c1a79b7ef3dca Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 7 Jun 2024 13:09:32 -0400
Subject: [PATCH 17/59] Add getOnlineSurvey and getCaseEventFormSurveyLinks
 APIs

---
 server/src/case-api.js      | 60 ++++++++++++++++++++++++-------------
 server/src/express-app.js   | 17 +++++++----
 server/src/online-survey.js | 25 +++++++++++++++-
 3 files changed, 76 insertions(+), 26 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 825486a6a1..e5c6fb9976 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -2,7 +2,6 @@ const DB = require('./db.js')
 const log = require('tangy-log').log
 const path = require('path')
 const fs = require('fs')
-const { v4: uuidV4 } = require('uuid')
 
 const {Case: Case, CaseEvent: CaseEvent, EventForm: EventForm } = require('./classes/case.class.js')
 
@@ -73,6 +72,17 @@ createCase = async (req, res) => {
   }
 }
 
+readCase = async (req, res) => {
+  const groupDb = new DB(req.params.groupId)
+  let data = {}
+  try {
+    data = await groupDb.get(caseId);
+  } catch (err) {
+    res.status(500).send(err);
+  }
+  res.send(data)
+}
+
 createCaseEvent = async (req, res) => {
   let groupId = req.params.groupId
   let caseId = req.params.caseId
@@ -198,42 +208,52 @@ createParticipant = async (req, res) => {
 }
 
 getCaseEventFormSurveyLinks = async (req, res) => {
-  const caseId = req.params.caseId
-  const groupDb = new DB(req.params.groupId)
-
   let data = []
   try {
-    const results = await groupDb.get(caseId)
-    if (results.rows.length > 0) {
-      const caseDoc = results.rows[0].doc
-      const caseDefinition = _getCaseDefinition(groupId, caseDoc.caseDefinitionId)
-      for (let event of doc.events) {
-        let eventForm = event.eventForms.find((f) => f.id === req.params.eventFormId);
-        if (eventForm) {
-          let formId;
-          for (let eventDefinition of caseDefinition.eventDefinitions) {
-            formId = eventDefinition.find((e) => e.id === eventForm.eventFormDefinitionId).formId
-            if (formId) break;
+    const caseId = req.params.caseId
+    const groupId = req.params.groupId
+
+    const GROUPS_DB = new DB('groups');
+    const groupData = await GROUPS_DB.get(groupId);
+    const onlineSurveys = groupData.onlineSurveys ? groupData.onlineSurveys : [];
+
+    const groupDb = new DB(req.params.groupId)
+    const caseDoc = await groupDb.get(caseId)
+    const caseDefinition = _getCaseDefinition(groupId, caseDoc.caseDefinitionId)
+
+    for (let event of caseDoc.events) {
+      for (let eventForm of event.eventForms.filter((f) => !f.formResponseId)) {
+        for (let eventDefinition of caseDefinition.eventDefinitions) {
+          let eventFormDefinition = eventDefinition.eventFormDefinitions.find((e) => e.id === eventForm.eventFormDefinitionId)
+          if (eventFormDefinition && eventFormDefinition.formId) {
+            const formId = eventFormDefinition.formId
+            const survey = onlineSurveys.find((s) => s.formId === formId && s.published === true && s.locked === true)
+            if (survey) {
+              const origin = `${process.env.T_PROTOCOL}://${process.env.T_HOST_NAME}`
+              const pathname = `releases/prod/online-survey-apps/${groupId}/${formId}`
+              const hash = `#/case/event/form/${caseId}/${event.id}/${eventForm.id}`
+              const url = `${origin}/${pathname}/${hash}`
+              data.push(url)
+            }
           }
         }
-        const url = `http://localhost/releases/prod/online-survey-apps/group-344fabfe-f892-4a6d-a1da-58616949982f/${formId}/#/caseFormResponse/${caseId}/${eventForm.id}`
-        data.push(url)
-        break;
       }
     }
+    res.send(data)
   } catch (err) {
     res.status(500).send(err);
   }
-  res.send(data)
 }
 
 module.exports = {
   getCaseDefinitions,
   getCaseDefinition,
   createCase,
+  readCase,
   createCaseEvent,
   createEventForm,
   readEventForm,
   updateEventForm,
-  createParticipant
+  createParticipant,
+  getCaseEventFormSurveyLinks
 }
\ No newline at end of file
diff --git a/server/src/express-app.js b/server/src/express-app.js
index 595d296ed6..5e0c494213 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -47,15 +47,18 @@ const { extendSession, findUserByUsername,
 const {registerUser,  getUserByUsername, isUserSuperAdmin, isUserAnAdminUser, getGroupsByUser, deleteUser,
    getAllUsers, checkIfUserExistByUsername, findOneUserByUsername,
    findMyUser, updateUser, restoreUser, updateMyUser} = require('./users');
-const {login: surveyLogin, getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey} = require('./online-survey')
+const {login: surveyLogin, getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey, getOnlineSurveys} = require('./online-survey')
 const {
   getCaseDefinitions,
   getCaseDefinition,
   createCase,
+  readCase,
   createCaseEvent,
   createEventForm,
   readEventForm,
-  updateEventForm
+  updateEventForm,
+  createParticipant,
+  getCaseEventFormSurveyLinks
 } = require('./case-api')
 const { createUserProfile } = require('./user-profile')
 log.info('heartbeat')
@@ -191,22 +194,25 @@ app.get('/users/groupPermissionsByGroupName/:groupName', isAuthenticated, getUse
  app.get('/configuration/passwordPolicyConfig', isAuthenticated, passwordPolicyConfig);
 
 /**
- * User Profile API
+ * User Profile API Routes
  */
 
-app.post('/userProfile/createUserProfile/:groupId', createUserProfile);
+app.post('/userProfile/createUserProfile/:groupId', isAuthenticated, createUserProfile);
 
 /**
- * Case API routes
+ * Case API Routes
  */
 
 app.get('/case/getCaseDefinitions/:groupId', isAuthenticated, getCaseDefinitions);
 app.get('/case/getCaseDefinition/:groupId/:caseDefinitionId', isAuthenticated, getCaseDefinition);
 app.post('/case/createCase/:groupId/:caseDefinitionId', isAuthenticated, createCase);
+app.post('/case/readCase/:groupId/:caseId', isAuthenticated, readCase);
 app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', isAuthenticated, createCaseEvent);
 app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', isAuthenticated, createEventForm);
 app.get('/case/readEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, readEventForm);
 app.post('/case/updateEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, updateEventForm);
+app.post('/case/createParticipant/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, createParticipant);
+app.get('/case/getCaseEventFormSurveyLinks/:groupId/:caseId', isAuthenticated, getCaseEventFormSurveyLinks);
 
 /**
  * Online survey routes
@@ -216,6 +222,7 @@ app.post('/onlineSurvey/login/:groupId/:accessCode', surveyLogin);
 app.post('/onlineSurvey/publish/:groupId/:formId', isAuthenticated, publishSurvey);
 app.put('/onlineSurvey/unpublish/:groupId/:formId', isAuthenticated, unpublishSurvey);
 app.post('/onlineSurvey/saveResponse/:groupId/:formId', hasSurveyUploadKey, saveSurveyResponse);
+app.get('/onlineSurvey/getOnlineSurveys/:groupId', isAuthenticated, getOnlineSurveys);
 
 /*
  * More API
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 5c1e820b64..70514e5b90 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -117,10 +117,33 @@ const unpublishSurvey = async (req, res) => {
   }
 };
 
+const getOnlineSurveys = async (req, res) => {
+  try {
+    const { groupId } = req.params;
+    const formId = req.params.formId || null; // optional
+    if (!groupId) {
+      return res.status(500).send('Could  not find role');
+    }
+    const data = await GROUPS_DB.get(groupId);
+    if (data.onlineSurveys) {
+      if (formId) {
+        return res.status(200).send({ data: data.onlineSurveys.filter((s) => s.formId === formId) });
+      } else {
+        return res.status(200).send({ data: data.onlineSurveys });
+      }
+    } else {
+      return res.status(200).send({ data: [] });
+    }
+  } catch (error) {
+    res.status(500).send('Could not find survey');
+  }
+}
+
 module.exports = {
   login,
   getResponse,
   saveResponse,
   publishSurvey,
-  unpublishSurvey
+  unpublishSurvey,
+  getOnlineSurveys
 }
\ No newline at end of file

From 5837be682347c927a2cf1bd61611189d996c5f78 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 8 Jun 2024 15:38:06 -0400
Subject: [PATCH 18/59] Fix up case create api

---
 server/src/case-api.js           | 69 +++++---------------------------
 server/src/classes/case.class.js | 17 +-------
 server/src/express-app.js        |  4 --
 3 files changed, 12 insertions(+), 78 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index e5c6fb9976..36fd03bc3a 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -54,12 +54,21 @@ createCase = async (req, res) => {
   let groupId = req.params.groupId
   let caseDefinitionId = req.params.caseDefinitionId
 
+  // check that req.body is json, if not, use JSON.parse
+  let inputs;
+  try {
+    if (typeof req.body == 'object' && Object.keys(req.body).length > 0) {
+      inputs = req.body
+    }
+  } catch (err) {
+    log.error(`Error parsing case inputs from request body: ${err}`)
+  }
+
   try {
     const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
     let caseDoc = new Case(groupId, caseDefinitionId, caseDefinition)
 
-    if (Object.keys(req.body).length > 0) {
-      const inputs = req.body
+    if (inputs) {
       caseDoc.addInputs(inputs)
     }
 
@@ -132,60 +141,6 @@ createEventForm = async (req, res) => {
   }
 }
 
-updateEventForm = async (req, res) => {
-
-  let groupId = req.params.groupId
-  let caseId = req.params.caseId
-  let caseEventId = req.params.caseEventId
-  let eventFormId = req.params.eventFormId
-
-  const db = new DB(groupId);
-  const data = req.body;
-
-  if (eventFormId) {
-    let newEventForm = new EventForm(data);
-    try {
-      let caseDoc = await db.get(caseId);
-      if (caseDoc) {
-        let event = caseDoc.events.find((e) => e.id === caseEventId);
-        if (event) {
-          let eventForm = event.eventForms.find((f) => f.id === eventFormId);
-          if (eventForm) {
-            eventForm = newEventForm;
-          } else {
-            event.eventForms.push(eventForm);
-          }
-          await db.put(caseDoc);
-        }
-      }
-    } catch (error) {
-      console.error(error);
-    }
-  }
-}
-
-readEventForm = async (req, res) => {
-  const groupDb = new DB(req.params.groupId)
-  let data = {}
-  try {
-    let options = { key: req.params.eventFormId, include_docs: true }
-    const results = await groupDb.query('eventForms/eventForms', options);
-    if (results.rows.length > 0) {
-      const doc = results.rows[0].doc
-      for (let event of doc.events) {
-        let eventForm = event.eventForms.find((f) => f.id === req.params.eventFormId);
-        if (eventForm) {
-          data = eventForm
-          break;
-        }
-      }
-    }
-  } catch (err) {
-    res.status(500).send(err);
-  }
-  res.send(data)
-}
-
 createParticipant = async (req, res) => {
   const groupId = req.params.groupId
   const caseId = req.params.caseId
@@ -252,8 +207,6 @@ module.exports = {
   readCase,
   createCaseEvent,
   createEventForm,
-  readEventForm,
-  updateEventForm,
   createParticipant,
   getCaseEventFormSurveyLinks
 }
\ No newline at end of file
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index c9f146936e..a19d0d8b15 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -58,23 +58,8 @@ class CaseEvent {
 }
 
 class EventForm {
-
-  id;
-  caseId;
-  caseEventId;
-  eventFormDefinitionId;
-  required;
-  participantId;
-  complete;
-  inactive;
-  formResponseId;
-
-  constructor(data) {
-    Object.assign(this, data);
-  }
-
   constructor(caseId, caseEventId, eventFormDefinition = null) {
-    this.id = id || uuidV4()
+    this.id = uuidV4()
     this.caseId = caseId
     this.caseEventId = caseEventId
     this.eventFormDefinitionId = eventFormDefinition.id
diff --git a/server/src/express-app.js b/server/src/express-app.js
index 5e0c494213..839b5ce009 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -55,8 +55,6 @@ const {
   readCase,
   createCaseEvent,
   createEventForm,
-  readEventForm,
-  updateEventForm,
   createParticipant,
   getCaseEventFormSurveyLinks
 } = require('./case-api')
@@ -209,8 +207,6 @@ app.post('/case/createCase/:groupId/:caseDefinitionId', isAuthenticated, createC
 app.post('/case/readCase/:groupId/:caseId', isAuthenticated, readCase);
 app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', isAuthenticated, createCaseEvent);
 app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', isAuthenticated, createEventForm);
-app.get('/case/readEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, readEventForm);
-app.post('/case/updateEventForm/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, updateEventForm);
 app.post('/case/createParticipant/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, createParticipant);
 app.get('/case/getCaseEventFormSurveyLinks/:groupId/:caseId', isAuthenticated, getCaseEventFormSurveyLinks);
 

From 1853c34dc8b94f108b4701b629e0982dd793a3de Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 8 Jun 2024 17:20:10 -0400
Subject: [PATCH 19/59] Update /case/createParticipant API

---
 server/src/case-api.js           | 26 ++++++++++++++++----------
 server/src/classes/case.class.js | 27 +++++++++++++++++++++------
 server/src/express-app.js        |  2 +-
 3 files changed, 38 insertions(+), 17 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 36fd03bc3a..86ca0de845 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -3,7 +3,12 @@ const log = require('tangy-log').log
 const path = require('path')
 const fs = require('fs')
 
-const {Case: Case, CaseEvent: CaseEvent, EventForm: EventForm } = require('./classes/case.class.js')
+const {
+  Case: Case,
+  CaseEvent: CaseEvent,
+  EventForm: EventForm,
+  Participant: Participant
+} = require('./classes/case.class.js')
 
 /* Return the contents of the case-definitions.json file or a sepcified */
 getCaseDefinitions = async (req, res) => {
@@ -142,21 +147,22 @@ createEventForm = async (req, res) => {
 }
 
 createParticipant = async (req, res) => {
-  const groupId = req.params.groupId
-  const caseId = req.params.caseId
-  const caseRoleId = req.params.caseRoleId
+  try {
+    const groupId = req.params.groupId
+    const caseId = req.params.caseId
+    const caseDefinitionId = req.params.caseDefinitionId
+    const caseRoleId = req.params.caseRoleId
 
-  const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
-  const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
+    const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
+    const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
 
-  let participant = new Participant(groupId, caseId, caseRole)
+    let participant = new Participant(caseRole)
 
-  try {
     const db = new DB(groupId)
     const caseDoc = await db.get(caseId);
-    caseDoc.participant.push(participant);
+    caseDoc.participants.push(participant);
     await db.put(caseDoc);
-    res.send(eventForm._id);
+    res.send(participant.id);
   } catch (err) {
     res.status(500).send
   }
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index a19d0d8b15..e227950493 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -44,6 +44,7 @@ class CaseEvent {
     this.complete = false
     this.inactive = false
     this.eventForms = []
+    this.data = []
 
     for (const eventForm of eventDefinition.eventFormDefinitions) {
       if (eventForm.required) {
@@ -53,7 +54,11 @@ class CaseEvent {
   }
 
   addData(data) {
-    this.data = data
+    if (Array.isArray(data)) {
+      this.data.push(...data)
+    } else {
+      this.data.push(data)
+    }
   }
 }
 
@@ -67,23 +72,33 @@ class EventForm {
     this.participantId = ''
     this.complete = false
     this.inactive = false
+    this.data = [];
   }
 
   addData(data) {
-    this.data = data
+    if (Array.isArray(data)) {
+      this.data.push(...data)
+    } else {
+      this.data.push(data)
+    }
   }
 }
 
 class Participant {
-  constructor(participantDefinition) {
+  constructor(caseRole) {
     this.id = uuidV4()
-    this.caseRoleId = participantDefinition.id
-    this.name = ''
+    this.caseRoleId = caseRole.id
+    this.name = caseRole.label
     this.inactive = false;
+    this.data = [];
   }
 
   addData(data) {
-    this.data = data
+    if (Array.isArray(data)) {
+      this.data.push(...data)
+    } else {
+      this.data.push(data)
+    }
   }
 }
 
diff --git a/server/src/express-app.js b/server/src/express-app.js
index 839b5ce009..2fa9cb5239 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -207,7 +207,7 @@ app.post('/case/createCase/:groupId/:caseDefinitionId', isAuthenticated, createC
 app.post('/case/readCase/:groupId/:caseId', isAuthenticated, readCase);
 app.post('/case/createCaseEvent/:groupId/:caseId/:caseEventDefinitionId', isAuthenticated, createCaseEvent);
 app.post('/case/createEventForm/:groupId/:caseId/:caseEventId/:caseEventFormDefinitionId', isAuthenticated, createEventForm);
-app.post('/case/createParticipant/:groupId/:caseId/:caseEventId/:eventFormId', isAuthenticated, createParticipant);
+app.post('/case/createParticipant/:groupId/:caseId/:caseDefinitionId/:caseRoleId', isAuthenticated, createParticipant);
 app.get('/case/getCaseEventFormSurveyLinks/:groupId/:caseId', isAuthenticated, getCaseEventFormSurveyLinks);
 
 /**

From 901ea4ca09730a26728b734fa33a999dc485560b Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 8 Jun 2024 18:11:52 -0400
Subject: [PATCH 20/59] Add case service set context and caseSErvice window var
 for online survey app

---
 .../tangy-forms-player.component.ts           | 44 +++++++++++--------
 1 file changed, 25 insertions(+), 19 deletions(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 4aed33dc0e..5a2666ed28 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -37,36 +37,42 @@ export class TangyFormsPlayerComponent implements OnInit {
     });
   }
 
-  async ngOnInit(): Promise<any> {
-    this.tangyFormService.initialize(window.location.pathname.split('/')[4]);
+  async ngOnInit(): Promise<any> {   
+    const groupId = window.location.pathname.split('/')[4]; 
+    this.tangyFormService.initialize(groupId);
+
     this.window = window;
-    this.window.T = {
-      case: this.caseService,
-      tangyForms: this.tangyFormService
-    }
 
     // Loading the formResponse must happen before rendering the innerHTML
     let formResponse;
     if (this.caseId) {
       try {
         await this.caseService.load(this.caseId);
-      } catch (error) {
-        console.log('Error loading case: ' + error)
-      }
+        this.caseService.setContext(this.caseEventId, this.eventFormId)
 
-      if (this.eventFormId) {
-        try {
-          // Attempt to load the form response for the event form
-          const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
-          if (event.id) {
-            const eventForm = event.eventForms.find(eventForm => eventForm.id === this.eventFormId);
-              if (eventForm && eventForm.id === this.eventFormId && eventForm.formResponseId) {
-                formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId);
+        this.window.T = {
+          case: this.caseService,
+          tangyForms: this.tangyFormService
+        }
+        this.window.caseService = this.caseService
+
+        if (this.eventFormId) {
+          try {
+            // Attempt to load the form response for the event form
+            const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
+            if (event.id) {
+              const eventForm = event.eventForms.find(eventForm => eventForm.id === this.eventFormId);
+                if (eventForm && eventForm.id === this.eventFormId && eventForm.formResponseId) {
+                  formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId);
+              }
             }
+          } catch (error) {
+            //pass
           }
-        } catch (error) {
-          //pass
         }
+
+      } catch (error) {
+        console.log('Error loading case: ' + error)
       }
     }
 

From 3640ef6ce0089c271259cacd2f193bdafccccf73 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 8 Jun 2024 18:12:13 -0400
Subject: [PATCH 21/59] Fix bugs in case-api and case class

---
 server/src/case-api.js           | 4 ++--
 server/src/classes/case.class.js | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 86ca0de845..27048436b5 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -105,7 +105,7 @@ createCaseEvent = async (req, res) => {
   
   const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
   let caseEventDefinition = caseDefinition.eventDefinitions.find((e) => e.id === caseEventDefinitionId)
-  let caseEvent = new CaseEvent(groupId, caseEventDefinition)
+  let caseEvent = new CaseEvent(caseId, caseEventDefinition)
 
   let caseDoc;
   try {
@@ -132,7 +132,7 @@ createEventForm = async (req, res) => {
       if (eventFormDefinition) break;
   }
   
-  let eventForm = new EventForm(groupId, caseId, caseEventId, eventFormDefinition)
+  let eventForm = new EventForm(caseId, caseEventId, eventFormDefinition)
 
   try {
     const db = new DB(groupId)
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index e227950493..3b9d8ad7c1 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -46,9 +46,9 @@ class CaseEvent {
     this.eventForms = []
     this.data = []
 
-    for (const eventForm of eventDefinition.eventFormDefinitions) {
-      if (eventForm.required) {
-        this.eventForms.push(new EventForm(caseId, this._id, eventForm))
+    for (const eventFormDefinition of eventDefinition.eventFormDefinitions) {
+      if (eventFormDefinition.required) {
+        this.eventForms.push(new EventForm(caseId, this._id, eventFormDefinition))
       }
     }
   }
@@ -63,7 +63,7 @@ class CaseEvent {
 }
 
 class EventForm {
-  constructor(caseId, caseEventId, eventFormDefinition = null) {
+  constructor(caseId, caseEventId, eventFormDefinition) {
     this.id = uuidV4()
     this.caseId = caseId
     this.caseEventId = caseEventId

From 78c20acc8e1ff1163a8c8a0bc8d6ad28d693ee89 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 8 Jun 2024 18:56:39 -0400
Subject: [PATCH 22/59] Add eventFormRedirect to online surveys

---
 .../tangy-forms-player.component.ts           | 40 +++++++++++--------
 1 file changed, 24 insertions(+), 16 deletions(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 5a2666ed28..334be7dcc2 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -45,7 +45,7 @@ export class TangyFormsPlayerComponent implements OnInit {
 
     // Loading the formResponse must happen before rendering the innerHTML
     let formResponse;
-    if (this.caseId) {
+    if (this.caseId && this.caseEventId && this.eventFormId) {
       try {
         await this.caseService.load(this.caseId);
         this.caseService.setContext(this.caseEventId, this.eventFormId)
@@ -56,19 +56,17 @@ export class TangyFormsPlayerComponent implements OnInit {
         }
         this.window.caseService = this.caseService
 
-        if (this.eventFormId) {
-          try {
-            // Attempt to load the form response for the event form
-            const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
-            if (event.id) {
-              const eventForm = event.eventForms.find(eventForm => eventForm.id === this.eventFormId);
-                if (eventForm && eventForm.id === this.eventFormId && eventForm.formResponseId) {
-                  formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId);
-              }
+        try {
+          // Attempt to load the form response for the event form
+          const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
+          if (event.id) {
+            const eventForm = event.eventForms.find(eventForm => eventForm.id === this.eventFormId);
+              if (eventForm && eventForm.id === this.eventFormId && eventForm.formResponseId) {
+                formResponse = await this.tangyFormService.getResponse(eventForm.formResponseId);
             }
-          } catch (error) {
-            //pass
           }
+        } catch (error) {
+          //pass
         }
 
       } catch (error) {
@@ -88,16 +86,15 @@ export class TangyFormsPlayerComponent implements OnInit {
       event.preventDefault();
       const formResponse = event.target.response;
       try {
-        if (await this.formsService.uploadFormResponse(formResponse)) {
-          this.router.navigate(['/form-submitted-success']);
-        } else {
+        if (!await this.formsService.uploadFormResponse(formResponse)) {
           alert('Form could not be submitted. Please retry');
+          return;
         }
       } catch (error) {
         console.error(error);
       }
 
-      if (this.eventFormId) {
+      if (this.caseId && this.caseEventId && this.eventFormId) {
         try {
           const caseEvent = this.caseService.case.events.find(event => event.id === this.caseEventId);
           if (caseEvent.id) {
@@ -112,6 +109,17 @@ export class TangyFormsPlayerComponent implements OnInit {
           console.error(error);
         }
       }
+      this.router.navigate(['/form-submitted-success']);
+
+      if (window['eventFormRedirect']) {
+        try {
+          // this.router.navigateByUrl(window['eventFormRedirect']) -- TODO figure this out later
+          this.window['location'] = window['eventFormRedirect']
+          window['eventFormRedirect'] = ''
+        } catch (error) {
+          console.error(error);
+        }
+      }
     });
   }
 }

From 992c76ff391422aa6a7442e8b8556984686d732e Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 10 Jun 2024 12:42:29 -0400
Subject: [PATCH 23/59] case api getEventFormSurveyLinks outputs a JSON object
 with list of links and metadata

---
 server/src/case-api.js | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index 27048436b5..c18c513171 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -184,8 +184,9 @@ getCaseEventFormSurveyLinks = async (req, res) => {
 
     for (let event of caseDoc.events) {
       for (let eventForm of event.eventForms.filter((f) => !f.formResponseId)) {
-        for (let eventDefinition of caseDefinition.eventDefinitions) {
-          let eventFormDefinition = eventDefinition.eventFormDefinitions.find((e) => e.id === eventForm.eventFormDefinitionId)
+        const eventDefinition = caseDefinition.eventDefinitions.find((e) => e.id === event.caseEventDefinitionId)
+        if (eventDefinition) {
+          const eventFormDefinition = eventDefinition.eventFormDefinitions.find((e) => e.id === eventForm.eventFormDefinitionId)
           if (eventFormDefinition && eventFormDefinition.formId) {
             const formId = eventFormDefinition.formId
             const survey = onlineSurveys.find((s) => s.formId === formId && s.published === true && s.locked === true)
@@ -194,7 +195,14 @@ getCaseEventFormSurveyLinks = async (req, res) => {
               const pathname = `releases/prod/online-survey-apps/${groupId}/${formId}`
               const hash = `#/case/event/form/${caseId}/${event.id}/${eventForm.id}`
               const url = `${origin}/${pathname}/${hash}`
-              data.push(url)
+              data.push(
+                {
+                  "eventDefinitionId": event.caseEventDefinitionId,
+                  "eventFormDefinitionId": eventForm.eventFormDefinitionId,
+                  "formId": formId,
+                  "url": url
+                }
+              )
             }
           }
         }

From dca07866344c17d3be2584a518317cdfa87c2ca0 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 11 Jun 2024 12:04:48 -0400
Subject: [PATCH 24/59] Fix setting event id in case creation api

---
 client/src/app/core/sync-records/_services/syncing.service.ts | 2 +-
 .../event-form-list-item/event-form-list-item.component.ts    | 4 +++-
 server/src/classes/case.class.js                              | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/src/app/core/sync-records/_services/syncing.service.ts b/client/src/app/core/sync-records/_services/syncing.service.ts
index f1b87f51a1..aba43b5385 100755
--- a/client/src/app/core/sync-records/_services/syncing.service.ts
+++ b/client/src/app/core/sync-records/_services/syncing.service.ts
@@ -61,7 +61,7 @@ export class SyncingService {
           doc['items'][0]['inputs'].push({ name: 'tabletUserName', value: username });
           // Redact any fields marked as private.
           doc['items'].forEach(item => {
-            item['inputs'].forEach(input => {
+          item['inputs'].forEach(input => {
               if (input.private) {
                 input.value = '';
               }
diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 46e16b0579..bb0e72a63e 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -75,7 +75,9 @@ export class EventFormListItemComponent implements OnInit {
       this.eventFormDefinition.allowOnline
     );
 
-    this.surveyLinkUrl = `/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
+    if (this.canLinkToOnlineSurvey) {
+      this.surveyLinkUrl = `/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.case._id}/${this.caseEvent.id}/${this.eventForm.id}`;
+    }
 
     this.canUserDeleteForms = ((this.eventFormDefinition.allowDeleteIfFormNotCompleted && !this.eventForm.complete)
     || (this.eventFormDefinition.allowDeleteIfFormNotStarted && !this.eventForm.formResponseId));
diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index 3b9d8ad7c1..01abcdbd22 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -48,7 +48,7 @@ class CaseEvent {
 
     for (const eventFormDefinition of eventDefinition.eventFormDefinitions) {
       if (eventFormDefinition.required) {
-        this.eventForms.push(new EventForm(caseId, this._id, eventFormDefinition))
+        this.eventForms.push(new EventForm(caseId, this.id, eventFormDefinition))
       }
     }
   }

From eaa0bbb6687b3357ba5abbd3e233ea0b2ca8e8a8 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 11 Jun 2024 14:04:16 -0400
Subject: [PATCH 25/59] Fix typo in content-set.md

---
 docs/editor/content-sets.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/editor/content-sets.md b/docs/editor/content-sets.md
index 1acbe44eb1..7ebf536c02 100644
--- a/docs/editor/content-sets.md
+++ b/docs/editor/content-sets.md
@@ -28,7 +28,7 @@ Be sure to update any cron jobs to include the new build commands if they are us
 
 ``
 git pull
-npm rn install-server
+npm run install-server
 npm build
 ``
 

From 26e197fd38ae919ddb3aef153925ecc0c9a05d30 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 11 Jun 2024 14:10:05 -0400
Subject: [PATCH 26/59] Event form link copy not allowed with http

---
 .../event-form-list-item.component.ts              | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index bb0e72a63e..0d0eace5d8 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -156,9 +156,17 @@ export class EventFormListItemComponent implements OnInit {
 
   onCopyLinkClick() {
     const url = `${window.location.origin}/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
-    navigator.clipboard.writeText(url);
-
-    this.tangyErrorHandler.handleError('Online Survey link copied to clipboard');
+    try {
+      navigator.clipboard.writeText(url);
+
+      this.tangyErrorHandler.handleError('Online Survey link copied to clipboard');
+    } catch (err) {
+      let errMsg = 'Failed to copy link to clipboard';
+      if (window.location.origin.startsWith('http://')) {
+        errMsg = 'Copy to clipboard is not supported with http://.';
+      }
+      this.tangyErrorHandler.handleError(errMsg);
+    }
   }
 
   onQRCodeLinkClick() {

From e65a88d4b3e446a91f9b7ae5ede5b9debd986095 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 11 Jun 2024 14:10:47 -0400
Subject: [PATCH 27/59] Add case, event and form info to response

---
 .../tangy-forms-player/tangy-forms-player.component.ts       | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 334be7dcc2..1efc08d0ff 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -85,6 +85,11 @@ export class TangyFormsPlayerComponent implements OnInit {
     tangyForm.addEventListener('after-submit', async (event) => {
       event.preventDefault();
       const formResponse = event.target.response;
+      if (this.caseId && this.caseEventId && this.eventFormId) {
+        formResponse.caseId = this.caseId;
+        formResponse.caseEventId = this.caseEventId;
+        formResponse.eventFormId = this.eventFormId;
+      }
       try {
         if (!await this.formsService.uploadFormResponse(formResponse)) {
           alert('Form could not be submitted. Please retry');

From 9f400018b15dbe972b182a831af8ec17f9fffac6 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 11 Jun 2024 16:42:48 -0400
Subject: [PATCH 28/59] Build custom-scripts when releasing online app

---
 server/src/scripts/release-online-survey-app.sh | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index 091901ce57..eb825bf98a 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -80,6 +80,12 @@ if [ -f "$CASE_DEFINITIONS" ]; then
   done < $CASE_DEFINITIONS
 fi
 
+if [ -f "/tangerine/groups/$GROUP/package.json" ]; then
+  cd "/tangerine/groups/$GROUP/"
+  npm run install-server && npm run build
+  cd /
+fi
+
 FORM_UPLOAD_URL="/onlineSurvey/saveResponse/$GROUP_ID/$FORM_ID"
 
 if [[ $REQUIRE_ACCESS_CODE == 'true' ]]; then

From eaebe5129aeec06bbf0cd6d5a2682c49da61a70b Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 03:59:50 -0400
Subject: [PATCH 29/59] use surveyLinkUrl in event form list item

---
 .../event-form-list-item/event-form-list-item.component.ts      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 0d0eace5d8..1946311578 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -155,7 +155,7 @@ export class EventFormListItemComponent implements OnInit {
   }
 
   onCopyLinkClick() {
-    const url = `${window.location.origin}/releases/prod/online-survey-apps/${this.groupId}/${this.eventFormDefinition.formId}/#/case/event/form/${this.eventForm.caseId}/${this.eventForm.caseEventId}/${this.eventForm.id}`;
+    const url = `${window.location.origin}/${this.surveyLinkUrl}`;
     try {
       navigator.clipboard.writeText(url);
 

From ac3d58b3b33de6427632e6910ceb47ef044037e0 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 04:44:43 -0400
Subject: [PATCH 30/59] Create participant event forms when adding participant
 through api

---
 server/src/classes/case.class.js | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index 01abcdbd22..3bb83bd38f 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -47,7 +47,7 @@ class CaseEvent {
     this.data = []
 
     for (const eventFormDefinition of eventDefinition.eventFormDefinitions) {
-      if (eventFormDefinition.required) {
+      if (eventFormDefinition.required && eventFormDefinition.forCaseRole == '') {
         this.eventForms.push(new EventForm(caseId, this.id, eventFormDefinition))
       }
     }
@@ -63,13 +63,13 @@ class CaseEvent {
 }
 
 class EventForm {
-  constructor(caseId, caseEventId, eventFormDefinition) {
+  constructor(caseId, caseEventId, eventFormDefinition, participantId) {
     this.id = uuidV4()
     this.caseId = caseId
     this.caseEventId = caseEventId
     this.eventFormDefinitionId = eventFormDefinition.id
     this.required = eventFormDefinition.required
-    this.participantId = ''
+    this.participantId = participantId || ''
     this.complete = false
     this.inactive = false
     this.data = [];
@@ -85,12 +85,25 @@ class EventForm {
 }
 
 class Participant {
-  constructor(caseRole) {
+  constructor(caseRole, caseInstance='', caseDefinition='') {
     this.id = uuidV4()
     this.caseRoleId = caseRole.id
     this.name = caseRole.label
     this.inactive = false;
     this.data = [];
+
+    if (caseInstance) {
+      for (let event of caseInstance.events) {
+        const eventDefinition = caseDefinition.eventDefinitions.find((e) => e.id === event.caseEventDefinitionId)
+        if (eventDefinition) {
+          const eventFormsForRole = eventDefinition.eventFormDefinitions.filter((f) => f.required && f.forCaseRole === caseRole.id)
+          for (const eventFormDefinition of eventFormsForRole) {
+            event.eventForms.push(new EventForm(caseInstance._id, event.id, eventFormDefinition, this.id))
+          }
+        }
+      }
+      caseInstance.participants.push(this)
+    }
   }
 
   addData(data) {

From 697a9f1717d342b2269d8de5fd9e03e812b65b9e Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 04:52:08 -0400
Subject: [PATCH 31/59] Set event and form name from definition in creation api

---
 server/src/classes/case.class.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/server/src/classes/case.class.js b/server/src/classes/case.class.js
index 3bb83bd38f..7000fe9db5 100644
--- a/server/src/classes/case.class.js
+++ b/server/src/classes/case.class.js
@@ -39,7 +39,7 @@ class CaseEvent {
   constructor(caseId, eventDefinition) {
     this.id = uuidV4();
     this.caseId = caseId
-    this.name = ''
+    this.name = eventDefinition.name || ''
     this.caseEventDefinitionId = eventDefinition.id
     this.complete = false
     this.inactive = false
@@ -66,6 +66,7 @@ class EventForm {
   constructor(caseId, caseEventId, eventFormDefinition, participantId) {
     this.id = uuidV4()
     this.caseId = caseId
+    this.name = eventFormDefinition.name || ''
     this.caseEventId = caseEventId
     this.eventFormDefinitionId = eventFormDefinition.id
     this.required = eventFormDefinition.required

From 45ed8ee46d5c65d6d2d85f5e82bf27ef45f77f29 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 05:52:57 -0400
Subject: [PATCH 32/59] Add helpLink config for online-survey to enable a
 custom link from an online form

---
 online-survey-app/src/app/app.component.html  |  7 +++++
 online-survey-app/src/app/app.component.ts    |  7 +++++
 .../src/app/shared/classes/app-config.ts      |  1 +
 .../src/scripts/release-online-survey-app.sh  | 27 ++++++++++++++-----
 4 files changed, 36 insertions(+), 6 deletions(-)

diff --git a/online-survey-app/src/app/app.component.html b/online-survey-app/src/app/app.component.html
index 98b3bf9360..a7df3ffd12 100644
--- a/online-survey-app/src/app/app.component.html
+++ b/online-survey-app/src/app/app.component.html
@@ -2,6 +2,13 @@
   <table>
     <tr>
       <td width="100%"></td>
+      <td>
+        <div *ngIf="hasHelpLink">
+          <a href="{{helpLink}}" target="_blank">
+            <iron-icon icon="help"></iron-icon>
+          </a>
+        </div>
+      </td>
       <td>
         <iron-icon icon="language"></iron-icon>
       </td>
diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index 3d856bcc62..222e181669 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -10,6 +10,8 @@ import { CaseService } from './case/services/case.service';
 export class AppComponent implements OnInit{
   languageDirection: string;
   appName: string;
+  hasHelpLink: boolean = false;
+  helpLink: string;
   window: any;
 
   constructor(private appConfigService: AppConfigService, private caseService: CaseService){
@@ -21,6 +23,11 @@ export class AppComponent implements OnInit{
       this.appName = appConfig.appName;
       this.languageDirection = appConfig.languageDirection;
 
+      if (appConfig.helpLink) {
+        this.hasHelpLink = true;
+        this.helpLink = appConfig.helpLink;
+      }
+
     } catch (error) {
       this.appName = '';
       this.languageDirection = 'ltr';
diff --git a/online-survey-app/src/app/shared/classes/app-config.ts b/online-survey-app/src/app/shared/classes/app-config.ts
index 1ff7efb215..4c2ec15cee 100644
--- a/online-survey-app/src/app/shared/classes/app-config.ts
+++ b/online-survey-app/src/app/shared/classes/app-config.ts
@@ -2,6 +2,7 @@ export class AppConfig {
     appName: string;
     languageCode: string;
     languageDirection: string;
+    helpLink: string;
     formUploadURL: string;
     uploadKey:string
     groupId: string;
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index eb825bf98a..e4f1f866de 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -41,6 +41,7 @@ MEDIA_DIRECTORY="$FORM_CLIENT_DIRECTORY/media"
 CUSTOM_SCRIPTS="$FORM_CLIENT_DIRECTORY/custom-scripts.js"
 CUSTOM_SCRIPTS_MAP="$FORM_CLIENT_DIRECTORY/custom-scripts.js.map"
 CUSTOM_LOGIN_MARKUP="$FORM_CLIENT_DIRECTORY/custom-login-markup.html"
+APP_CONFIG="$FORM_CLIENT_DIRECTORY/app-config.json"
 
 # Set up the release dir from the dist
 cp -r /tangerine/online-survey-app/dist/online-survey-app/ $RELEASE_DIRECTORY
@@ -94,11 +95,25 @@ else
   REQUIRE_ACCESS_CODE="false"
 fi
 
-# NOTE: App Config does NOT come from the app-config.json file in the release directory
-sed -i -e "s#GROUP_ID#"$GROUP_ID"#g" $RELEASE_DIRECTORY/assets/app-config.json
-sed -i -e "s#FORM_UPLOAD_URL#"$FORM_UPLOAD_URL"#g" $RELEASE_DIRECTORY/assets/app-config.json
-sed -i -e "s#UPLOAD_KEY#"$UPLOAD_KEY"#g" $RELEASE_DIRECTORY/assets/app-config.json
-sed -i -e "s#APP_NAME#"$APP_NAME"#g" $RELEASE_DIRECTORY/assets/app-config.json
-sed -i -e "s#REQUIRE_ACCESS_CODE#"$REQUIRE_ACCESS_CODE"#g" $RELEASE_DIRECTORY/assets/app-config.json
+# Copy the app-config.json to the release directory
+cp $APP_CONFIG $RELEASE_DIRECTORY/assets/
+
+# first delete the last line of the file with the closing bracket
+sed -i '$ d' $RELEASE_DIRECTORY/assets/app-config.json
+
+#append a comma to the end of the last line
+sed -i '$ s/$/,/' $RELEASE_DIRECTORY/assets/app-config.json
+
+#then append the online survey configs
+echo "\"formUploadURL\": \"$FORM_UPLOAD_URL\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"uploadKey\": \"$UPLOAD_KEY\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"groupId\": \"$GROUP_ID\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"languageDirection\": \"ltr\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"languageCode\": \"en\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"appName\": \"$APP_NAME\"," >> $RELEASE_DIRECTORY/assets/app-config.json
+echo "\"requireAccessCode\": $REQUIRE_ACCESS_CODE" >> $RELEASE_DIRECTORY/assets/app-config.json
+
+# finaly, add the closing bracket
+echo "}" >> $RELEASE_DIRECTORY/assets/app-config.json
 
 echo "Release with UUID of $UUID to $RELEASE_DIRECTORY with Build ID of $BUILD_ID, channel of $RELEASE_TYPE"

From ed615fc02499e103f89e51aabce361c6aa260985 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 09:29:45 -0400
Subject: [PATCH 33/59] Fix creation of event forms for participant role in
 case api

---
 server/src/case-api.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/server/src/case-api.js b/server/src/case-api.js
index c18c513171..6572b77e26 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -156,11 +156,11 @@ createParticipant = async (req, res) => {
     const caseDefinition =  _getCaseDefinition(groupId, caseDefinitionId)
     const caseRole = caseDefinition.caseRoles.find((r) => r.id === caseRoleId)
 
-    let participant = new Participant(caseRole)
-
     const db = new DB(groupId)
     const caseDoc = await db.get(caseId);
-    caseDoc.participants.push(participant);
+
+    let participant = new Participant(caseRole, caseDoc, caseDefinition)
+
     await db.put(caseDoc);
     res.send(participant.id);
   } catch (err) {

From 175787ea50c7c56f859bc4bb23fca1bcc1126d26 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 12 Jun 2024 13:29:49 -0400
Subject: [PATCH 34/59] Revert addition of all client/app-config.json; only set
 helpLink

---
 online-survey-app/src/assets/app-config.json  |  3 +-
 .../src/scripts/release-online-survey-app.sh  | 34 +++++++------------
 2 files changed, 15 insertions(+), 22 deletions(-)

diff --git a/online-survey-app/src/assets/app-config.json b/online-survey-app/src/assets/app-config.json
index 58d107cffe..df0647755d 100644
--- a/online-survey-app/src/assets/app-config.json
+++ b/online-survey-app/src/assets/app-config.json
@@ -5,5 +5,6 @@
   "languageDirection": "ltr",
   "languageCode": "en",
   "appName":"APP_NAME",
-  "requireAccessCode": "REQUIRE_ACCESS_CODE"
+  "requireAccessCode": "REQUIRE_ACCESS_CODE",
+  "helpLink": "HELP_LINK"
 }
\ No newline at end of file
diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index e4f1f866de..c19ba68f86 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -41,7 +41,6 @@ MEDIA_DIRECTORY="$FORM_CLIENT_DIRECTORY/media"
 CUSTOM_SCRIPTS="$FORM_CLIENT_DIRECTORY/custom-scripts.js"
 CUSTOM_SCRIPTS_MAP="$FORM_CLIENT_DIRECTORY/custom-scripts.js.map"
 CUSTOM_LOGIN_MARKUP="$FORM_CLIENT_DIRECTORY/custom-login-markup.html"
-APP_CONFIG="$FORM_CLIENT_DIRECTORY/app-config.json"
 
 # Set up the release dir from the dist
 cp -r /tangerine/online-survey-app/dist/online-survey-app/ $RELEASE_DIRECTORY
@@ -95,25 +94,18 @@ else
   REQUIRE_ACCESS_CODE="false"
 fi
 
-# Copy the app-config.json to the release directory
-cp $APP_CONFIG $RELEASE_DIRECTORY/assets/
-
-# first delete the last line of the file with the closing bracket
-sed -i '$ d' $RELEASE_DIRECTORY/assets/app-config.json
-
-#append a comma to the end of the last line
-sed -i '$ s/$/,/' $RELEASE_DIRECTORY/assets/app-config.json
-
-#then append the online survey configs
-echo "\"formUploadURL\": \"$FORM_UPLOAD_URL\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"uploadKey\": \"$UPLOAD_KEY\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"groupId\": \"$GROUP_ID\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"languageDirection\": \"ltr\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"languageCode\": \"en\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"appName\": \"$APP_NAME\"," >> $RELEASE_DIRECTORY/assets/app-config.json
-echo "\"requireAccessCode\": $REQUIRE_ACCESS_CODE" >> $RELEASE_DIRECTORY/assets/app-config.json
-
-# finaly, add the closing bracket
-echo "}" >> $RELEASE_DIRECTORY/assets/app-config.json
+# NOTE: App Config does NOT come from the app-config.json file in the release directory
+sed -i -e "s#GROUP_ID#"$GROUP_ID"#g" $RELEASE_DIRECTORY/assets/app-config.json
+sed -i -e "s#FORM_UPLOAD_URL#"$FORM_UPLOAD_URL"#g" $RELEASE_DIRECTORY/assets/app-config.json
+sed -i -e "s#UPLOAD_KEY#"$UPLOAD_KEY"#g" $RELEASE_DIRECTORY/assets/app-config.json
+sed -i -e "s#APP_NAME#"$APP_NAME"#g" $RELEASE_DIRECTORY/assets/app-config.json
+sed -i -e "s#REQUIRE_ACCESS_CODE#"$REQUIRE_ACCESS_CODE"#g" $RELEASE_DIRECTORY/assets/app-config.json
+
+# copy the HELP_LINK from client/app-config.json to assets/app-config.json
+HELP_LINK=$(jq -r '.helpLink' $FORM_CLIENT_DIRECTORY/app-config.json)
+if [ "$HELP_LINK" = "null" ]; then
+  HELP_LINK=""
+fi
+jq '.helpLink = "'$HELP_LINK'"' $RELEASE_DIRECTORY/assets/app-config.json > tmp.$$.json && mv tmp.$$.json $RELEASE_DIRECTORY/assets/app-config.json
 
 echo "Release with UUID of $UUID to $RELEASE_DIRECTORY with Build ID of $BUILD_ID, channel of $RELEASE_TYPE"

From 8c2dbc6e4ffaf75111893fdbab4529915a7a5dac Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 14:03:53 -0400
Subject: [PATCH 35/59] Fix GROUP_ID in release online survey app

---
 server/src/scripts/release-online-survey-app.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/src/scripts/release-online-survey-app.sh b/server/src/scripts/release-online-survey-app.sh
index c19ba68f86..6ff075200b 100755
--- a/server/src/scripts/release-online-survey-app.sh
+++ b/server/src/scripts/release-online-survey-app.sh
@@ -80,8 +80,8 @@ if [ -f "$CASE_DEFINITIONS" ]; then
   done < $CASE_DEFINITIONS
 fi
 
-if [ -f "/tangerine/groups/$GROUP/package.json" ]; then
-  cd "/tangerine/groups/$GROUP/"
+if [ -f "/tangerine/groups/$GROUP_ID/package.json" ]; then
+  cd "/tangerine/groups/$GROUP_ID/"
   npm run install-server && npm run build
   cd /
 fi

From 981d49b5ddf7a7719324144ede3e5ec46b0c01f6 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 14:05:26 -0400
Subject: [PATCH 36/59] Add timeout and logout to online survey

---
 online-survey-app/src/app/app.component.ts    | 52 +++++++++++++++++--
 .../auth/_services/authentication.service.ts  | 16 ++++++
 2 files changed, 64 insertions(+), 4 deletions(-)

diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index 222e181669..e35b71d63e 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -1,6 +1,8 @@
 import { Component, OnInit } from '@angular/core';
 import { AppConfigService } from './shared/_services/app-config.service';
-import { CaseService } from './case/services/case.service';
+import { _TRANSLATE } from './shared/_services/translation-marker';
+import { AuthenticationService } from './core/auth/_services/authentication.service';
+import { Router } from '@angular/router';
 
 @Component({
   selector: 'app-root',
@@ -12,17 +14,34 @@ export class AppComponent implements OnInit{
   appName: string;
   hasHelpLink: boolean = false;
   helpLink: string;
-  window: any;
+  sessionTimeoutCheckTimerID;
+  isConfirmDialogActive = false;
+  loggedIn = false;
 
-  constructor(private appConfigService: AppConfigService, private caseService: CaseService){
+  constructor(
+    private appConfigService: AppConfigService, 
+    private authenticationService: AuthenticationService,
+    private router: Router
+  ){
   }
 
   async ngOnInit(): Promise<any>{
+    this.authenticationService.currentUserLoggedIn$.subscribe(async isLoggedIn => {
+      if (isLoggedIn) {
+        this.loggedIn = isLoggedIn;
+        this.sessionTimeoutCheck();
+        this.sessionTimeoutCheckTimerID =
+        setInterval(await this.sessionTimeoutCheck.bind(this), 10 * 60 * 1000); // check every 10 minutes
+      } else {
+        this.loggedIn = false;
+        this.router.navigate(['/survey-login']);
+      }
+    });
+
     try {
       const appConfig = await this.appConfigService.getAppConfig();
       this.appName = appConfig.appName;
       this.languageDirection = appConfig.languageDirection;
-
       if (appConfig.helpLink) {
         this.hasHelpLink = true;
         this.helpLink = appConfig.helpLink;
@@ -31,6 +50,31 @@ export class AppComponent implements OnInit{
     } catch (error) {
       this.appName = '';
       this.languageDirection = 'ltr';
+      this.helpLink = '';
+    }
+  }
+  
+  async sessionTimeoutCheck() {
+    const token = localStorage.getItem('token');
+    const claims = JSON.parse(atob(token.split('.')[1]));
+    const expiryTimeInMs = claims['exp'] * 1000;
+    const minutesBeforeExpiry = expiryTimeInMs - (15 * 60 * 1000); // warn 15 minutes before expiry of token
+    if (Date.now() >= minutesBeforeExpiry && !this.isConfirmDialogActive) {
+      this.isConfirmDialogActive = true;
+      const extendSession = confirm(_TRANSLATE('You are about to be logged out. Should we extend your session?'));
+      if (extendSession) {
+        await this.authenticationService.extendUserSession();
+        this.isConfirmDialogActive = false;
+      } else {
+        await this.logout();
+      }
     }
   }
+
+  async logout() {
+    clearInterval(this.sessionTimeoutCheckTimerID);
+    await this.authenticationService.logout();
+    this.loggedIn = false;
+    this.router.navigate(['/survey-login']);
+  }
 }
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 8aca5f2adb..9a7a40be09 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -70,6 +70,22 @@ export class AuthenticationService {
     this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
   }
 
+  async extendUserSession() {
+    const username = localStorage.getItem('user_id');
+    try {
+      const data = await this.http.post('/extendSession', {username}, {observe: 'response'}).toPromise();
+      if (data.status === 200) {
+        const token = data.body['data']['token'];
+       await this.setTokens(token);
+        return true;
+      } else {
+        return false;
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
   async setTokens(token) {
     const jwtData = jwtDecode(token);
     document.cookie = "Authorization=;max-age=-1";

From 526f837246008e59d63327ad5c97e5956df1c8bf Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 15:39:46 -0400
Subject: [PATCH 37/59] Add logout to editor app after warning

---
 editor/src/app/app.component.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts
index 50abcaa846..7f72119b10 100644
--- a/editor/src/app/app.component.ts
+++ b/editor/src/app/app.component.ts
@@ -171,7 +171,10 @@ export class AppComponent implements OnInit, OnDestroy {
         this.isConfirmDialogActive = false;
       } else {
         await this.logout();
-      }
+      } 
+    } else if (Date.now() > expiryTimeInMs && this.isConfirmDialogActive) {
+      // the token expired, and we warned them. Time to log out.
+      await this.logout();
     }
   }
 

From 88071e57e1f8e4f6c112ae369f378e340754270f Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 15:40:16 -0400
Subject: [PATCH 38/59] Add process monitor dialogs to publish/unpublish online
 survey

---
 .../release-online-survey.component.ts        | 34 +++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
index bc38a1a000..947bf818f2 100644
--- a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
+++ b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
@@ -5,6 +5,9 @@ import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.
 import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
 import { GroupsService } from '../services/groups.service';
 import { TangerineFormsService } from '../services/tangerine-forms.service';
+import { ProcessMonitorService } from 'src/app/shared/_services/process-monitor.service';
+import { ProcessMonitorDialogComponent } from 'src/app/shared/_components/process-monitor-dialog/process-monitor-dialog.component';
+import { MatDialog } from '@angular/material/dialog';
 
 @Component({
   selector: 'app-release-online-survey',
@@ -19,10 +22,15 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
   group;
   publishedSurveys;
   unPublishedSurveys;
+  dialogRef:any
+
   constructor(private route: ActivatedRoute,
     private groupService: GroupsService,
     private errorHandler: TangyErrorHandler,
-    private tangyFormService: TangerineFormsService) { }
+    private tangyFormService: TangerineFormsService,
+    private processMonitorService: ProcessMonitorService,
+    private dialog: MatDialog,
+  ) { }
 
   async ngOnInit() {
     this.groupId = this.route.snapshot.paramMap.get('groupId');
@@ -32,6 +40,20 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
         url: 'onlineSurvey'
       }
     ];
+    this.processMonitorService.change.subscribe((isDone) => {
+      if (this.processMonitorService.processes.length === 0) {
+        this.dialog.closeAll()
+      } else {
+        this.dialog.closeAll()
+        this.dialogRef = this.dialog.open(ProcessMonitorDialogComponent, {
+          data: {
+            messages: this.processMonitorService.processes.map(process => process.description).reverse()
+          },
+          disableClose: true
+        })
+      }
+    })
+
     await this.getForms();
   }
 
@@ -47,6 +69,8 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
     this.unPublishedSurveys = surveyData.filter(e => !e.published);
   }
   async publishSurvey(formId, appName, locked) {
+    const process = this.processMonitorService.start('publishSurvey', 'Publishing Survey');
+
     try {
       await this.groupService.publishSurvey(this.groupId, formId, 'prod', appName, locked);
       await this.getForms();
@@ -54,16 +78,22 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
     } catch (error) {
       console.error(error);
       this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+    } finally {
+      this.processMonitorService.stop(process.id);
     }
   }
   async unPublishSurvey(formId) {
+    const process = this.processMonitorService.start('unpublishSurvey', 'Un-publishing Survey');
+
     try {
       await this.groupService.unPublishSurvey(this.groupId, formId);
       await this.getForms();
-      this.errorHandler.handleError(_TRANSLATE('Survey UnPublished Successfully.'));
+      this.errorHandler.handleError(_TRANSLATE('Survey Un-published Successfully.'));
     } catch (error) {
       console.error(error);
       this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
+    } finally {
+      this.processMonitorService.stop(process.id);
     }
   }
 

From 59d05b7fb594878f6b7dfa8b3493af84e2609d84 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 15:41:15 -0400
Subject: [PATCH 39/59] Add expiry logout in online survey after warning

---
 online-survey-app/src/app/app.component.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index e35b71d63e..d4632c3175 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -68,6 +68,9 @@ export class AppComponent implements OnInit{
       } else {
         await this.logout();
       }
+    } else if (Date.now() > expiryTimeInMs && this.isConfirmDialogActive) {
+      // the token expired, and we warned them. Time to log out.
+      await this.logout();
     }
   }
 

From 3eaa97ae30898c4683b66d6a4205bed4d097680b Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 15:42:43 -0400
Subject: [PATCH 40/59] Save-as-you-go in online surveys with case

---
 .../tangy-forms-player.component.ts           | 144 +++++++++++++-----
 1 file changed, 108 insertions(+), 36 deletions(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 1efc08d0ff..4b887ea9bf 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -1,10 +1,12 @@
 import { HttpClient } from '@angular/common/http';
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { Component, ElementRef, OnInit, ViewChild, Input } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { FormsService } from 'src/app/shared/_services/forms-service.service';
 import { CaseService } from 'src/app/case/services/case.service';
 import { TangyFormService } from '../tangy-form.service';
 
+const sleep = (milliseconds) => new Promise((res) => setTimeout(() => res(true), milliseconds))
+
 @Component({
   selector: 'app-tangy-forms-player',
   templateUrl: './tangy-forms-player.component.html',
@@ -12,6 +14,13 @@ import { TangyFormService } from '../tangy-form.service';
 })
 export class TangyFormsPlayerComponent implements OnInit {
   @ViewChild('container', {static: true}) container: ElementRef;
+  @Input('response') response;
+
+  @Input('templateId') templateId:string
+  @Input('location') location:any
+  @Input('skipSaving') skipSaving = false
+  @Input('preventSubmit') preventSubmit = false
+  @Input('metadata') metadata:any
 
   formId: string;
   formResponseId: string;
@@ -19,6 +28,9 @@ export class TangyFormsPlayerComponent implements OnInit {
   caseEventId: string;
   eventFormId: string;
   window: any;
+
+  throttledSaveLoaded
+  throttledSaveFiring
   
   constructor(
     private route: ActivatedRoute, 
@@ -56,6 +68,12 @@ export class TangyFormsPlayerComponent implements OnInit {
         }
         this.window.caseService = this.caseService
 
+        this.metadata = {
+          caseId: this.caseId,
+          caseEventId: this.caseEventId,
+          eventFormId: this.eventFormId
+        }
+
         try {
           // Attempt to load the form response for the event form
           const event = this.caseService.case.events.find(event => event.id === this.caseEventId);
@@ -82,49 +100,103 @@ export class TangyFormsPlayerComponent implements OnInit {
       tangyForm.response = formResponse;
     }
 
-    tangyForm.addEventListener('after-submit', async (event) => {
-      event.preventDefault();
-      const formResponse = event.target.response;
-      if (this.caseId && this.caseEventId && this.eventFormId) {
-        formResponse.caseId = this.caseId;
-        formResponse.caseEventId = this.caseEventId;
-        formResponse.eventFormId = this.eventFormId;
-      }
-      try {
-        if (!await this.formsService.uploadFormResponse(formResponse)) {
-          alert('Form could not be submitted. Please retry');
-          return;
+    if (this.caseService) {
+      tangyForm.addEventListener('TANGY_FORM_UPDATE', async (event) => {
+        let response = event.target.store.getState()
+        this.throttledSaveResponse(response)
+  
+        if (this.caseService.eventForm && !this.caseService.eventForm.formResponseId) {
+          this.caseService.eventForm.formResponseId = tangyForm.response._id;
+          await this.caseService.save();
+          await this.caseService.load(this.caseId);
         }
-      } catch (error) {
-        console.error(error);
-      }
+      })
 
-      if (this.caseId && this.caseEventId && this.eventFormId) {
-        try {
-          const caseEvent = this.caseService.case.events.find(event => event.id === this.caseEventId);
-          if (caseEvent.id) {
-            const eventForm = caseEvent.eventForms.find(eventForm => eventForm.id === this.eventFormId);
-              if (eventForm && eventForm.id === this.eventFormId) {
-                eventForm.formResponseId = formResponse._id
-                eventForm.complete = true
-                await this.caseService.save();
-            }
+      tangyForm.addEventListener('after-submit', async (event) => {
+        event.preventDefault();
+
+        let response = event.target.store.getState()
+        this.throttledSaveResponse(response)
+
+        if (this.caseService && this.caseService.caseEvent && this.caseService.eventForm) {
+          this.caseService.markEventFormComplete(this.caseService.caseEvent.id, this.caseService.eventForm.id)
+          await this.caseService.save()
+        }
+        
+        // first route to form-submitted, then redirect to the url in window['eventFormRedirect']
+        this.router.navigate(['/form-submitted-success']);
+        if (window['eventFormRedirect']) {
+          try {
+            // this.router.navigateByUrl(window['eventFormRedirect']) -- TODO figure this out later
+            this.window['location'] = window['eventFormRedirect']
+            window['eventFormRedirect'] = ''
+          } catch (error) {
+            console.error(error);
           }
-        } catch (error) {
-          console.error(error);
         }
-      }
-      this.router.navigate(['/form-submitted-success']);
-
-      if (window['eventFormRedirect']) {
+      });
+    } else {
+      tangyForm.addEventListener('after-submit', async (event) => {
+        event.preventDefault();
         try {
-          // this.router.navigateByUrl(window['eventFormRedirect']) -- TODO figure this out later
-          this.window['location'] = window['eventFormRedirect']
-          window['eventFormRedirect'] = ''
+          if (await this.formsService.uploadFormResponse(event.target.response)){
+            this.router.navigate(['/form-submitted-success']);
+          } else {
+            alert('Form could not be submitted. Please retry');
+          }
         } catch (error) {
           console.error(error);
         }
+      });
+    }
+  }
+
+
+  // Prevent parallel saves which leads to race conditions. Only save the first and then last state of the store.
+  // Everything else in between we can ignore.
+  async throttledSaveResponse(response) {
+    // If already loaded, return.
+    if (this.throttledSaveLoaded) return
+    // Throttle this fire by waiting until last fire is done.
+    if (this.throttledSaveFiring) {
+      this.throttledSaveLoaded = true
+      while (this.throttledSaveFiring) await sleep(200)
+      this.throttledSaveLoaded = false
+    }
+    // Fire it.
+    this.throttledSaveFiring = true
+    await this.saveResponse(response)
+    this.throttledSaveFiring = false
+  }
+
+  async saveResponse(state) {
+    let stateDoc = await this.tangyFormService.getResponse(state._id)
+    const archiveStateChange = state.archived === stateDoc['archived']
+    if (stateDoc && stateDoc['complete'] && state.complete && stateDoc['form'] && !stateDoc['form'].hasSummary && archiveStateChange) {
+      // Since what is in the database is complete, and it's still complete, and it doesn't have 
+      // a summary where they might add some input, don't save! They are probably reviewing data.
+    } else {
+      // add metadata
+      stateDoc = {
+        ...state,
+        location: this.location || state.location,
+        ...this.metadata
+      }   
+      await this.tangyFormService.saveResponse(stateDoc)
+    }
+    this.response = state
+  }
+
+  async saveFormResponse(formResponse) {
+
+    try {
+      if (!await this.formsService.uploadFormResponse(formResponse)) {
+        alert('Form could not be saved. Please retry');
+        return false;
       }
-    });
+    } catch (error) {
+      console.error(error);
+    }
   }
+
 }

From a0011c2486e4cea355b71548900145674e2b3413 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Sat, 15 Jun 2024 16:37:19 -0400
Subject: [PATCH 41/59] Remove online survey getResponse API

---
 server/src/express-app.js   |  2 +-
 server/src/online-survey.js | 17 -----------------
 2 files changed, 1 insertion(+), 18 deletions(-)

diff --git a/server/src/express-app.js b/server/src/express-app.js
index 2fa9cb5239..6aa1969adf 100644
--- a/server/src/express-app.js
+++ b/server/src/express-app.js
@@ -47,7 +47,7 @@ const { extendSession, findUserByUsername,
 const {registerUser,  getUserByUsername, isUserSuperAdmin, isUserAnAdminUser, getGroupsByUser, deleteUser,
    getAllUsers, checkIfUserExistByUsername, findOneUserByUsername,
    findMyUser, updateUser, restoreUser, updateMyUser} = require('./users');
-const {login: surveyLogin, getResponse: getSurveyResponse, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey, getOnlineSurveys} = require('./online-survey')
+const {login: surveyLogin, saveResponse: saveSurveyResponse, publishSurvey, unpublishSurvey, getOnlineSurveys} = require('./online-survey')
 const {
   getCaseDefinitions,
   getCaseDefinition,
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 70514e5b90..b09dba9f05 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -35,22 +35,6 @@ const login = async (req, res) => {
   }
 }
 
-const getResponse = async (req, res) => {
-  try {
-
-    const { groupId, formResponseId } = req.params;
-
-    console.log('getResponse', groupId, formResponseId);
-
-    const db = new DB(groupId);
-    const doc = await db.get(formResponseId);
-    return res.status(200).send(doc);
-  } catch (error) {
-    console.error(error);
-    return res.status(500).send('Could not get response');
-  }
-};
-
 const saveResponse = async (req, res) => {
   try {
     const { groupId, formId } = req.params;
@@ -141,7 +125,6 @@ const getOnlineSurveys = async (req, res) => {
 
 module.exports = {
   login,
-  getResponse,
   saveResponse,
   publishSurvey,
   unpublishSurvey,

From 21f922e1c2a35963b8278e611a423421e6bf12de Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 25 Jun 2024 17:38:34 -0400
Subject: [PATCH 42/59] Add online-survey-app logout button and return to case
 url

---
 .../src/app/core/auth/_components/login/login.component.ts | 4 ++--
 online-survey-app/src/app/app.component.html               | 7 +++++++
 .../_components/survey-login/survey-login.component.ts     | 4 +++-
 .../tangy-forms-player/tangy-forms-player.component.ts     | 3 +++
 4 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/editor/src/app/core/auth/_components/login/login.component.ts b/editor/src/app/core/auth/_components/login/login.component.ts
index d58127b726..8e7904e3c3 100644
--- a/editor/src/app/core/auth/_components/login/login.component.ts
+++ b/editor/src/app/core/auth/_components/login/login.component.ts
@@ -41,10 +41,10 @@ export class LoginComponent implements OnInit {
       if (await this.authenticationService.login(this.user.username, this.user.password)) {
         this.router.navigate(['/projects']);
       } else {
-        this.errorMessage = _TRANSLATE('Login Unsuccesful');
+        this.errorMessage = _TRANSLATE('Login Unsuccessful');
       }
     } catch (error) {
-      this.errorMessage = _TRANSLATE('Login Unsuccesful');
+      this.errorMessage = _TRANSLATE('Login Unsuccessful');
       console.error(error);
     }
   }
diff --git a/online-survey-app/src/app/app.component.html b/online-survey-app/src/app/app.component.html
index a7df3ffd12..83243f8912 100644
--- a/online-survey-app/src/app/app.component.html
+++ b/online-survey-app/src/app/app.component.html
@@ -2,6 +2,13 @@
   <table>
     <tr>
       <td width="100%"></td>
+      <td>
+        <div *ngIf="loggedIn">
+          <a (click)="logout()">
+            <iron-icon icon="lock"></iron-icon>
+          </a>
+        </div>
+      </td>
       <td>
         <div *ngIf="hasHelpLink">
           <a href="{{helpLink}}" target="_blank">
diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
index 15f8194fd8..2ee61c34bf 100644
--- a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
@@ -26,7 +26,9 @@ export class SurveyLoginComponent implements OnInit {
   ) { }
 
   async ngOnInit() {
-    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || 'forms-list';
+    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] ||
+                      localStorage.getItem('caseUrlHash') ||
+                      'forms-list';
 
     if (await this.authenticationService.isLoggedIn()) {
       this.router.navigate([this.returnUrl]);
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 4b887ea9bf..8e435a1280 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -68,6 +68,9 @@ export class TangyFormsPlayerComponent implements OnInit {
         }
         this.window.caseService = this.caseService
 
+        // Store the caseUrlHash in localStorage so that we can redirect to the correct page after logout -> login
+        localStorage.setItem('caseUrlHash', `/case/event/form/${this.caseId}/${this.caseEventId}/${this.eventFormId}`);
+
         this.metadata = {
           caseId: this.caseId,
           caseEventId: this.caseEventId,

From ab92d868bc396898648086db820fdb8041092a94 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 1 Jul 2024 13:32:28 -0400
Subject: [PATCH 43/59] Fix online survey extend session call

---
 .../survey-login/survey-login.component.ts    |  9 +--------
 .../auth/_services/authentication.service.ts  | 20 +++++++++++++------
 server/src/online-survey.js                   |  8 ++++----
 3 files changed, 19 insertions(+), 18 deletions(-)

diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
index 2ee61c34bf..3948c5c79d 100644
--- a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
@@ -2,8 +2,6 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { AuthenticationService } from '../../_services/authentication.service';
 import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import { AppConfigService } from 'src/app/shared/_services/app-config.service';
-
 
 @Component({
   selector: 'survey-login',
@@ -20,7 +18,6 @@ export class SurveyLoginComponent implements OnInit {
 
   constructor(
     private authenticationService: AuthenticationService,
-    private appConfigService: AppConfigService,
     private route: ActivatedRoute,
     private router: Router,
   ) { }
@@ -55,11 +52,7 @@ export class SurveyLoginComponent implements OnInit {
 
   async loginUser() {
     try {
-
-      const appConfig = await this.appConfigService.getAppConfig();
-      const groupId = appConfig['groupId'];
-
-      if (await this.authenticationService.surveyLogin(groupId, this.user.accessCode)) {
+      if (await this.authenticationService.surveyLogin(this.user.accessCode)) {
         this.router.navigate([this.returnUrl]);
       } else {
         this.errorMessage = _TRANSLATE('Login Unsuccessful');
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 9a7a40be09..7118908883 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -1,16 +1,18 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { UserService } from './user.service';
 import { Subject } from 'rxjs';
 import { jwtDecode } from 'jwt-decode';
 import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
+import { AppConfigService } from 'src/app/shared/_services/app-config.service';
 
 @Injectable()
 export class AuthenticationService {
   public currentUserLoggedIn$: any;
   private _currentUserLoggedIn: boolean;
-  constructor(private userService: UserService, private http: HttpClient, private errorHandler: TangyErrorHandler) {
+  constructor(
+    private http: HttpClient,
+    private appConfigService: AppConfigService
+  ) {
     this.currentUserLoggedIn$ = new Subject();
   }
 
@@ -33,7 +35,10 @@ export class AuthenticationService {
     }
   }
 
-  async surveyLogin(groupId: string, accessCode: string) {
+  async surveyLogin(accessCode: string) {
+    const appConfig = await this.appConfigService.getAppConfig();
+    const groupId = appConfig['groupId'];
+
     try {
       const data = await this.http.post(`/onlineSurvey/login/${groupId}/${accessCode}`, {groupId, accessCode}, {observe: 'response'}).toPromise();
       if (data.status === 200) {
@@ -71,9 +76,12 @@ export class AuthenticationService {
   }
 
   async extendUserSession() {
-    const username = localStorage.getItem('user_id');
+    const appConfig = await this.appConfigService.getAppConfig();
+    const groupId = appConfig['groupId'];
+    const accessCode = localStorage.getItem('user_id');
+
     try {
-      const data = await this.http.post('/extendSession', {username}, {observe: 'response'}).toPromise();
+      const data = await this.http.post(`/onlineSurvey/login/${groupId}/${accessCode}`, {groupId, accessCode}, {observe: 'response'}).toPromise();
       if (data.status === 200) {
         const token = data.body['data']['token'];
        await this.setTokens(token);
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index b09dba9f05..11a7fcae0f 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -9,8 +9,6 @@ const login = async (req, res) => {
 
     const { groupId, accessCode } = req.params;
 
-    console.log('login', groupId, accessCode);
-
     const groupDb = new DB(groupId)
     let options = { key: accessCode, include_docs: true }
     if (req.params.limit) {
@@ -24,14 +22,16 @@ const login = async (req, res) => {
     const userProfileDoc = docs.find(doc => doc.form.id === 'user-profile');
 
     if (userProfileDoc) {
-      debugger;
       let permissions = {groupPermissions: [], sitewidePermissions: []};
       const token = createLoginJWT({ "username": accessCode, permissions });
       return res.status(200).send({ data: { token } });
+    } else {
+      console.error(error);
+      return res.status(401).send({ data: 'Invalid Credentials' });
     }
   } catch (error) {
     console.error(error);
-    return res.status(401).send({ data: 'Invalid Credentials' });
+    return res.status(500).send({ data: 'An error occurred attempting to login' });
   }
 }
 

From 621a7516898c638502e7bdd07324e7fbecee8fa5 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 3 Jul 2024 15:02:59 -0400
Subject: [PATCH 44/59] Remove excessive logging in CSV generation scripts that
 causes ERR_STDOUT_MAX_SIZE error for wide csvs

---
 server/src/scripts/generate-csv-data-set/batch.js |  5 -----
 server/src/scripts/generate-csv/batch.js          | 10 ----------
 2 files changed, 15 deletions(-)

diff --git a/server/src/scripts/generate-csv-data-set/batch.js b/server/src/scripts/generate-csv-data-set/batch.js
index 5688e8481a..f16e9144cc 100755
--- a/server/src/scripts/generate-csv-data-set/batch.js
+++ b/server/src/scripts/generate-csv-data-set/batch.js
@@ -27,7 +27,6 @@ function getData(dbName, formId, skip, batchSize, year, month) {
     try {
       const key = (year && month) ? `${formId}_${year}_${month}` : formId
       const target = `${dbDefaults.prefix}/${dbName}/_design/tangy-reporting/_view/resultsByGroupFormId?keys=["${key}"]&include_docs=true&skip=${skip}&limit=${limit}`
-      console.log(target)
       axios.get(target)
         .then(response => {
           resolve(response.data.rows.map(row => row.doc))
@@ -43,13 +42,9 @@ function getData(dbName, formId, skip, batchSize, year, month) {
 }
 
 async function batch() {
-  console.log("in batch.")
   const state = JSON.parse(await readFile(params.statePath))
-  console.log("state.skip: " + state.skip)
   const docs = await getData(state.dbName, state.formId, state.skip, state.batchSize, state.year, state.month)
-  // console.log("docs: " + JSON.stringify(docs))
   let outputDisabledFieldsToCSV = state.groupConfigurationDoc? state.groupConfigurationDoc["outputDisabledFieldsToCSV"] : false
-  console.log("outputDisabledFieldsToCSV: " + outputDisabledFieldsToCSV)
   if (docs.length === 0) {
     state.complete = true
   } else {
diff --git a/server/src/scripts/generate-csv/batch.js b/server/src/scripts/generate-csv/batch.js
index 0c5c0bdcc4..c3d7bb098a 100755
--- a/server/src/scripts/generate-csv/batch.js
+++ b/server/src/scripts/generate-csv/batch.js
@@ -27,7 +27,6 @@ function getData(dbName, formId, skip, batchSize, year, month) {
     try {
       const key = (year && month) ? `${formId}_${year}_${month}` : formId
       const target = `${dbDefaults.prefix}/${dbName}/_design/tangy-reporting/_view/resultsByGroupFormId?keys=["${key}"]&include_docs=true&skip=${skip}&limit=${limit}`
-      console.log(target)
       axios.get(target)
         .then(response => {
           resolve(response.data.rows.map(row => row.doc))
@@ -44,13 +43,9 @@ function getData(dbName, formId, skip, batchSize, year, month) {
 
 async function batch() {
   const state = JSON.parse(await readFile(params.statePath))
-  console.log("state.skip: " + state.skip)
   const docs = await getData(state.dbName, state.formId, state.skip, state.batchSize, state.year, state.month)
   let outputDisabledFieldsToCSV = state.groupConfigurationDoc? state.groupConfigurationDoc["outputDisabledFieldsToCSV"] : false
-  console.log("outputDisabledFieldsToCSV: " + outputDisabledFieldsToCSV)
   let csvReplacementCharacters = state.groupConfigurationDoc? state.groupConfigurationDoc["csvReplacementCharacters"] : false
-  console.log("csvReplacementCharacters: " + JSON.stringify(csvReplacementCharacters))
-  // let csvReplacement = csvReplacementCharacters? JSON.parse(csvReplacementCharacters) : false
   if (docs.length === 0) {
     state.complete = true
   } else {
@@ -67,7 +62,6 @@ async function batch() {
             // skip
           } else {
             let value = doc[header];
-            console.log("header: " + header + " value: " + value)
             if (typeof value === 'string') {
               if (csvReplacementCharacters) {
                 csvReplacementCharacters.forEach(expression => {
@@ -85,7 +79,6 @@ async function batch() {
               }
             }
             if (typeof header === 'string' && header.split('.').length === 3) {
-              console.log("Checking header: " + header + " to see if it is disabled.")
               const itemId = header.split('.')[1]
               if (itemId && doc[`${itemId}_disabled`] === 'true') {
                 if (outputDisabledFieldsToCSV) {
@@ -113,7 +106,6 @@ async function batch() {
           doc.attendanceList.forEach(attendance => {
             let row = [doc._id, ...state.headersKeys.map(header => {
               let value = attendance[header];
-              console.log("header: " + header + " value: " + value)
               return value
             })]
             rows.push(row)
@@ -122,7 +114,6 @@ async function batch() {
           doc.scoreList.forEach(score => {
             let row = [doc._id, ...state.headersKeys.map(header => {
               let value = score[header];
-              console.log("header: " + header + " value: " + value)
               return value
             })]
             rows.push(row)
@@ -131,7 +122,6 @@ async function batch() {
           doc.studentBehaviorList.forEach(behavior => {
             let row = [doc._id, ...state.headersKeys.map(header => {
               let value = behavior[header];
-              console.log("header: " + header + " value: " + value)
               return value
             })]
             rows.push(row)

From f635d85ec69771b8b28d5b2b74c8b7ad43ffedf9 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 10 Jul 2024 11:01:26 -0400
Subject: [PATCH 45/59] Await throttled save response to ensure response is
 saved after-submit

---
 .../tangy-forms-player/tangy-forms-player.component.ts          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 8e435a1280..de87da8218 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -119,7 +119,7 @@ export class TangyFormsPlayerComponent implements OnInit {
         event.preventDefault();
 
         let response = event.target.store.getState()
-        this.throttledSaveResponse(response)
+        await this.throttledSaveResponse(response)
 
         if (this.caseService && this.caseService.caseEvent && this.caseService.eventForm) {
           this.caseService.markEventFormComplete(this.caseService.caseEvent.id, this.caseService.eventForm.id)

From 59442896758ccc9e043d3e64c9c4257d5b2a169c Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 10 Jul 2024 14:47:59 -0400
Subject: [PATCH 46/59] Online-survey-app logout on unload

---
 online-survey-app/src/app/app.component.ts        | 15 +++++++++++++--
 .../core/auth/_services/authentication.service.ts |  1 +
 server/src/online-survey.js                       |  1 -
 3 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index d4632c3175..4c70797cda 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, HostListener } from '@angular/core';
 import { AppConfigService } from './shared/_services/app-config.service';
 import { _TRANSLATE } from './shared/_services/translation-marker';
 import { AuthenticationService } from './core/auth/_services/authentication.service';
@@ -63,9 +63,14 @@ export class AppComponent implements OnInit{
       this.isConfirmDialogActive = true;
       const extendSession = confirm(_TRANSLATE('You are about to be logged out. Should we extend your session?'));
       if (extendSession) {
-        await this.authenticationService.extendUserSession();
         this.isConfirmDialogActive = false;
+        const extendedSession = await this.authenticationService.extendUserSession();
+        if (!extendedSession) {
+          await this.logout();
+        }
       } else {
+        this.isConfirmDialogActive = false;
+
         await this.logout();
       }
     } else if (Date.now() > expiryTimeInMs && this.isConfirmDialogActive) {
@@ -80,4 +85,10 @@ export class AppComponent implements OnInit{
     this.loggedIn = false;
     this.router.navigate(['/survey-login']);
   }
+
+  @HostListener("window:unload",["$event"])
+  async onUnload(event) {
+    clearInterval(this.sessionTimeoutCheckTimerID);
+    await this.authenticationService.logout();
+  }
 }
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 7118908883..ca254dae90 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -91,6 +91,7 @@ export class AuthenticationService {
       }
     } catch (error) {
       console.log(error);
+      return false;
     }
   }
 
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index 11a7fcae0f..c410c2a912 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -26,7 +26,6 @@ const login = async (req, res) => {
       const token = createLoginJWT({ "username": accessCode, permissions });
       return res.status(200).send({ data: { token } });
     } else {
-      console.error(error);
       return res.status(401).send({ data: 'Invalid Credentials' });
     }
   } catch (error) {

From 5e363fb3b54abb80bde18bc0ddc315e9e293ac16 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 12 Jul 2024 09:46:05 -0400
Subject: [PATCH 47/59] Save caseUrlHash earlier in online-survey-app

---
 .../tangy-forms-player.component.ts              | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index de87da8218..98975dbfbd 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -49,16 +49,19 @@ export class TangyFormsPlayerComponent implements OnInit {
     });
   }
 
-  async ngOnInit(): Promise<any> {   
-    const groupId = window.location.pathname.split('/')[4]; 
-    this.tangyFormService.initialize(groupId);
-
+  async ngOnInit(): Promise<any> {
     this.window = window;
 
-    // Loading the formResponse must happen before rendering the innerHTML
+    // Loading the formResponse from a case must happen before rendering the innerHTML
     let formResponse;
     if (this.caseId && this.caseEventId && this.eventFormId) {
+      // Store the caseUrlHash in localStorage so that we can redirect to the correct page after logout -> login
+      localStorage.setItem('caseUrlHash', `/case/event/form/${this.caseId}/${this.caseEventId}/${this.eventFormId}`);
+
       try {
+        const groupId = window.location.pathname.split('/')[4];
+        this.tangyFormService.initialize(groupId);
+
         await this.caseService.load(this.caseId);
         this.caseService.setContext(this.caseEventId, this.eventFormId)
 
@@ -68,9 +71,6 @@ export class TangyFormsPlayerComponent implements OnInit {
         }
         this.window.caseService = this.caseService
 
-        // Store the caseUrlHash in localStorage so that we can redirect to the correct page after logout -> login
-        localStorage.setItem('caseUrlHash', `/case/event/form/${this.caseId}/${this.caseEventId}/${this.eventFormId}`);
-
         this.metadata = {
           caseId: this.caseId,
           caseEventId: this.caseEventId,

From 5beca6b220113b33c2c5fd67433c534cc58c8897 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Mon, 15 Jul 2024 14:59:05 -0400
Subject: [PATCH 48/59] Don't show case forms in online-survey publish list

---
 .../release-online-survey/release-online-survey.component.ts  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
index 947bf818f2..f224f9b5b3 100644
--- a/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
+++ b/editor/src/app/groups/release-online-survey/release-online-survey.component.ts
@@ -65,8 +65,8 @@ export class ReleaseOnlineSurveyComponent implements OnInit {
       const survey = groupOnlineSurveys.find(s => f.id === s.formId) || {};
       return { ...f, ...survey };
     });
-    this.publishedSurveys = surveyData.filter(e => e.published);
-    this.unPublishedSurveys = surveyData.filter(e => !e.published);
+    this.publishedSurveys = surveyData.filter(e => e.published && e.type == "form");
+    this.unPublishedSurveys = surveyData.filter(e => !e.published && e.type == "form");
   }
   async publishSurvey(formId, appName, locked) {
     const process = this.processMonitorService.start('publishSurvey', 'Publishing Survey');

From f82f70cf3fa3364f83f634f08c1cc691f2390049 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Tue, 16 Jul 2024 17:28:10 -0400
Subject: [PATCH 49/59] Revert logout on window unload in online survey app

---
 online-survey-app/src/app/app.component.ts | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index 4c70797cda..3dcee801f2 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, HostListener } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { AppConfigService } from './shared/_services/app-config.service';
 import { _TRANSLATE } from './shared/_services/translation-marker';
 import { AuthenticationService } from './core/auth/_services/authentication.service';
@@ -85,10 +85,4 @@ export class AppComponent implements OnInit{
     this.loggedIn = false;
     this.router.navigate(['/survey-login']);
   }
-
-  @HostListener("window:unload",["$event"])
-  async onUnload(event) {
-    clearInterval(this.sessionTimeoutCheckTimerID);
-    await this.authenticationService.logout();
-  }
 }

From ced1ce56bb137de503a90827897ce5b9bca6426c Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Wed, 17 Jul 2024 17:43:13 -0400
Subject: [PATCH 50/59] Save online survey app response directly on
 after-submit

---
 .../tangy-forms-player.component.ts           | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 98975dbfbd..3ea6eba18c 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -119,15 +119,11 @@ export class TangyFormsPlayerComponent implements OnInit {
         event.preventDefault();
 
         let response = event.target.store.getState()
-        await this.throttledSaveResponse(response)
-
+        await this.saveResponse(response)
         if (this.caseService && this.caseService.caseEvent && this.caseService.eventForm) {
           this.caseService.markEventFormComplete(this.caseService.caseEvent.id, this.caseService.eventForm.id)
           await this.caseService.save()
         }
-        
-        // first route to form-submitted, then redirect to the url in window['eventFormRedirect']
-        this.router.navigate(['/form-submitted-success']);
         if (window['eventFormRedirect']) {
           try {
             // this.router.navigateByUrl(window['eventFormRedirect']) -- TODO figure this out later
@@ -136,6 +132,8 @@ export class TangyFormsPlayerComponent implements OnInit {
           } catch (error) {
             console.error(error);
           }
+        } else {
+          this.router.navigate(['/form-submitted-success']);
         }
       });
     } else {
@@ -178,16 +176,21 @@ export class TangyFormsPlayerComponent implements OnInit {
     if (stateDoc && stateDoc['complete'] && state.complete && stateDoc['form'] && !stateDoc['form'].hasSummary && archiveStateChange) {
       // Since what is in the database is complete, and it's still complete, and it doesn't have 
       // a summary where they might add some input, don't save! They are probably reviewing data.
+      this.response = stateDoc
     } else {
       // add metadata
       stateDoc = {
         ...state,
         location: this.location || state.location,
         ...this.metadata
-      }   
-      await this.tangyFormService.saveResponse(stateDoc)
+      }
+      const updatedStateDoc = await this.tangyFormService.saveResponse(stateDoc)
+      if (updatedStateDoc) {
+        this.response = updatedStateDoc
+        return true;
+      }
     }
-    this.response = state
+    return false;
   }
 
   async saveFormResponse(formResponse) {

From ac8078bbbd8d742666b05fbfbd438581b4a7697a Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 26 Jul 2024 13:03:18 -0400
Subject: [PATCH 51/59] Show link to survey in case event form with form
 response id

---
 .../event-form-list-item/event-form-list-item.component.html    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
index 505ebb27f5..96a1dcbfbc 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.html
@@ -5,7 +5,7 @@
 		<div [innerHTML]="renderedTemplateListItemPrimary|unsanitizeHtml"></div>
 		<div [innerHTML]="renderedTemplateListItemSecondary|unsanitizeHtml" secondary></div>
 	</div>
-	<span *ngIf="!eventFormArchived && !eventForm.formResponseId && canLinkToOnlineSurvey">
+	<span *ngIf="!eventFormArchived && canLinkToOnlineSurvey">
 		<button class="tangy-small-list-icon" [matMenuTriggerFor]="linkMenu" (click)="showLinkMenu(); $event.stopPropagation()">
 			<mwc-icon>share</mwc-icon>
 		</button>

From 1b488c98b7c0a270400c3145efa37b8d48a73606 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Fri, 26 Jul 2024 13:12:43 -0400
Subject: [PATCH 52/59] Fix survey link copied in case event form

---
 .../event-form-list-item/event-form-list-item.component.ts      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
index 1946311578..22d3877723 100644
--- a/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
+++ b/editor/src/app/case/components/event-form-list-item/event-form-list-item.component.ts
@@ -155,7 +155,7 @@ export class EventFormListItemComponent implements OnInit {
   }
 
   onCopyLinkClick() {
-    const url = `${window.location.origin}/${this.surveyLinkUrl}`;
+    const url = `${window.location.origin}${this.surveyLinkUrl}`;
     try {
       navigator.clipboard.writeText(url);
 

From f6054f242b75c321e17f15ed6a59cebfcd6d2823 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 1 Aug 2024 06:05:38 -0400
Subject: [PATCH 53/59] Add startDatetime and startUnixtime to user-profile
 when created with API

---
 server/src/classes/user-profile.class.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/server/src/classes/user-profile.class.js b/server/src/classes/user-profile.class.js
index 698815f0c9..930062dd25 100644
--- a/server/src/classes/user-profile.class.js
+++ b/server/src/classes/user-profile.class.js
@@ -9,6 +9,8 @@ class UserProfile {
     this.form = { id: "user-profile" }
     this.items = [{ inputs: [] }]
     this.location = {};
+    this.startDatetime = (new Date()).toLocaleString();
+    this.startUnixtime = Date.now();
   }
 
   addInputs(inputs) {

From 75cda621e5462ef841d81cec45d8fbfa615206b6 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 1 Aug 2024 06:07:13 -0400
Subject: [PATCH 54/59] Store caseUrlHash in sessionStorage instead of
 localStorage

---
 .../auth/_components/survey-login/survey-login.component.ts   | 2 +-
 .../tangy-forms-player/tangy-forms-player.component.ts        | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
index 3948c5c79d..b29091b9a7 100644
--- a/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/survey-login/survey-login.component.ts
@@ -24,7 +24,7 @@ export class SurveyLoginComponent implements OnInit {
 
   async ngOnInit() {
     this.returnUrl = this.route.snapshot.queryParams['returnUrl'] ||
-                      localStorage.getItem('caseUrlHash') ||
+                      sessionStorage.getItem('caseUrlHash') ||
                       'forms-list';
 
     if (await this.authenticationService.isLoggedIn()) {
diff --git a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 3ea6eba18c..cae00b8a26 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -55,8 +55,8 @@ export class TangyFormsPlayerComponent implements OnInit {
     // Loading the formResponse from a case must happen before rendering the innerHTML
     let formResponse;
     if (this.caseId && this.caseEventId && this.eventFormId) {
-      // Store the caseUrlHash in localStorage so that we can redirect to the correct page after logout -> login
-      localStorage.setItem('caseUrlHash', `/case/event/form/${this.caseId}/${this.caseEventId}/${this.eventFormId}`);
+      // Store the caseUrlHash in sessionStorage so that we can redirect to the correct page after logout -> login
+      sessionStorage.setItem('caseUrlHash', `/case/event/form/${this.caseId}/${this.caseEventId}/${this.eventFormId}`);
 
       try {
         const groupId = window.location.pathname.split('/')[4];

From 30b61d30f4a9ff17981b5f33a713cdae184576b7 Mon Sep 17 00:00:00 2001
From: esurface <esurface@rti.org>
Date: Thu, 1 Aug 2024 06:08:14 -0400
Subject: [PATCH 55/59] Storage auth token vars in sessionStorage in online
 survey app

---
 online-survey-app/src/app/app.component.ts    |  2 +-
 .../auth/_components/login/login.component.ts |  2 +-
 .../auth/_services/authentication.service.ts  | 32 +++++++++----------
 .../app/core/auth/_services/user.service.ts   |  4 +--
 .../app/shared/classes/user-database.class.ts |  6 ++--
 .../src/app/tangy-forms/tangy-form.service.ts |  2 +-
 6 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/online-survey-app/src/app/app.component.ts b/online-survey-app/src/app/app.component.ts
index 3dcee801f2..081f718fc6 100644
--- a/online-survey-app/src/app/app.component.ts
+++ b/online-survey-app/src/app/app.component.ts
@@ -55,7 +55,7 @@ export class AppComponent implements OnInit{
   }
   
   async sessionTimeoutCheck() {
-    const token = localStorage.getItem('token');
+    const token = sessionStorage.getItem('token');
     const claims = JSON.parse(atob(token.split('.')[1]));
     const expiryTimeInMs = claims['exp'] * 1000;
     const minutesBeforeExpiry = expiryTimeInMs - (15 * 60 * 1000); // warn 15 minutes before expiry of token
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
index b770f708de..4194829b0d 100644
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.ts
+++ b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
@@ -38,7 +38,7 @@ export class LoginComponent implements OnInit {
 
       if (window.location.origin.startsWith('http://localhost')) {
         // If we are running on localhost, we want to use the local server for authentication 
-        localStorage.setItem(this.user.username, this.user.password);
+        sessionStorage.setItem(this.user.username, this.user.password);
         this.router.navigate([this.returnUrl]);
       } else if (await this.authenticationService.login(this.user.username, this.user.password)) {
         this.router.navigate([this.returnUrl]);
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index ca254dae90..1f67df0f17 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -28,9 +28,9 @@ export class AuthenticationService {
       }
     } catch (error) {
       console.error(error);
-      localStorage.removeItem('token');
-      localStorage.removeItem('user_id');
-      localStorage.removeItem('permissions');
+      sessionStorage.removeItem('token');
+      sessionStorage.removeItem('user_id');
+      sessionStorage.removeItem('permissions');
       return false;
     }
   }
@@ -50,26 +50,26 @@ export class AuthenticationService {
       }
     } catch (error) {
       console.error(error);
-      localStorage.removeItem('token');
-      localStorage.removeItem('user_id');
-      localStorage.removeItem('password');
-      localStorage.removeItem('permissions');
+      sessionStorage.removeItem('token');
+      sessionStorage.removeItem('user_id');
+      sessionStorage.removeItem('password');
+      sessionStorage.removeItem('permissions');
       return false;
     }
   }
 
   async isLoggedIn():Promise<boolean> {
     this._currentUserLoggedIn = false;
-    this._currentUserLoggedIn = !!localStorage.getItem('user_id');
+    this._currentUserLoggedIn = !!sessionStorage.getItem('user_id');
     this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
     return this._currentUserLoggedIn;
   }
 
   async logout() {
-    localStorage.removeItem('token');
-    localStorage.removeItem('user_id');
-    localStorage.removeItem('password');
-    localStorage.removeItem('permissions');
+    sessionStorage.removeItem('token');
+    sessionStorage.removeItem('user_id');
+    sessionStorage.removeItem('password');
+    sessionStorage.removeItem('permissions');
     document.cookie = "Authorization=;max-age=-1";
     this._currentUserLoggedIn = false;
     this.currentUserLoggedIn$.next(this._currentUserLoggedIn);
@@ -78,7 +78,7 @@ export class AuthenticationService {
   async extendUserSession() {
     const appConfig = await this.appConfigService.getAppConfig();
     const groupId = appConfig['groupId'];
-    const accessCode = localStorage.getItem('user_id');
+    const accessCode = sessionStorage.getItem('user_id');
 
     try {
       const data = await this.http.post(`/onlineSurvey/login/${groupId}/${accessCode}`, {groupId, accessCode}, {observe: 'response'}).toPromise();
@@ -98,9 +98,9 @@ export class AuthenticationService {
   async setTokens(token) {
     const jwtData = jwtDecode(token);
     document.cookie = "Authorization=;max-age=-1";
-    localStorage.setItem('token', token);
-    localStorage.setItem('user_id', jwtData['username']);
-    localStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
+    sessionStorage.setItem('token', token);
+    sessionStorage.setItem('user_id', jwtData['username']);
+    sessionStorage.setItem('permissions', JSON.stringify(jwtData['permissions']));
     document.cookie = `Authorization=${token}`;
   }
 
diff --git a/online-survey-app/src/app/core/auth/_services/user.service.ts b/online-survey-app/src/app/core/auth/_services/user.service.ts
index 100d81d8d5..55c91fd7b1 100644
--- a/online-survey-app/src/app/core/auth/_services/user.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/user.service.ts
@@ -43,7 +43,7 @@ export class UserService {
   }
   
   async getCurrentUser() {
-    return await localStorage.getItem('user_id');
+    return await sessionStorage.getItem('user_id');
   }
   private showError(error: any) {
     console.log(error);
@@ -58,7 +58,7 @@ export class UserService {
 
   async getMyUser() {
     try {
-      if (localStorage.getItem('user_id') === 'user1') {
+      if (sessionStorage.getItem('user_id') === 'user1') {
         return {
           email: 'user1@tangerinecentral.org',
           firstName: 'user1',
diff --git a/online-survey-app/src/app/shared/classes/user-database.class.ts b/online-survey-app/src/app/shared/classes/user-database.class.ts
index 38e681573c..97abb819c7 100644
--- a/online-survey-app/src/app/shared/classes/user-database.class.ts
+++ b/online-survey-app/src/app/shared/classes/user-database.class.ts
@@ -24,7 +24,7 @@ export class UserDatabase {
   }
 
   async get(id) {
-    const token = localStorage.getItem('token');
+    const token = sessionStorage.getItem('token');
     return (<any>await axios.get(`/group-responses/read/${this.groupId}/${id}`, { headers: { authorization: token }})).data
   }
 
@@ -33,7 +33,7 @@ export class UserDatabase {
   }
 
   async post(doc) {
-    const token = localStorage.getItem('token');
+    const token = sessionStorage.getItem('token');
     if (this.attachHistoryToDocs === undefined) {
       const appConfig = (<any>await axios.get('./assets/app-config.json', { headers: { authorization: token }})).data
       this.attachHistoryToDocs = appConfig['attachHistoryToDocs']
@@ -70,7 +70,7 @@ export class UserDatabase {
 
   async remove(doc) {
     // This is not implemented...
-    const token = localStorage.getItem('token');
+    const token = sessionStorage.getItem('token');
     return await axios.delete(`/api/${this.groupId}`, doc)
   }
 
diff --git a/online-survey-app/src/app/tangy-forms/tangy-form.service.ts b/online-survey-app/src/app/tangy-forms/tangy-form.service.ts
index f4a48e1860..3818ee7be2 100644
--- a/online-survey-app/src/app/tangy-forms/tangy-form.service.ts
+++ b/online-survey-app/src/app/tangy-forms/tangy-form.service.ts
@@ -21,7 +21,7 @@ export class TangyFormService {
   }
 
   initialize(groupId) {
-    this.userId = localStorage.getItem('user_id') || 'Survey'
+    this.userId = sessionStorage.getItem('user_id') || 'Survey'
     this.db = new UserDatabase(this.userId, groupId)
   }
 

From a792d5535b88412a647ff56323dfc58b05cbe325 Mon Sep 17 00:00:00 2001
From: Erik Surface <esurface@rti.org>
Date: Fri, 10 Jan 2025 11:43:03 -0500
Subject: [PATCH 56/59] Remove unused auth components in online-survey-app

---
 .../src/app/case/services/case.service.ts     |   2 -
 .../app/case/services/cases.service.spec.ts   |   5 -
 .../edit-user/edit-user.component.css         |   9 -
 .../edit-user/edit-user.component.html        |  59 -------
 .../edit-user/edit-user.component.spec.ts     |  25 ---
 .../edit-user/edit-user.component.ts          |  78 ---------
 .../_components/login/login.component.css     |  33 ----
 .../_components/login/login.component.html    |  31 ----
 .../_components/login/login.component.spec.ts |  25 ---
 .../auth/_components/login/login.component.ts |  54 ------
 .../update-personal-profile.component.css     |   4 -
 .../update-personal-profile.component.html    |  68 --------
 .../update-personal-profile.component.spec.ts |  25 ---
 .../update-personal-profile.component.ts      |  87 ----------
 .../update-user-role.component.css            |  16 --
 .../update-user-role.component.html           |  16 --
 .../update-user-role.component.spec.ts        |  25 ---
 .../update-user-role.component.ts             |  76 ---------
 .../user-registration.component.css           |   3 -
 .../user-registration.component.html          |  70 --------
 .../user-registration.component.spec.ts       |  25 ---
 .../user-registration.component.ts            |  81 ---------
 .../user-registration/user.model.interface.ts |   8 -
 .../auth/_services/authentication.service.ts  |  19 ---
 .../core/auth/_services/user.service.spec.ts  |  15 --
 .../app/core/auth/_services/user.service.ts   | 154 ------------------
 .../src/app/core/auth/auth-routing.module.ts  |   5 -
 .../src/app/core/auth/auth.module.ts          |   7 +-
 28 files changed, 2 insertions(+), 1023 deletions(-)
 delete mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
 delete mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
 delete mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.css
 delete mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.html
 delete mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/login/login.component.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
 delete mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
 delete mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_services/user.service.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_services/user.service.ts

diff --git a/online-survey-app/src/app/case/services/case.service.ts b/online-survey-app/src/app/case/services/case.service.ts
index 20b084ba35..ea8e44d5af 100644
--- a/online-survey-app/src/app/case/services/case.service.ts
+++ b/online-survey-app/src/app/case/services/case.service.ts
@@ -6,7 +6,6 @@ import { NotificationStatus, Notification, NotificationType } from './../classes
 import { TangyFormService } from 'src/app/tangy-forms/tangy-form.service';
 import { CaseDefinitionsService } from './case-definitions.service';
 import { HttpClient } from '@angular/common/http';
-import { UserService } from 'src/app/core/auth/_services/user.service';
 // Classes.
 import { TangyFormResponseModel } from 'tangy-form/tangy-form-response-model.js';
 import { Case } from '../classes/case.class'
@@ -102,7 +101,6 @@ class CaseService {
   constructor(
     private tangyFormService: TangyFormService,
     private caseDefinitionsService: CaseDefinitionsService,
-    private userService:UserService,
     private appConfigService:AppConfigService,
     private http:HttpClient
   ) {
diff --git a/online-survey-app/src/app/case/services/cases.service.spec.ts b/online-survey-app/src/app/case/services/cases.service.spec.ts
index 1ccaee9023..140a520e25 100644
--- a/online-survey-app/src/app/case/services/cases.service.spec.ts
+++ b/online-survey-app/src/app/case/services/cases.service.spec.ts
@@ -1,7 +1,6 @@
 import { TestBed } from '@angular/core/testing';
 
 import { CasesService } from './cases.service';
-import { UserService } from 'src/app/shared/_services/user.service';
 import { HttpClientModule } from '@angular/common/http';
 import { CaseEventInfo } from './case-event-info.class';
 import { CASE_EVENT_STATUS_IN_PROGRESS } from '../classes/case-event.class';
@@ -237,10 +236,6 @@ describe('CasesService', () => {
     imports:[HttpClientModule],
     providers: [
       {provide:CasesService, useClass:MockCasesService},
-      {
-        provide: UserService,
-        useClass: MockUserService
-      }
     ]
   }));
 
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
deleted file mode 100644
index 7e475170aa..0000000000
--- a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.css
+++ /dev/null
@@ -1,9 +0,0 @@
-form {
-  margin: 20px;
-}
-
-:host() {
-
-  display: block;
-  padding-right: 45px;
-}
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
deleted file mode 100644
index 99bdd90ae9..0000000000
--- a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<div *ngIf="user">
-      <h1 class="tangy-foreground-secondary">
-        {{'Edit User'|translate}} {{user.username}}
-      </h1>
-        <form class="tangy-full-width" novalidate #editUserForm="ngForm">
-          
-          <mat-form-field class="tangy-full-width">
-            <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
-              required>
-              <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
-                {{'This Field is Required'|translate}}
-              </mat-error>
-          </mat-form-field>
-          <br>
-          <br>
-          <mat-form-field class="tangy-full-width">
-            <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
-              required>
-              <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
-                {{'This Field is Required'|translate}}
-              </mat-error>
-          </mat-form-field>
-          <br>
-          <br>
-          <mat-form-field class="tangy-full-width">
-            <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
-              matInput placeholder="{{'Email'|translate}}" required>
-            <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
-              {{'Please enter a valid email address'|translate}}
-            </mat-error>
-          </mat-form-field>
-          <br>
-          <br>
-          <mat-checkbox [(ngModel)]="updateUserPassword" name="updateUserPassword">Update User password?</mat-checkbox>
-          <br>
-          <br>
-          <div *ngIf="updateUserPassword">
-            <mat-form-field class="tangy-full-width">
-              <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'Password'|translate}}"
-                required>
-            </mat-form-field>
-            <br>
-            <br>
-            <mat-form-field class="tangy-full-width">
-              <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
-                required>
-              <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
-                {{'Passwords do not match'|translate}}
-              </mat-error>
-            </mat-form-field>
-          </div>
-          <span [id]="statusMessage.type" *ngIf="statusMessage.type==='error'">
-            <small>{{statusMessage.message}}</small>
-          </span>
-          <p>
-            <button [disabled]="editUserForm.invalid||(user.confirmPassword!==user.password)" mat-raised-button color="warn" (click)="editUser()">{{'UPDATE USER'|translate}}</button>
-          </p>
-        </form>
-    </div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
deleted file mode 100644
index 3702bd7a39..0000000000
--- a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { EditUserComponent } from './edit-user.component';
-
-describe('EditUserComponent', () => {
-  let component: EditUserComponent;
-  let fixture: ComponentFixture<EditUserComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ EditUserComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(EditUserComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts b/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
deleted file mode 100644
index d261999573..0000000000
--- a/online-survey-app/src/app/core/auth/_components/edit-user/edit-user.component.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { User } from '../user-registration/user.model.interface';
-import { UserService } from '../../_services/user.service';
-import { ActivatedRoute, Router } from '@angular/router';
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import {HttpClient} from "@angular/common/http";
-
-@Component({
-  selector: 'app-edit-user',
-  templateUrl: './edit-user.component.html',
-  styleUrls: ['./edit-user.component.css']
-})
-export class EditUserComponent implements OnInit {
-  user: User;
-  updateUserPassword = false;
-  statusMessage = { type: '', message: '' };
-  disableSubmit = false;
-  passwordIsNotStrong = { type: 'error', message: _TRANSLATE('Password is not strong enough.') };
-  incorrectAdminPassword = { type: 'error', message: _TRANSLATE('Incorrect Admin Password') };
-  passwordPolicy: string
-  passwordRecipe: string
-  constructor(
-    private userService: UserService,
-    private route: ActivatedRoute,
-    private errorHandler: TangyErrorHandler,
-    private router: Router,
-    private httpClient: HttpClient
-  ) { }
-
-  async ngOnInit() {
-    this.user = await this.userService.getAUserByUsername(this.route.snapshot.paramMap.get('username')) as User;
-    this.user.password = '';
-    this.user.confirmPassword = '';
-    try {
-      const result: any = await this.httpClient.get('/configuration/passwordPolicyConfig').toPromise();
-      if(result.T_PASSWORD_POLICY){
-        this.passwordPolicy = result.T_PASSWORD_POLICY;
-      }
-      if(result.T_PASSWORD_RECIPE){
-        this.passwordRecipe = result.T_PASSWORD_RECIPE;
-      }
-    } catch (error) {
-      if (typeof error.status === 'undefined') {
-        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-      }
-    }
-    this.passwordIsNotStrong.message = this.passwordIsNotStrong.message + ' ' + this.passwordRecipe
-  }
-
-  async editUser() {
-    try {
-      if (!this.updateUserPassword) {
-        this.user.password = null;
-        this.user.confirmPassword = null;
-      }
-      const policy = new RegExp(this.passwordPolicy)
-      if (!policy.test(this.user.password)) {
-        this.statusMessage = this.passwordIsNotStrong
-        // this.disableSubmit = true
-        // this.errorHandler.handleError(this.passwordIsNotStrong.message);
-        return
-      } else {
-        // Clear out statusMessage is it had been set earlier.
-        this.statusMessage = { type: '', message: '' };
-      }
-      const data = await this.userService.updateUserDetails({...this.user, updateUserPassword: this.updateUserPassword});
-      if (data === 200) {
-        this.errorHandler.handleError(_TRANSLATE('User Details Updated Successfully'));
-        this.router.navigate(['users'])
-      } else {
-        this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
-      }
-    } catch (error) {
-      this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
-    }
-  }
-}
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.css b/online-survey-app/src/app/core/auth/_components/login/login.component.css
deleted file mode 100644
index e347211a77..0000000000
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.css
+++ /dev/null
@@ -1,33 +0,0 @@
-mat-card {
-  width: 300px;
-  margin: 30px auto;
-}
-.mat-title {
-  color: var(--primary-color);
-  font-size: 1.5em;
-  font-weight: 400;
-  font-family: Roboto, 'Helvetica Nue', sans-serif;
-}
-#logo {
-  margin-bottom: 15px;
-}
-mat-placeholder i {
-  margin-right: 0.075em;
-}
-
-input.mat-input-element {
-  margin-top: 1em;
-}
-
-.mat-placeholder-required.mat-form-field-required-marker.ng-tns-c6-2
-  .mat-form-field-invalid
-  .mat-form-field-placeholder.mat-accent,
-.mat-form-field-invalid
-  .mat-form-field-placeholder
-  .mat-form-field-required-marker {
-  position: relative;
-  bottom: 1em !important;
-}
-.mat-form-field {
-  width: 100%;
-}
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.html b/online-survey-app/src/app/core/auth/_components/login/login.component.html
deleted file mode 100644
index 839e51e028..0000000000
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<mat-card *ngIf="!ready">
-  Loading...
-</mat-card>
-<mat-card *ngIf="ready">
-    <form role="form" #login='ngForm' novalidate>
-      <img id="logo" src="/logo.png" width="100%">
-      <mat-form-field>
-        <input matInput type="text" required [(ngModel)]="user.username" id="username" name="username">
-        <mat-placeholder>
-          <i class="material-icons app-input-icon">face</i>
-          <span>{{'Username'|translate}}</span>
-        </mat-placeholder>
-      </mat-form-field>
-      <br>
-      <mat-form-field>
-        <input matInput type="password" required [(ngModel)]="user.password" id="password" name="password">
-        <mat-placeholder>
-          <i class="material-icons app-input-icon">lock_open</i>
-          <span>{{'Password'|translate}}</span>
-        </mat-placeholder>
-      </mat-form-field>
-      <br>
-      <button (click)="loginUser()" mat-raised-button color="accent" name="action">{{'LOGIN'|translate}}</button>
-      <!-- <a [routerLink]="['/forgot-password']" mat-button color="primary">{{'Forgot Password?'|translate}}</a> -->
-      <a [routerLink]="['/register-user']" mat-button color="primary">{{'Register'|translate}}</a>
-      <span id="err">
-        <small>{{errorMessage}}</small>
-      </span>
-    </form>
-</mat-card>
-<span #customLoginMarkup></span>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
deleted file mode 100644
index d6d85a8465..0000000000
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { LoginComponent } from './login.component';
-
-describe('LoginComponent', () => {
-  let component: LoginComponent;
-  let fixture: ComponentFixture<LoginComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ LoginComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(LoginComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_components/login/login.component.ts b/online-survey-app/src/app/core/auth/_components/login/login.component.ts
deleted file mode 100644
index 4194829b0d..0000000000
--- a/online-survey-app/src/app/core/auth/_components/login/login.component.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
-import { AuthenticationService } from '../../_services/authentication.service';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-
-
-@Component({
-  selector: 'login',
-  templateUrl: './login.component.html',
-  styleUrls: ['./login.component.css']
-})
-export class LoginComponent implements OnInit {
-
-  errorMessage = '';
-  returnUrl: string; // stores the value of the url to redirect to after login
-  user = { username: '', password: '' };
-  @ViewChild('customLoginMarkup', {static: true}) customLoginMarkup: ElementRef;
-  ready = false
-
-  constructor(
-    private authenticationService: AuthenticationService,
-    private route: ActivatedRoute,
-    private router: Router,
-  ) { }
-
-  async ngOnInit() {
-    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || 'forms-list';
-
-    if (await this.authenticationService.isLoggedIn()) {
-      this.router.navigate([this.returnUrl]);
-      return;
-    }
-    this.customLoginMarkup.nativeElement.innerHTML = await this.authenticationService.getCustomLoginMarkup()
-    this.ready = true
-  }
-  async loginUser() {
-    try {
-
-      if (window.location.origin.startsWith('http://localhost')) {
-        // If we are running on localhost, we want to use the local server for authentication 
-        sessionStorage.setItem(this.user.username, this.user.password);
-        this.router.navigate([this.returnUrl]);
-      } else if (await this.authenticationService.login(this.user.username, this.user.password)) {
-        this.router.navigate([this.returnUrl]);
-      } else {
-        this.errorMessage = _TRANSLATE('Login Unsuccesful');
-      }
-    } catch (error) {
-      this.errorMessage = _TRANSLATE('Login Unsuccesful');
-      console.error(error);
-    }
-  }
-
-}
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
deleted file mode 100644
index 09e9b04a15..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.css
+++ /dev/null
@@ -1,4 +0,0 @@
-#container {
-  margin: 0px 15px;
-
-}
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
deleted file mode 100644
index dea5885145..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.html
+++ /dev/null
@@ -1,68 +0,0 @@
-<h1 class="tangy-foreground-secondary" *ngIf="user; else elseBlock">
-  {{'Edit User'|translate }} {{user.username}}
-</h1>
-<ng-template #elseBlock>
-  <h1 class="tangy-foreground-secondary">
-    {{'Edit User'|translate }}
-  </h1>
-</ng-template>
-<div id="container" *ngIf="user">
-  <form class="tangy-full-width" novalidate #editUserForm="ngForm">
-    
-    <mat-form-field class="tangy-full-width">
-      <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
-        required>
-        <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
-          {{'This Field is Required'|translate}}
-        </mat-error>
-    </mat-form-field>
-    <br>
-    <br>
-    <mat-form-field class="tangy-full-width">
-      <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
-        required>
-        <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
-          {{'This Field is Required'|translate}}
-        </mat-error>
-    </mat-form-field>
-    <br>
-    <br>
-    <mat-form-field class="tangy-full-width">
-      <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
-        matInput placeholder="{{'Email'|translate}}" required>
-      <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
-        {{'Please enter a valid email address'|translate}}
-      </mat-error>
-    </mat-form-field>
-    <br>
-    <br>
-    <mat-checkbox [(ngModel)]="updateUserPassword" name="updateUserPassword">Update User password?</mat-checkbox>
-    <br>
-    <br>
-    <div *ngIf="updateUserPassword">
-      <mat-form-field class="tangy-full-width">
-        <input name="password" type="password" [(ngModel)]="user.currentPassword" #currentPassword="ngModel" matInput placeholder="{{'Current Password'|translate}}"
-          required>
-      </mat-form-field>
-      <mat-form-field class="tangy-full-width">
-        <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'New Password'|translate}}"
-          required>
-      </mat-form-field>
-      <br>
-      <br>
-      <mat-form-field class="tangy-full-width">
-        <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
-          required>
-        <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
-          {{'Passwords do not match'|translate}}
-        </mat-error>
-      </mat-form-field>
-    </div>
-    <span [id]="statusMessage.type" *ngIf="statusMessage.type==='error'">
-        <small>{{statusMessage.message}}</small>
-    </span>
-    <p>
-    <button [disabled]="editUserForm.invalid||(user.confirmPassword!==user.password)||disableSubmit" mat-raised-button color="warn" (click)="editUser()">{{'UPDATE DETAILS'|translate}}</button>
-    </p>
-  </form>
-</div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
deleted file mode 100644
index e3bb688e39..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UpdatePersonalProfileComponent } from './update-personal-profile.component';
-
-describe('UpdatePersonalProfileComponent', () => {
-  let component: UpdatePersonalProfileComponent;
-  let fixture: ComponentFixture<UpdatePersonalProfileComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ UpdatePersonalProfileComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(UpdatePersonalProfileComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts b/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
deleted file mode 100644
index 12dee61574..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-personal-profile/update-personal-profile.component.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Router } from '@angular/router';
-import { Component, OnInit } from '@angular/core';
-import { MenuService } from 'src/app/shared/_services/menu.service';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import { UserService } from '../../_services/user.service';
-import { User } from '../user-registration/user.model.interface';
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-import {HttpClient} from "@angular/common/http";
-
-@Component({
-  selector: 'app-update-personal-profile',
-  templateUrl: './update-personal-profile.component.html',
-  styleUrls: ['./update-personal-profile.component.css']
-})
-export class UpdatePersonalProfileComponent implements OnInit {
-
-  user;
-  updateUserPassword = false;
-  statusMessage = { type: '', message: '' };
-  disableSubmit = false;
-  passwordIsNotStrong = { type: 'error', message: _TRANSLATE('Password is not strong enough.') };
-  incorrectAdminPassword = { type: 'error', message: _TRANSLATE('Incorrect Admin Password') };
-  passwordPolicy: string
-  passwordRecipe: string
-  config:any = { enabledModules: [] }
-  
-  constructor(
-    private menuService: MenuService,
-    private userService: UserService,
-    private errorHandler: TangyErrorHandler,
-    private router: Router,
-    private httpClient: HttpClient
-  ) { }
-
-  async ngOnInit() {
-    this.menuService.setContext(_TRANSLATE('Update My Profile'));
-    this.user = await this.userService.getMyUser();
-    this.user.password = '';
-    this.user.confirmPassword = '';
-    try {
-      const result: any = await this.httpClient.get('/configuration/passwordPolicyConfig').toPromise();
-      if(result.T_PASSWORD_POLICY){
-        this.passwordPolicy = result.T_PASSWORD_POLICY;
-      }
-      if(result.T_PASSWORD_RECIPE){
-        this.passwordRecipe = result.T_PASSWORD_RECIPE;
-      }
-    } catch (error) {
-      if (typeof error.status === 'undefined') {
-        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-      }
-    }
-    this.passwordIsNotStrong.message = this.passwordIsNotStrong.message + ' ' + this.passwordRecipe
-  }
-  async editUser() {
-    this.statusMessage = { type: '', message: '' };
-    try {
-      if (!this.updateUserPassword) {
-        this.user.password = null;
-        this.user.confirmPassword = null;
-        this.user.currentPassword = null;
-      }
-
-      // const policy = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})');
-      const policy = new RegExp(this.passwordPolicy)
-      if (!policy.test(this.user.password)) {
-        this.statusMessage = this.passwordIsNotStrong
-        // this.disableSubmit = true
-        // this.errorHandler.handleError(this.passwordIsNotStrong.message);
-        return
-      } else {
-        // Clear out statusMessage is it had been set earlier.
-        this.statusMessage = { type: '', message: '' };
-      }
-      
-      const data = await this.userService.updateMyUser({...this.user, updateUserPassword: this.updateUserPassword});
-      if (data === 200) {
-        this.errorHandler.handleError(_TRANSLATE('User Details Updated Successfully'));
-        this.router.navigate(['/'])
-      } else {
-        this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
-      }
-    } catch (error) {
-      this.errorHandler.handleError(_TRANSLATE('User Details could not be Updated'));
-    }
-  }
-}
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
deleted file mode 100644
index 03834b16a8..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.tangy-input-width{
-    width: 80%;
-}
-
-p {
-    margin: 15px;
-}
-
-mat-checkbox{
-    display: block;
-    margin: 0 10px;
-}
-
-button {
-    margin-left: 15px;
-}
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
deleted file mode 100644
index 81450a32a7..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<app-breadcrumb [title]="title" [breadcrumbs]="breadcrumbs"></app-breadcrumb>
-<form class="tangy-full-width" novalidate #updateUserRoleForm="ngForm">
-    <div>
-        <p>{{'Update Role of' | translate}}: {{username}}</p>
-
-    </div>
-    <p *ngIf="allRoles" class="tangy-input-width">
-        <mat-checkbox #f (change)="onSelectChange(role.role, f.checked)"
-            [checked]="doesUserHaveRole(role.role)" *ngFor="let role of allRoles" [name]="role.role">
-            {{role.role}}
-        </mat-checkbox>
-    </p>
-    <p *ngIf="allRoles.length<1" class="tangy-input-width">{{'No Roles exist yet. '|translate}}</p>
-    <button [disabled]="!username" mat-raised-button color="warn"
-        (click)="addUserToGroup();updateUserRoleForm.reset()">{{"submit"|translate}}</button>
-</form>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
deleted file mode 100644
index 1db6f21c83..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UpdateUserRoleComponent } from './update-user-role.component';
-
-describe('UpdateUserRoleComponent', () => {
-  let component: UpdateUserRoleComponent;
-  let fixture: ComponentFixture<UpdateUserRoleComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ UpdateUserRoleComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(UpdateUserRoleComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts b/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
deleted file mode 100644
index 497de34091..0000000000
--- a/online-survey-app/src/app/core/auth/_components/update-user-role/update-user-role.component.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Component, OnInit, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
-import { Breadcrumb } from 'src/app/shared/_components/breadcrumb/breadcrumb.component';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import { ActivatedRoute, Router } from '@angular/router';
-import { GroupsService } from 'src/app/groups/services/groups.service';
-import { UserService } from '../../_services/user.service';
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-import { AuthenticationService } from '../../_services/authentication.service';
-
-@Component({
-  selector: 'app-update-user-role',
-  templateUrl: './update-user-role.component.html',
-  styleUrls: ['./update-user-role.component.css']
-})
-export class UpdateUserRoleComponent implements OnInit {
-
-  user;
-  username;
-  role;
-  groupId;
-  title = _TRANSLATE('Update User\'s roles');
-  breadcrumbs: Array<Breadcrumb> = [];
-  allRoles;
-  myGroup;
-  @ViewChild('search', { static: false }) search: ElementRef;
-  constructor(
-    private route: ActivatedRoute,
-    private groupsService: GroupsService,
-    private usersService: UserService,
-    private errorHandler: TangyErrorHandler,
-    private router: Router,
-    private authenticationService: AuthenticationService
-  ) { }
-
-  async ngOnInit() {
-    this.breadcrumbs = [
-      <Breadcrumb>{
-        label: _TRANSLATE('Security'),
-        url: `security`
-      },
-      <Breadcrumb>{
-        label: _TRANSLATE(`Update User's Roles`),
-        url: `security/assign-role`
-      }
-    ];
-    this.username = this.route.snapshot.paramMap.get('username');
-    this.groupId = this.route.snapshot.paramMap.get('groupId');
-    this.allRoles = await this.authenticationService.getAllRoles(this.groupId);
-    this.user = await this.usersService.getAUserByUsername(this.username);
-    this.myGroup = this.user.groups.find(g => g.groupName === this.groupId);
-    this.myGroup.roles = this.myGroup?.roles ?? [];
-    this.role = { groupName: this.groupId, roles: this.myGroup.roles };
-  }
-
-  async addUserToGroup() {
-    try {
-      await this.groupsService.addUserToGroup(this.groupId, this.username, this.role);
-      this.errorHandler.handleError(_TRANSLATE('User Added to Group Successfully'));
-      this.router.navigate([`groups/${this.groupId}`]);
-    } catch (error) {
-      console.log(error);
-    }
-  }
-
-  doesUserHaveRole(role) {
-    return this.myGroup.roles.indexOf(role) >= 0;
-  }
-  onSelectChange(role, value) {
-    if (value) {
-      this.role.roles = [...new Set([...this.role.roles, role])];
-    } else {
-      this.role.roles = this.role.roles.filter(perm => perm !== role);
-    }
-  }
-
-}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
deleted file mode 100644
index 6c157bdf31..0000000000
--- a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.tangy-full-width {
-  width: 98%;
-}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
deleted file mode 100644
index f80e1a10f4..0000000000
--- a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<div class="tangy-content-top-margin">
-<mat-card>
-  <mat-card-title class="tangy-foreground-secondary">
-    {{'Add New User'|translate}}
-  </mat-card-title>
-  <mat-card-content>
-    <form class="tangy-full-width" novalidate #registrationForm="ngForm">
-      <mat-form-field class="tangy-full-width">
-        <input name="username" [(ngModel)]="user.username" #username="ngModel" matInput placeholder="{{'Username'|translate}}" (blur)="doesUserExist(user.username)"
-          required>
-        <br>
-        <br>
-        <span>
-          <mat-error *ngIf="userExists" style="font-size:75%;"> {{'Username Unavailable'|translate}}</mat-error>
-          <mat-error *ngIf="(userExists!==null&&!userExists)&&(username.dirty || username.touched)" style="font-size:75%;color: green"> {{'Username Available'|translate}}</mat-error>
-          <mat-error *ngIf="(username.invalid||username.errors) && (username.dirty || username.touched)">
-            {{'This Field is Required'|translate}}
-          </mat-error>
-        </span>
-      </mat-form-field>
-      <br>
-      <br>
-
-      <mat-form-field class="tangy-full-width">
-        <input name="firstName" [(ngModel)]="user.firstName" #firstName="ngModel" matInput placeholder="{{'First Name'|translate}}"
-          required>
-          <mat-error *ngIf="(firstName.invalid||firstName.errors) && (firstName.dirty || firstName.touched)">
-            {{'This Field is Required'|translate}}
-          </mat-error>
-      </mat-form-field>
-      <br>
-      <br>
-      <mat-form-field class="tangy-full-width">
-        <input name="lastName" [(ngModel)]="user.lastName" #lastName="ngModel" matInput placeholder="{{'Last Name'|translate}}"
-          required>
-          <mat-error *ngIf="(lastName.invalid||lastName.errors) && (lastName.dirty || lastName.touched)">
-            {{'This Field is Required'|translate}}
-          </mat-error>
-      </mat-form-field>
-      <br>
-      <br>
-      <mat-form-field class="tangy-full-width">
-        <input name="email" [(ngModel)]="user.email" #email="ngModel" name="email" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
-          matInput placeholder="{{'Email'|translate}}" required>
-        <mat-error *ngIf="(email.invalid||email.errors) && (email.dirty || email.touched)">
-          {{'Please enter a valid email address'|translate}}
-        </mat-error>
-      </mat-form-field>
-      <br>
-      <br>
-      <mat-form-field class="tangy-full-width">
-        <input name="password" [(ngModel)]="user.password" #password="ngModel" type="password" matInput placeholder="{{'Password'|translate}}"
-          required>
-      </mat-form-field>
-      <br>
-      <br>
-      <mat-form-field class="tangy-full-width">
-        <input name="confirmPassword" [(ngModel)]="user.confirmPassword" #confirmPassword="ngModel" type="password" matInput placeholder="{{'Confirm Password'|translate}}"
-          required>
-        <mat-error *ngIf="(user.password!==user.confirmPassword) && ((confirmPassword.dirty || confirmPassword.touched)||(password.dirty||password.touched))">
-          {{'Passwords do not match'|translate}}
-        </mat-error>
-      </mat-form-field>
-      <br>
-      <br>
-      <button [disabled]="registrationForm.invalid||userExists" mat-raised-button color="warn" (click)="createUser();registrationForm.reset()">{{'REGISTER'|translate}}</button>
-    </form>
-  </mat-card-content>
-</mat-card>
-</div>
\ No newline at end of file
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
deleted file mode 100644
index 39b3cb325f..0000000000
--- a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UserRegistrationComponent } from './user-registration.component';
-
-describe('UserRegistrationComponent', () => {
-  let component: UserRegistrationComponent;
-  let fixture: ComponentFixture<UserRegistrationComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ UserRegistrationComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(UserRegistrationComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
deleted file mode 100644
index 48bd0d4359..0000000000
--- a/online-survey-app/src/app/core/auth/_components/user-registration/user-registration.component.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Component, OnInit, AfterContentInit } from '@angular/core';
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-import { UserService } from '../../_services/user.service';
-import { User } from './user.model.interface';
-import {ActivatedRoute} from '@angular/router';
-
-
-@Component({
-  selector: 'app-user-registration',
-  templateUrl: './user-registration.component.html',
-  styleUrls: ['./user-registration.component.css']
-})
-export class UserRegistrationComponent implements OnInit, AfterContentInit {
-
-  user: User = {
-    username: '',
-    password: '',
-    email: '',
-    firstName: '',
-    lastName: ''
-  };
-  userExists: boolean = null;
-  userNameUnavailableMessage = { type: 'error', message: _TRANSLATE('Username Unavailable') };
-  userNameAvailableMessage = { type: 'success', message: _TRANSLATE('Username Available') };
-  couldNotCreateUserMessage = { type: 'error', message: _TRANSLATE('Could Not Create User') };
-  id: string;
-
-  constructor(private userService: UserService, private errorHandler: TangyErrorHandler, private route: ActivatedRoute) { }
-
-  ngOnInit() {
-  }
-
-  async ngAfterContentInit() {
-    this.route.params.subscribe(async params => {
-      this.id = params.id;
-      // await this.userService.doesUserExist(this.id);
-    })
-  }
-
-  async createUser() {
-    try {
-      delete this.user.confirmPassword;
-      const userData = Object.assign({}, this.user);
-      if (!(await this.doesUserExist(this.user.username))) {
-        const result: any = await this.userService.createUser(userData);
-        if (result && result.statusCode && result.statusCode === 200) {
-          this.errorHandler.handleError(_TRANSLATE('User Created Succesfully'));
-          this.user = {
-            username: '',
-            password: '',
-            email: '',
-            firstName: '',
-            lastName: ''
-          };
-        } else {
-          this.errorHandler.handleError(_TRANSLATE('Could Not Create User'));
-        }
-        return result;
-      }
-    } catch (error) {
-      this.errorHandler.handleError(_TRANSLATE('Could Not Create User'));
-    }
-  }
-
-  async doesUserExist(username: string) {
-    try {
-      this.user.username = username.replace(/\s/g, ''); // Remove all whitespaces including spaces and tabs
-      if (this.user.username.length > 0) {
-        this.userExists = <boolean>await this.userService.doesUserExist(username);
-        return this.userExists;
-      } else {
-        this.userExists = null;
-        return this.userExists;
-      }
-    } catch (error) {
-      console.error(error);
-    }
-  }
-
-}
diff --git a/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts b/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
deleted file mode 100644
index 3346f356e9..0000000000
--- a/online-survey-app/src/app/core/auth/_components/user-registration/user.model.interface.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface User {
-    username: string;
-    password: string;
-    email: string;
-    firstName:string;
-    lastName:string;
-    confirmPassword?: string;
-}
diff --git a/online-survey-app/src/app/core/auth/_services/authentication.service.ts b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
index 1f67df0f17..2f30577f07 100644
--- a/online-survey-app/src/app/core/auth/_services/authentication.service.ts
+++ b/online-survey-app/src/app/core/auth/_services/authentication.service.ts
@@ -16,25 +16,6 @@ export class AuthenticationService {
     this.currentUserLoggedIn$ = new Subject();
   }
 
-  async login(username: string, password: string) {
-    try {
-      const data = await this.http.post('/login', {username, password}, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        const token = data.body['data']['token'];
-        await this.setTokens(token);
-        return true;
-      } else {
-        return false;
-      }
-    } catch (error) {
-      console.error(error);
-      sessionStorage.removeItem('token');
-      sessionStorage.removeItem('user_id');
-      sessionStorage.removeItem('permissions');
-      return false;
-    }
-  }
-
   async surveyLogin(accessCode: string) {
     const appConfig = await this.appConfigService.getAppConfig();
     const groupId = appConfig['groupId'];
diff --git a/online-survey-app/src/app/core/auth/_services/user.service.spec.ts b/online-survey-app/src/app/core/auth/_services/user.service.spec.ts
deleted file mode 100644
index b26195c25d..0000000000
--- a/online-survey-app/src/app/core/auth/_services/user.service.spec.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { TestBed, inject } from '@angular/core/testing';
-
-import { UserService } from './user.service';
-
-describe('UserService', () => {
-  beforeEach(() => {
-    TestBed.configureTestingModule({
-      providers: [UserService]
-    });
-  });
-
-  it('should be created', inject([UserService], (service: UserService) => {
-    expect(service).toBeTruthy();
-  }));
-});
diff --git a/online-survey-app/src/app/core/auth/_services/user.service.ts b/online-survey-app/src/app/core/auth/_services/user.service.ts
deleted file mode 100644
index 55c91fd7b1..0000000000
--- a/online-survey-app/src/app/core/auth/_services/user.service.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-
-import { TangyErrorHandler } from 'src/app/shared/_services/tangy-error-handler.service';
-import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
-
-@Injectable()
-export class UserService {
-
-  constructor(private httpClient: HttpClient, private errorHandler: TangyErrorHandler, private http: HttpClient) { }
-
-  async createUser(payload) {
-    try {
-      if (!(await this.doesUserExist(payload.username))) {
-        const result = await this.httpClient.post('/users/register-user', payload).toPromise();
-        return result;
-      }
-
-    } catch (error) {
-      this.showError(error);
-    }
-
-  }
-
-  async doesUserExist(username: string) {
-    try {
-      const result: any = await this.httpClient.get(`/users/userExists/${username}`).toPromise();
-      return result.data;
-    } catch (error) {
-      this.showError(error);
-
-    }
-  }
-
-  async getAllUsers() {
-    try {
-      const users: any = await this.httpClient.get('/users').toPromise();
-      return users.data;
-    } catch (error) {
-      this.showError(error);
-
-    }
-  }
-  
-  async getCurrentUser() {
-    return await sessionStorage.getItem('user_id');
-  }
-  private showError(error: any) {
-    console.log(error);
-    if (typeof error.status === 'undefined') {
-      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-    }
-  }
-
-  async canManageSitewideUsers() {
-    return <boolean>await this.http.get('/user/permission/can-manage-sitewide-users').toPromise()
-  }
-
-  async getMyUser() {
-    try {
-      if (sessionStorage.getItem('user_id') === 'user1') {
-        return {
-          email: 'user1@tangerinecentral.org',
-          firstName: 'user1',
-          lastName: 'user1',
-          username: 'user1',
-          _id: 'user1'
-        }
-      } else {
-        const data = await this.http.get(`/users/findMyUser/`, {observe: 'response'}).toPromise();
-        if (data.status === 200) {
-          return data.body['data'];
-        }
-      }
-    } catch (error) {
-      console.error(error);
-      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-    }
-  }
-
-  async getAUserByUsername(username) {
-    try {
-      const data = await this.http.get(`/users/findOneUser/${username}`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.body['data'];
-      }
-    } catch (error) {
-      console.error(error);
-      this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-    }
-  }
-
-  async searchUsersByUsername(username: string) {
-    try {
-      const data: any = await this.httpClient
-        .get(`/users/byUsername/${username}`)
-        .toPromise();
-      return data.data;
-    } catch (error) {
-      console.error(error);
-      if (typeof error.status === 'undefined') {
-        this.errorHandler.handleError(_TRANSLATE('Could Not Contact Server.'));
-      }
-    }
-  }
-
-  async deleteUser(username: string) {
-    try {
-      const data = await this.http.delete(`/users/delete/${username}`, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return true;
-      }
-    } catch (error) {
-      console.error(error);
-      return false;
-    }
-  }
-  async restoreUser(username: string) {
-    try {
-      const data = await this.http.patch(`/users/restore/${username}`, {isActive: true}, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return true;
-      }
-    } catch (error) {
-      console.error(error);
-      return false;
-    }
-  }
-
-  async updateUserDetails(payload) {
-    try {
-      const data = await this.httpClient.put(`/users/update/${payload.username}`, payload, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.status;
-      }
-    } catch (error) {
-      console.error(error);
-      return 500;
-    }
-  }
-  async updateMyUser(payload) {
-    try {
-      const data = await this.httpClient.put(`/users/updateMyUser/`,
-      payload, {observe: 'response'}).toPromise();
-      if (data.status === 200) {
-        return data.status;
-      }
-    } catch (error) {
-      console.error(error);
-      return 500;
-    }
-  }
-
-}
diff --git a/online-survey-app/src/app/core/auth/auth-routing.module.ts b/online-survey-app/src/app/core/auth/auth-routing.module.ts
index e0b5fa9b74..1804ab241c 100644
--- a/online-survey-app/src/app/core/auth/auth-routing.module.ts
+++ b/online-survey-app/src/app/core/auth/auth-routing.module.ts
@@ -1,13 +1,8 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 import { SurveyLoginComponent } from './_components/survey-login/survey-login.component';
-import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
 
 const routes: Routes = [
-  {
-    path: 'register-user',
-    component: UserRegistrationComponent
-  },
   {
     path: 'survey-login',
     component: SurveyLoginComponent
diff --git a/online-survey-app/src/app/core/auth/auth.module.ts b/online-survey-app/src/app/core/auth/auth.module.ts
index 5e6cf3ba04..a6eed353c6 100644
--- a/online-survey-app/src/app/core/auth/auth.module.ts
+++ b/online-survey-app/src/app/core/auth/auth.module.ts
@@ -10,12 +10,9 @@ import { MatTableModule } from '@angular/material/table';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { SharedModule } from '../../shared/shared.module';
 import { AuthRoutingModule } from './auth-routing.module';
-import { LoginComponent } from './_components/login/login.component';
 import { SurveyLoginComponent } from './_components/survey-login/survey-login.component';
-import { UserRegistrationComponent } from './_components/user-registration/user-registration.component';
 import { LoginGuard } from './_guards/login-guard.service';
 import { AuthenticationService } from './_services/authentication.service';
-import { UserService } from './_services/user.service';
 import { MatCheckboxModule } from '@angular/material/checkbox';
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
 
@@ -38,7 +35,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
         MatCheckboxModule
 
     ],
-  declarations: [UserRegistrationComponent, LoginComponent, SurveyLoginComponent],
-  providers: [AuthenticationService, LoginGuard, UserService]
+  declarations: [SurveyLoginComponent],
+  providers: [AuthenticationService, LoginGuard]
 })
 export class AuthModule { }

From 9ac8e2dd965cd37017829afd359577d139a0f031 Mon Sep 17 00:00:00 2001
From: Erik Surface <esurface@rti.org>
Date: Fri, 10 Jan 2025 15:19:36 -0500
Subject: [PATCH 57/59] Updates and fixes for locked and unlocked
 online-survey-app

---
 .../app/core/auth/_guards/login-guard.service.ts   |  5 +++--
 online-survey-app/src/assets/forms.json            | 14 ++++++++++++++
 server/src/online-survey.js                        |  2 +-
 3 files changed, 18 insertions(+), 3 deletions(-)
 create mode 100755 online-survey-app/src/assets/forms.json

diff --git a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
index 1f4ac789a3..c6b845562d 100644
--- a/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
+++ b/online-survey-app/src/app/core/auth/_guards/login-guard.service.ts
@@ -16,9 +16,10 @@ export class LoginGuard implements CanActivate {
     if (config['requireAccessCode'] === 'true') {
       if (await this.authenticationService.isLoggedIn()) {
         return true;
+      } else {
+        this.router.navigate(['survey-login'], { queryParams: { returnUrl: state.url } });
+        return false;
       }
-      this.router.navigate(['survey-login'], { queryParams: { returnUrl: state.url } });
-      return false;
     }
 
     return true;
diff --git a/online-survey-app/src/assets/forms.json b/online-survey-app/src/assets/forms.json
new file mode 100755
index 0000000000..e89d753f86
--- /dev/null
+++ b/online-survey-app/src/assets/forms.json
@@ -0,0 +1,14 @@
+[
+  {
+    "id": "user-profile",
+    "src": ".\/assets\/user-profile\/form.html",
+    "title": "User Profile",
+    "listed": false
+  },
+  {
+    "id": "reports",
+    "src": ".\/reports\/form.html",
+    "title": "Reports",
+    "listed": false
+  }
+]
\ No newline at end of file
diff --git a/server/src/online-survey.js b/server/src/online-survey.js
index c410c2a912..d551ff993a 100644
--- a/server/src/online-survey.js
+++ b/server/src/online-survey.js
@@ -10,7 +10,7 @@ const login = async (req, res) => {
     const { groupId, accessCode } = req.params;
 
     const groupDb = new DB(groupId)
-    let options = { key: accessCode, include_docs: true }
+    let options = { key: accessCode, include_docs: true, reduce: false }
     if (req.params.limit) {
       options.limit = req.params.limit
     }

From b2e2f9e77becdfdab88f1f2fe04906ba3e9590a8 Mon Sep 17 00:00:00 2001
From: Erik Surface <esurface@rti.org>
Date: Fri, 10 Jan 2025 15:24:06 -0500
Subject: [PATCH 58/59] Remove auth directives from online-survey-app

---
 .../has-a-permission.directive.spec.ts        |  8 -----
 .../_directives/has-a-permission.directive.ts | 31 -------------------
 .../has-all-permissions.directive.spec.ts     |  8 -----
 .../has-all-permissions.directive.ts          | 29 -----------------
 .../has-some-permissions.directive.spec.ts    |  8 -----
 .../has-some-permissions.directive.ts         | 31 -------------------
 6 files changed, 115 deletions(-)
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
 delete mode 100644 online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts

diff --git a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
deleted file mode 100644
index 403650b0ba..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { HasAPermissionDirective } from './has-a-permission.directive';
-
-describe('HasAPermissionDirective', () => {
-  it('should create an instance', () => {
-    const directive = new HasAPermissionDirective();
-    expect(directive).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
deleted file mode 100644
index 1204beeb01..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-a-permission.directive.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
-import { AuthenticationService } from '../_services/authentication.service';
-
-@Directive({
-  selector: '[appHasAPermission]'
-})
-export class HasAPermissionDirective implements OnInit {
-  private _groupId;
-  private _permission;
-  @Input()
-  set appHasAPermissionGroup(value: string) {
-    this._groupId = value;
-  }
-  @Input()
-  set appHasAPermissionPermission(value: string[]) {
-    this._permission = value;
-  }
-
-  constructor(private authenticationService: AuthenticationService,
-    private templateRef: TemplateRef<any>,
-    private viewContainer: ViewContainerRef) { }
-  async ngOnInit() {
-    const isPermitted = await this.authenticationService.doesUserHaveAPermission(this._groupId, this._permission);
-    if (isPermitted) {
-      this.viewContainer.createEmbeddedView(this.templateRef);
-    } else {
-      this.viewContainer.clear();
-    }
-  }
-
-}
diff --git a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
deleted file mode 100644
index 00447ee7b3..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { HasAllPermissionsDirective } from './has-all-permissions.directive';
-
-describe('HasAllPermissionsDirective', () => {
-  it('should create an instance', () => {
-    const directive = new HasAllPermissionsDirective();
-    expect(directive).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
deleted file mode 100644
index 4f19f18b07..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-all-permissions.directive.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Directive, Input, AfterViewInit, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
-import { AuthenticationService } from '../_services/authentication.service';
-@Directive({
-  selector: '[appHasAllPermissions]'
-})
-export class HasAllPermissionsDirective implements OnInit {
-  private _groupId;
-  private _permissions;
-  @Input()
-  set appHasAllPermissionsGroup(value: string) {
-    this._groupId = value;
-  }
-  @Input()
-  set appHasAllPermissionsPermissions(value: string[]) {
-    this._permissions = value;
-  }
-
-  constructor(private authenticationService: AuthenticationService,
-    private templateRef: TemplateRef<any>,
-    private viewContainer: ViewContainerRef) { }
-  async ngOnInit() {
-    const isPermitted = await this.authenticationService.doesUserHaveAllPermissions(this._groupId, this._permissions);
-    if (isPermitted) {
-      this.viewContainer.createEmbeddedView(this.templateRef);
-    } else {
-      this.viewContainer.clear();
-    }
-  }
-}
diff --git a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
deleted file mode 100644
index 83187b8309..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { HasSomePermissionsDirective } from './has-some-permissions.directive';
-
-describe('HasSomePermissionsDirective', () => {
-  it('should create an instance', () => {
-    const directive = new HasSomePermissionsDirective();
-    expect(directive).toBeTruthy();
-  });
-});
diff --git a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts b/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts
deleted file mode 100644
index e1dcdbcd77..0000000000
--- a/online-survey-app/src/app/core/auth/_directives/has-some-permissions.directive.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
-import { AuthenticationService } from '../_services/authentication.service';
-
-@Directive({
-  selector: '[appHasSomePermissions]'
-})
-export class HasSomePermissionsDirective implements OnInit {
-  private _groupId;
-  private _permissions;
-  @Input()
-  set appHasSomePermissionsGroup(value: string) {
-    this._groupId = value;
-  }
-  @Input()
-  set appHasSomePermissionsPermissions(value: string[]) {
-    this._permissions = value;
-  }
-
-  constructor(private authenticationService: AuthenticationService,
-    private templateRef: TemplateRef<any>,
-    private viewContainer: ViewContainerRef) { }
-  async ngOnInit() {
-    const isPermitted = await this.authenticationService.doesUserHaveSomePermissions(this._groupId, this._permissions);
-    if (isPermitted) {
-      this.viewContainer.createEmbeddedView(this.templateRef);
-    } else {
-      this.viewContainer.clear();
-    }
-  }
-
-}

From 5813f6e052e4f646b95b0e0e75c59da824338e24 Mon Sep 17 00:00:00 2001
From: Erik Surface <esurface@rti.org>
Date: Mon, 13 Jan 2025 15:12:22 -0400
Subject: [PATCH 59/59] Update CHANGELOG and fixes to case-api

---
 CHANGELOG.md                                  | 60 +++++++++++++++++++
 .../case-module/client/case-type-1.json       | 12 ++++
 .../client/custom-login-markup.html           | 10 ++++
 server/src/case-api.js                        |  2 +
 server/src/releases.js                        |  2 -
 5 files changed, 84 insertions(+), 2 deletions(-)
 create mode 100644 content-sets/case-module/client/custom-login-markup.html

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71d9333526..9adaf62700 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,65 @@
 # What's new
 
+## v3.32.0
+
+Tangerine v3.32.0 contains a major update to the deployment and fuctionality of Online Surveys including the introduction of authentication and case association. 
+
+__Online Survey Authentication__
+
+Online Surveys can now be released for public access or require authentication. When authentication is required, data collectors will be required to provide a device user Access Code to access the survey. The survey will be associated with the device user who provided the short code. 
+
+To deploy an Online Survey and require authentication:
+1. Create the form
+2. Add the `"requireAccessCode": true` property to the `app-config.json` file
+3. Navigate to Deploy > Release Online Survey for the group
+4. In the Unpublished Surveys list, click the <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="rgb(255, 149, 34)"><path d="M240-80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h40v-80q0-83 58.5-141.5T480-920q83 0 141.5 58.5T680-720v80h40q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Zm0-80h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM360-640h240v-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80ZM240-160v-400 400Z"/></svg> icon to deploy a survey and require an access code.
+
+__Custom Markdown for Online Survey Login Page__
+
+Custom styling (text, logos, formatting etc.) can be added to the login page for Online Surveys. In the content set repository, create a file called `client/custom-login-markup.html`. The file can contain any valid html. Any images can be added to the `media` folder and used in the html. Example [Custom Login Markup](content-sets/case-module/client/custom-login-markup.html).
+
+__Online Survey Case Form Authentication__
+
+Online Surveys with authentication can also be configured when using the Case Module. This is useful for studies who want to deploy secure forms without needing to install a full PWA or APK for one form associated with the case. For example, a mother-child cohort study deploys Tangerine to track the health of the mother and child after birth. Labs are collected in the field and sent for analysis. Instead of requiring the lab to install a tablet or PWA with the Tangerine app, the lab forms can be deployed online to simplify the completion process.
+
+To configure forms for secure deployment online in Case, add the `"allowOnline": true` property to the `eventFormDefinitions` section in the case definition file. See the form definition for `form-allowed-online-survey` in [case-type-1](./content-sets/case-module/client/case-type-1.json).
+
+__Online Survey Help Link__
+
+A help link can be added to the web pages for Online Surveys and will appear with the <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg> icon in the header. Define the link url by adding the `"helpLink": "https://www.tangerinecentral.org"` property to the `app-config.json` file. When clicked, the link will open in a new browser window.
+
+__Tangerine Case APIs__
+
+- `/userProfile/createUserProfile/:groupId`
+    - Creates a user-profile document in the group
+    - Additional body data in json format will add properties to the user-profile: `{ "age": "7", "dob": "1/1/2018"}`
+- `/case/createCase/:groupId/:caseDefinitionId`
+    - Creates a case in the group with the case type defined by the case definition id
+    - Additional body data in json format will add properties to the case: `{ "age": "7", "dob": "1/1/2018"}`
+- `/case/readCase/:groupId/:caseId`
+    - Read a case from the group with the case id
+- `/case/createCaseEvent/:groupId/:caseId/:caseDefinitionId/:caseEventDefinitionId`
+    - Creates an event with the event type as defined in the case definition
+- `/case/createParticipant/:groupId/:caseId/:caseDefinitionId/:caseRoleId`
+    - Creates a participant with the case role as defined in the case definition
+- `/case/getCaseEventFormSurveyLinks/:groupId/:caseId/`
+    Returns a JSON document with the urls for all case forms with active online surveys in the format:
+    ```json
+    {
+      "eventDefinitionId": event.caseEventDefinitionId,
+      "eventFormDefinitionId": eventForm.eventFormDefinitionId,
+      "formId": formId,
+      "url": url
+    }
+    ```
+
+__Server upgrade instructions__
+
+See the [Server Upgrade Insturctions](https://docs.tangerinecentral.org/system-administrator/upgrade-instructions).
+
+*Special Instructions for this release:* N/A
+
+
 ## v3.31.2
 
 __General Updates__
diff --git a/content-sets/case-module/client/case-type-1.json b/content-sets/case-module/client/case-type-1.json
index e515ce6701..ed80912446 100644
--- a/content-sets/case-module/client/case-type-1.json
+++ b/content-sets/case-module/client/case-type-1.json
@@ -137,6 +137,18 @@
           "allowDeleteIfFormNotStarted": true,
           "required": false,
           "repeatable": true 
+        },
+        {
+          "id": "event-form-definition-h9d3hd9",
+          "formId": "form-allowed-online-survey",
+          "forCaseRole": "role-2",
+          "name": "A form allowed to be filled online",
+          "autoPopulate": true,
+          "allowDeleteIfFormNotCompleted": true,
+          "allowDeleteIfFormNotStarted": true,
+          "required": false,
+          "repeatable": true,
+          "allowOnline": true
         }
       ]
     },
diff --git a/content-sets/case-module/client/custom-login-markup.html b/content-sets/case-module/client/custom-login-markup.html
new file mode 100644
index 0000000000..d5d7584458
--- /dev/null
+++ b/content-sets/case-module/client/custom-login-markup.html
@@ -0,0 +1,10 @@
+<div>
+  <img id="logo" src="./assets/media/logo.png" width="100%">
+</div>
+<div>
+  <center><strong>Online Survey for Tangerine Study</strong></center>
+  <br>
+  <p>
+    <small>Enter the provided Access Code below.</small>
+  </p>
+</div>
\ No newline at end of file
diff --git a/server/src/case-api.js b/server/src/case-api.js
index 6572b77e26..d5ac21aff1 100644
--- a/server/src/case-api.js
+++ b/server/src/case-api.js
@@ -88,6 +88,7 @@ createCase = async (req, res) => {
 
 readCase = async (req, res) => {
   const groupDb = new DB(req.params.groupId)
+  let caseId = req.params.caseId
   let data = {}
   try {
     data = await groupDb.get(caseId);
@@ -123,6 +124,7 @@ createEventForm = async (req, res) => {
   let groupId = req.params.groupId
   let caseId = req.params.caseId
   let caseEventId = req.params.caseEventId
+  let caseDefinitionId = req.params.caseDefinitionId
   let eventFormDefinitionId = req.params.eventFormDefinitionId
 
   const caseDefinition = _getCaseDefinition(groupId, caseDefinitionId)
diff --git a/server/src/releases.js b/server/src/releases.js
index 21f8b20acb..8735df3312 100644
--- a/server/src/releases.js
+++ b/server/src/releases.js
@@ -63,7 +63,6 @@ const releasePWA = async (req, res)=>{
 
 const releaseOnlineSurveyApp = async(req, res) => {
 	// @TODO Make sure user is member of group.
-	debugger;
 	const groupId = sanitize(req.params.groupId)
 	const formId = sanitize(req.params.formId)
 	const releaseType = sanitize(req.params.releaseType)
@@ -75,7 +74,6 @@ const releaseOnlineSurveyApp = async(req, res) => {
 	} else {
 		uploadKey = sanitize(req.body.uploadKey)
 	}
-	debugger;
 	const requireAccessCode = req.body.locked ? req.body.locked : false
 
 	try {