Skip to content
Merged

Saml #1344

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 166 additions & 277 deletions frontend/CLAUDE.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
FROM public.ecr.aws/docker/library/node:18-alpine as builder
ARG VERSION
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn global add @angular/cli && yarn install
COPY angular.json browserslist tsconfig.app.json tsconfig.json tslint.json ./
COPY scripts ./scripts/
COPY src ./src/

# Update version if VERSION build arg is provided
RUN if [ -n "$VERSION" ]; then \
echo "Updating package.json version to $VERSION" && \
npm version $VERSION --no-git-tag-version && \
yarn update-version; \
fi

RUN ng build --configuration=production

FROM public.ecr.aws/docker/library/nginx:alpine
Expand Down
9 changes: 5 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"name": "dissendium-v0",
"version": "0.0.0",
"version": "1.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"start": "node scripts/update-version.js && ng serve",
"build": "node scripts/update-version.js && ng build",
"test": "ng test",
"test:ci": "ng test --watch=false --browsers=ChromeHeadlessCustom",
"lint": "ng lint",
"e2e": "ng e2e",
"analyze": "webpack-bundle-analyzer dist/dissendium-v0/stats.json",
"build:stats": "ng build --stats-json"
"build:stats": "node scripts/update-version.js && ng build --stats-json",
"update-version": "node scripts/update-version.js"
},
"private": true,
"dependencies": {
Expand Down
16 changes: 16 additions & 0 deletions frontend/scripts/update-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const fs = require('fs');
const path = require('path');

// Read package.json to get the version
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const version = packageJson.version;

// Update the version.ts file
const versionTsPath = path.join(__dirname, '..', 'src', 'app', 'version.ts');
const versionTsContent = `// This file is auto-generated during build
export const version = '${version}';
`;

fs.writeFileSync(versionTsPath, versionTsContent);
console.log(`Updated version.ts with version ${version}`);
2 changes: 2 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { UserDeletedSuccessComponent } from './components/user-deleted-success/u
import { UserSettingsComponent } from './components/user-settings/user-settings.component';
import { UsersComponent } from './components/users/users.component';
import { ZapierComponent } from './components/zapier/zapier.component';
import { SsoComponent } from './components/sso/sso.component';

const routes: Routes = [
{path: '', redirectTo: '/connections-list', pathMatch: 'full'},
Expand All @@ -46,6 +47,7 @@ const routes: Routes = [
// company routes have to be in this specific order
{path: 'company/:company-id/verify/:verification-token', pathMatch: 'full', component: CompanyMemberInvitationComponent, title: 'Invitation | Rocketadmin'},
{path: 'company', pathMatch: 'full', component: CompanyComponent, canActivate: [AuthGuard]},
{path: 'sso/:company-id', pathMatch: 'full', component: SsoComponent, canActivate: [AuthGuard]},
{path: 'change-password', component: PasswordChangeComponent, canActivate: [AuthGuard]},
{path: 'upgrade', component: UpgradeComponent, canActivate: [AuthGuard], title: 'Upgrade | Rocketadmin'},
{path: 'upgrade/payment', component: PaymentFormComponent, canActivate: [AuthGuard], title: 'Payment | Rocketadmin'},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
</mat-tab-nav-panel>

<div *ngIf="!authBarTheme" class="footer">
<span class="footer__text">&copy; 2025 Rocketadmin</span>
<span class="footer__text">&copy; 2025 Rocketadmin • v{{appVersion}}</span>
</div>
<app-feature-notification *ngIf="isFeatureNotificationShown" (dismiss)="dismissFeatureNotification()"></app-feature-notification>
</mat-sidenav-content>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { UserService } from './services/user.service';
import amplitude from 'amplitude-js';
import { differenceInMilliseconds } from 'date-fns';
import { environment } from '../environments/environment';
import { version } from './version';

//@ts-ignore
window.amplitude = amplitude;
Expand Down Expand Up @@ -58,6 +59,7 @@ amplitude.getInstance().init("9afd282be91f94da735c11418d5ff4f5");
export class AppComponent {

public isSaas = (environment as any).saas;
public appVersion = version;
userActivity;
userInactive: Subject<any> = new Subject();
currentFeatureNotificationId: string = 'saved-filters';
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/components/company/company.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@
}
}

.tableHeader__button {
.tableHeader__actions {
display: flex;
gap: 8px;
margin-bottom: 16px;
}

Expand Down
43 changes: 28 additions & 15 deletions frontend/src/app/components/company/company.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,35 @@ <h1 class="mat-h1 companyPageHeader">

<div class="tableHeader">
<h2 class="heading-2 tableHeader__heading">Members <span *ngIf="currentPlan === 'free' && isSaas" data-testid="company-members-max-string">(max 3)</span></h2>
<div data-testid="company-invitation-button-wrapper"
[matTooltip]="currentPlan === 'free' && usersCount >= 3 && isSaas ? 'To add more members please upgrade your plan.' : null">
<button *ngIf="currentUser && currentUser.role === 'ADMIN'"
mat-flat-button type="button"
data-testid="company-invitation-button"
angulartics2On="click"
angularticsAction="Company: invite member is clicked"
class="tableHeader__button" color="primary"
(click)="handleAddMemberDialogOpen()"
[disabled]="currentPlan === 'free' && usersCount >= 3 && isSaas">
Invite member
<mat-icon *ngIf="currentPlan === 'free' && usersCount >= 3 && isSaas">
info_outline
</mat-icon>
</button>
<div class="tableHeader__actions">
<div data-testid="sso-button-wrapper"
[matTooltip]="currentPlan !== 'enterprise' && isSaas ? 'To turn on SAML SSO upgrade your plan to Enterprise.' : null">
<a *ngIf="currentUser && (currentUser.role === 'ADMIN' || currentUser.role === 'DB_ADMIN')"
mat-button
routerLink="/sso/{{company.id}}"
[disabled]="currentPlan !== 'enterprise' && isSaas"
angulartics2On="click"
angularticsAction="Dashboard: add row is clicked">
Configure SAML
</a>
</div>
<div data-testid="company-invitation-button-wrapper"
[matTooltip]="currentPlan === 'free' && usersCount >= 3 && isSaas ? 'To add more members please upgrade your plan.' : null">
<button *ngIf="currentUser && currentUser.role === 'ADMIN'"
mat-flat-button type="button" color="primary"
data-testid="company-invitation-button"
angulartics2On="click"
angularticsAction="Company: invite member is clicked"
(click)="handleAddMemberDialogOpen()"
[disabled]="currentPlan === 'free' && usersCount >= 3 && isSaas">
Invite member
<mat-icon *ngIf="currentPlan === 'free' && usersCount >= 3 && isSaas">
info_outline
</mat-icon>
</button>
</div>
</div>

</div>

<app-placeholder-table-data *ngIf="submittingUsersChange"></app-placeholder-table-data>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/components/company/company.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SubscriptionPlans } from 'src/app/models/user';
import { UserService } from 'src/app/services/user.service';
import { of } from 'rxjs';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';

describe('CompanyComponent', () => {
let component: CompanyComponent;
Expand Down Expand Up @@ -128,6 +129,7 @@ describe('CompanyComponent', () => {
CompanyComponent
],
providers: [
provideRouter([]),
provideHttpClient(),
{ provide: CompanyService, useValue: fakeCompanyService },
{ provide: UserService, useValue: fakeUserService }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<app-alert *ngIf="isDemo" [alert]="isDemoConnectionWarning"></app-alert>

<div class="connectPage">
<form action="" #connectForm="ngForm" class="form"
<form #connectForm="ngForm" class="form"
(ngSubmit)="handleCredentialsSubmitting(connectForm)">
<h1 class="mat-h1 connectForm__fullLine">
{{ connectionID ? 'Edit credentials' : 'Connect database' }}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/app/components/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ <h1 class="mat-headline-4 loginTitle">
<span class="login-form__github-caption">Continue with GitHub</span>
</button>

<div *ngIf="!isCustomDomain" class="login-form__sso-button-box"
matTooltip="Coming soon"
matTooltipPosition="above">
<button disabled type="button" mat-stroked-button color="primary" data-testid="login-github-button"
<div *ngIf="!isCustomDomain" class="login-form__sso-button-box">
<button type="button" mat-stroked-button color="primary"
data-testid="login-sso-button"
class="login-form__sso-button"
angulartics2On="click"
angularticsAction="Login: login with github is clicked">
angularticsAction="Login: login with sso is clicked"
(click)="openLoginWithSSOdialog()">
<svg class="login-form__sso-icon" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.75 8.21393H14.625V5.96393C14.625 2.85893 12.105 0.338928 9 0.338928C5.895 0.338928 3.375 2.85893 3.375 5.96393V8.21393H2.25C1.0125 8.21393 0 9.22643 0 10.4639V21.7139C0 22.9514 1.0125 23.9639 2.25 23.9639H15.75C16.9875 23.9639 18 22.9514 18 21.7139V10.4639C18 9.22643 16.9875 8.21393 15.75 8.21393ZM9 18.3389C7.7625 18.3389 6.75 17.3264 6.75 16.0889C6.75 14.8514 7.7625 13.8389 9 13.8389C10.2375 13.8389 11.25 14.8514 11.25 16.0889C11.25 17.3264 10.2375 18.3389 9 18.3389ZM12.4875 8.21393H5.5125V5.96393C5.5125 4.04018 7.07625 2.47643 9 2.47643C10.9237 2.47643 12.4875 4.04018 12.4875 5.96393V8.21393Z" fill="#212121"/>
</svg>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/app/components/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Router } from '@angular/router';
import { RouterModule } from '@angular/router';
import { accounts } from 'google-one-tap';
import { environment } from 'src/environments/environment';
import { MatDialog } from '@angular/material/dialog';
import { SsoDialogComponent } from './sso-dialog/sso-dialog.component';

declare var google: any;

Expand Down Expand Up @@ -68,6 +70,8 @@ export class LoginComponent implements OnInit, AfterViewInit {
private ngZone: NgZone,
private _notifications: NotificationsService,
public _company: CompanyService,
public dialog: MatDialog,

) { }

ngOnInit(): void {
Expand Down Expand Up @@ -150,6 +154,12 @@ export class LoginComponent implements OnInit, AfterViewInit {
});
}

openLoginWithSSOdialog() {
this.dialog.open(SsoDialogComponent, {
width: '32em'
});
}

loginWith2FA() {
this.submitting = true;
this._auth.loginWith2FA(this.authCode)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.mat-mdc-form-field {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<h1 mat-dialog-title>Single Sign-On</h1>
<form #ssoForm="ngForm" (ngSubmit)="loginWithSSO()">
<mat-dialog-content>
<mat-form-field appearance="outline">
<mat-label>Enter company SSO Identifier</mat-label>
<input matInput [(ngModel)]="companySsoIdentifier" name="ssoIdentifier" #ssoIdentifier="ngModel" required data-testid="company-sso-identifier-input">
<mat-error *ngIf="ssoIdentifier.errors?.required && (ssoIdentifier.invalid && ssoIdentifier.touched)">Company SSO Identifier should not be empty.</mat-error>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-flat-button mat-dialog-close>Cancel</button>
<button mat-flat-button color="primary">
Login
</button>
</mat-dialog-actions>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SsoDialogComponent } from './sso-dialog.component';

describe('SsoDialogComponent', () => {
let component: SsoDialogComponent;
let fixture: ComponentFixture<SsoDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SsoDialogComponent]
})
.compileComponents();

fixture = TestBed.createComponent(SsoDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
selector: 'app-sso-dialog',
imports: [
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule
],
templateUrl: './sso-dialog.component.html',
styleUrl: './sso-dialog.component.css'
})
export class SsoDialogComponent {
public companySsoIdentifier: string = '';

loginWithSSO() {
window.location.href = `https://app.rocketadmin.com/saas/saml/login-by-slug/${this.companySsoIdentifier}`;
}
}
45 changes: 45 additions & 0 deletions frontend/src/app/components/sso/sso.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.samlConfigForm {
display: flex;
flex-direction: column;
margin: 3em auto;
max-width: 520px;
min-width: 300px;
width: 100%;
}

.ssoPageHeader {
margin-bottom: 24px !important;
}

.actions {
position: fixed;
left: 0;
bottom: 0;
display: flex;
align-items: center;
/* justify-content: space-between; */
background-color: var(--mat-sidenav-content-background-color);
box-shadow: var(--shadow);
height: 64px;
padding: 0 max(calc(50vw - 260px), 2%);
width: 100vw;
}

@media (prefers-color-scheme: dark) {
.actions {
--shadow: 0 3px 1px -2px rgba(0,0,0,.5),0 2px 2px 0 rgba(0,0,0,.64),0 1px 5px 0 rgba(0,0,0,0.85);
}
}

@media (prefers-color-scheme: light) {
.actions {
--shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);
}
}

.actions-box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
Loading
Loading