mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Remove unused files and configurations
This commit is contained in:
14
.github/workflows/compile.yml
vendored
14
.github/workflows/compile.yml
vendored
@@ -137,20 +137,6 @@ jobs:
|
||||
- run: cd CommonServer && npm install
|
||||
- run: cd Home && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-identity:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
- run: cd Common && npm install
|
||||
- run: cd Model && npm install
|
||||
- run: cd CommonServer && npm install
|
||||
- run: cd Identity && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-notification:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
.github/workflows/docker-build.yml
vendored
15
.github/workflows/docker-build.yml
vendored
@@ -133,21 +133,6 @@ jobs:
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Home/Dockerfile .
|
||||
|
||||
docker-build-identity:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Preinstall
|
||||
run: npm run prerun
|
||||
|
||||
# build image for identity
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Identity/Dockerfile .
|
||||
|
||||
|
||||
docker-build-notification:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
59
.github/workflows/release.yml
vendored
59
.github/workflows/release.yml
vendored
@@ -690,65 +690,6 @@ jobs:
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
|
||||
identity-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/identity
|
||||
ghcr.io/oneuptime/identity
|
||||
tags: |
|
||||
type=raw,value=release,enable=true
|
||||
type=semver,value=7.0.${{needs.generate-build-number.outputs.build_number}},pattern={{version}},enable=true
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
# Build and deploy identity.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: ./Identity/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
|
||||
home-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
|
||||
61
.github/workflows/test-release.yaml
vendored
61
.github/workflows/test-release.yaml
vendored
@@ -633,67 +633,6 @@ jobs:
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
|
||||
identity-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/identity
|
||||
ghcr.io/oneuptime/identity
|
||||
tags: |
|
||||
type=raw,value=test,enable=true
|
||||
type=semver,value=7.0.${{needs.generate-build-number.outputs.build_number}}-test,pattern={{version}},enable=true
|
||||
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
run: npm run prerun
|
||||
|
||||
# Build and deploy identity.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: ./Identity/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
|
||||
home-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -33,15 +33,7 @@ const DashboardFooter: () => JSX.Element = () => {
|
||||
{
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
},
|
||||
{
|
||||
name: 'Notification',
|
||||
path: '/notification',
|
||||
},
|
||||
{
|
||||
name: 'Identity Service',
|
||||
path: '/identity',
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
for (const app of apps) {
|
||||
|
||||
@@ -33,15 +33,7 @@ const DashboardFooter: () => JSX.Element = () => {
|
||||
{
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
},
|
||||
{
|
||||
name: 'Notification',
|
||||
path: '/notification',
|
||||
},
|
||||
{
|
||||
name: 'Identity Service',
|
||||
path: '/identity',
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
for (const app of apps) {
|
||||
|
||||
@@ -33,12 +33,8 @@
|
||||
value: {{ $.Release.Name }}-ingestor.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_TEST_SERVER_HOSTNAME
|
||||
value: {{ $.Release.Name }}-test-server.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_FILE_HOSTNAME
|
||||
value: {{ $.Release.Name }}-file.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_HOME_HOSTNAME
|
||||
value: {{ $.Release.Name }}-home.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_IDENTITY_HOSTNAME
|
||||
value: {{ $.Release.Name }}-identity.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: SERVER_NOTIFICATION_HOSTNAME
|
||||
value: {{ $.Release.Name }}-notification.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
|
||||
- name: OTEL_COLLECTOR_HOSTNAME
|
||||
|
||||
1
Identity/.gitattributes
vendored
1
Identity/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.js text eol=lf
|
||||
29
Identity/.gitignore
vendored
29
Identity/.gitignore
vendored
@@ -1,29 +0,0 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
#/backend/node_modules
|
||||
/kubernetes
|
||||
/node_modules
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
|
||||
**/*/paymentService.test.js
|
||||
apiTest.rest
|
||||
|
||||
application_security_dir
|
||||
container_security_dir
|
||||
|
||||
# coverage
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
/greenlock.d/config.json
|
||||
/greenlock.d/config.json.bak
|
||||
@@ -1,612 +0,0 @@
|
||||
import {
|
||||
IsBillingEnabled,
|
||||
EncryptionSecret,
|
||||
} from 'CommonServer/EnvironmentConfig';
|
||||
import { AccountsRoute } from 'Common/ServiceRoute';
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from 'CommonServer/Utils/Express';
|
||||
import BadRequestException from 'Common/Types/Exception/BadRequestException';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import User from 'Model/Models/User';
|
||||
import EmailVerificationTokenService from 'CommonServer/Services/EmailVerificationTokenService';
|
||||
import UserService from 'CommonServer/Services/UserService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import EmailVerificationToken from 'Model/Models/EmailVerificationToken';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MailService from 'CommonServer/Services/MailService';
|
||||
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import Response from 'CommonServer/Utils/Response';
|
||||
import JSONWebToken from 'CommonServer/Utils/JsonWebToken';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import PartialEntity from 'Common/Types/Database/PartialEntity';
|
||||
import Email from 'Common/Types/Email';
|
||||
import Name from 'Common/Types/Name';
|
||||
import AuthenticationEmail from '../Utils/AuthenticationEmail';
|
||||
import AccessTokenService from 'CommonServer/Services/AccessTokenService';
|
||||
import Hostname from 'Common/Types/API/Hostname';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import DatabaseConfig from 'CommonServer/DatabaseConfig';
|
||||
import CookieUtil from 'CommonServer/Utils/Cookie';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.post(
|
||||
'/signup',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (await DatabaseConfig.shouldDisableSignup()) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException(
|
||||
'Sign up is disabled on this OneUptime Server. Please contact your server admin to enable it.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
/* Creating a type that is a partial of the TBaseModel type. */
|
||||
const partialUser: PartialEntity<User> = data;
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
//ALERT: Delete data.role so user don't accidently sign up as master-admin from the API.
|
||||
partialUser.isMasterAdmin = false;
|
||||
partialUser.isEmailVerified = false;
|
||||
} else {
|
||||
// IF its not a saas service then we will make the email verified.
|
||||
|
||||
// check if there are more than one user and if there is then we will not make the user master admin.
|
||||
|
||||
const userCount: PositiveNumber = await UserService.countBy({
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
query: {},
|
||||
});
|
||||
|
||||
partialUser.isMasterAdmin = userCount.isZero(); // if the user count is 0 then make the first user master admin.
|
||||
partialUser.isEmailVerified = true;
|
||||
}
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
query: { email: partialUser.email as Email },
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alreadySavedUser && alreadySavedUser.password) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
`User with email ${partialUser.email} already exists.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let savedUser: User | null = null;
|
||||
|
||||
if (alreadySavedUser) {
|
||||
//@ts-ignore
|
||||
savedUser = await UserService.updateOneByIdAndFetch({
|
||||
id: alreadySavedUser.id!,
|
||||
data: partialUser,
|
||||
select: {
|
||||
email: true,
|
||||
_id: true,
|
||||
name: true,
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const user: User = BaseModel.fromJSON(
|
||||
partialUser as JSONObject,
|
||||
User
|
||||
) as User;
|
||||
|
||||
savedUser = await UserService.create({
|
||||
data: user,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const generatedToken: ObjectID = ObjectID.generate();
|
||||
|
||||
const emailVerificationToken: EmailVerificationToken =
|
||||
new EmailVerificationToken();
|
||||
emailVerificationToken.userId = savedUser?.id as ObjectID;
|
||||
emailVerificationToken.email = savedUser?.email as Email;
|
||||
emailVerificationToken.token = generatedToken;
|
||||
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter();
|
||||
|
||||
await EmailVerificationTokenService.create({
|
||||
data: emailVerificationToken,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: partialUser.email as Email,
|
||||
subject: 'Welcome to OneUptime. Please verify your email.',
|
||||
templateType: EmailTemplateType.SignupWelcomeEmail,
|
||||
vars: {
|
||||
name: (partialUser.name! as Name).toString(),
|
||||
tokenVerifyUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AccountsRoute.toString()).addRoute(
|
||||
'/verify-email/' + generatedToken.toString()
|
||||
)
|
||||
).toString(),
|
||||
homeUrl: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
if (savedUser) {
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(
|
||||
savedUser.id!
|
||||
);
|
||||
|
||||
const token: string = JSONWebToken.sign(
|
||||
savedUser,
|
||||
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
|
||||
);
|
||||
|
||||
// Set a cookie with token.
|
||||
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
return Response.sendEntityResponse(req, res, savedUser, User);
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Failed to create a user')
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/forgot-password',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
const user: User = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
User
|
||||
) as User;
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
query: { email: user.email! },
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isMasterAdmin: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alreadySavedUser && alreadySavedUser.password) {
|
||||
const token: string = ObjectID.generate().toString();
|
||||
await UserService.updateOneBy({
|
||||
query: {
|
||||
_id: alreadySavedUser._id!,
|
||||
},
|
||||
data: {
|
||||
resetPasswordToken: token,
|
||||
resetPasswordExpires: OneUptimeDate.getOneDayAfter(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: user.email!,
|
||||
subject: 'Password Reset Request for OneUptime',
|
||||
templateType: EmailTemplateType.ForgotPassword,
|
||||
vars: {
|
||||
homeURL: new URL(httpProtocol, host).toString(),
|
||||
tokenVerifyUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AccountsRoute.toString()).addRoute(
|
||||
'/reset-password/' + token
|
||||
)
|
||||
).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
}
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
`No user is registered with ${user.email?.toString()}. Please sign up for a new account.`
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/verify-email',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
const token: EmailVerificationToken = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
EmailVerificationToken
|
||||
) as EmailVerificationToken;
|
||||
|
||||
const alreadySavedToken: EmailVerificationToken | null =
|
||||
await EmailVerificationTokenService.findOneBy({
|
||||
query: { token: token.token! },
|
||||
select: {
|
||||
_id: true,
|
||||
userId: true,
|
||||
email: true,
|
||||
expires: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alreadySavedToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Invalid link. Please try to log in and we will resend you another link which you should be able to verify email with.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (OneUptimeDate.hasExpired(alreadySavedToken.expires!)) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Link expired. Please try to log in and we will resend you another link which you should be able to verify email with.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const user: User | null = await UserService.findOneBy({
|
||||
query: {
|
||||
email: alreadySavedToken.email!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Invalid link. Please try to log in and we will resend you another link which you should be able to verify email with.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await UserService.updateOneBy({
|
||||
query: {
|
||||
_id: user._id!,
|
||||
},
|
||||
data: {
|
||||
isEmailVerified: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: user.email!,
|
||||
subject: 'Email Verified.',
|
||||
templateType: EmailTemplateType.EmailVerified,
|
||||
vars: {
|
||||
homeURL: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/reset-password',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
const user: User = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
User
|
||||
) as User;
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
query: {
|
||||
resetPasswordToken:
|
||||
(user.resetPasswordToken as string) || '',
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isMasterAdmin: true,
|
||||
resetPasswordExpires: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alreadySavedUser) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Invalid link. Please go to forgot password page again and request a new link.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
alreadySavedUser &&
|
||||
OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!)
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Expired link. Please go to forgot password page again and request a new link.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await UserService.updateOneById({
|
||||
id: alreadySavedUser.id!,
|
||||
data: {
|
||||
password: user.password!,
|
||||
resetPasswordToken: null!,
|
||||
resetPasswordExpires: null!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: alreadySavedUser.email!,
|
||||
subject: 'Password Changed.',
|
||||
templateType: EmailTemplateType.PasswordChanged,
|
||||
vars: {
|
||||
homeURL: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/logout',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
CookieUtil.removeAllCookies(req, res);
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
const user: User = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
User
|
||||
) as User;
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
query: { email: user.email! },
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isMasterAdmin: true,
|
||||
isEmailVerified: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alreadySavedUser) {
|
||||
if (!alreadySavedUser.password) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'You have not signed up so far. Please go to the registration page to sign up.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!alreadySavedUser.isEmailVerified) {
|
||||
await AuthenticationEmail.sendVerificationEmail(
|
||||
alreadySavedUser
|
||||
);
|
||||
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Email is not verified. We have sent you an email with the verification link. Please do not forget to check spam.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(
|
||||
alreadySavedUser.id!
|
||||
);
|
||||
|
||||
if (
|
||||
alreadySavedUser.password.toString() ===
|
||||
user.password!.toString()
|
||||
) {
|
||||
const token: string = JSONWebToken.sign(
|
||||
alreadySavedUser,
|
||||
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
|
||||
);
|
||||
|
||||
// Set a cookie with token.
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(),
|
||||
token,
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
httpOnly: true,
|
||||
}
|
||||
);
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
alreadySavedUser,
|
||||
User
|
||||
);
|
||||
}
|
||||
}
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
'Invalid login: Email or password does not match.'
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,79 +0,0 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from 'CommonServer/Utils/Express';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Reseller from 'Model/Models/Reseller';
|
||||
import ResellerService from 'CommonServer/Services/ResellerService';
|
||||
import JSONWebToken from 'CommonServer/Utils/JsonWebToken';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import Response from 'CommonServer/Utils/Response';
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.post(
|
||||
'/reseller/auth/:resellerid',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const resellerId: string | undefined = req.params['resellerid'];
|
||||
|
||||
if (!resellerId) {
|
||||
throw new BadDataException('Reseller ID not found');
|
||||
}
|
||||
|
||||
const username: string = req.body['username'];
|
||||
const password: string = req.body['password'];
|
||||
|
||||
if (!username) {
|
||||
throw new BadDataException('Username not found');
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new BadDataException('Password not found');
|
||||
}
|
||||
|
||||
// get the reseller user.
|
||||
const reseller: Reseller | null = await ResellerService.findOneBy({
|
||||
query: {
|
||||
resellerId: resellerId,
|
||||
username: username,
|
||||
password: password,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
resellerId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!reseller) {
|
||||
throw new BadDataException(
|
||||
'Reseller not found or username and password is incorrect'
|
||||
);
|
||||
}
|
||||
|
||||
// if found then generate a token and return it.
|
||||
|
||||
const token: string = JSONWebToken.sign(
|
||||
{ resellerId: resellerId },
|
||||
OneUptimeDate.getDayInSeconds(365)
|
||||
);
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
access: token,
|
||||
});
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,386 +0,0 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from 'CommonServer/Utils/Express';
|
||||
import BadRequestException from 'Common/Types/Exception/BadRequestException';
|
||||
import ServerException from 'Common/Types/Exception/ServerException';
|
||||
import Response from 'CommonServer/Utils/Response';
|
||||
import ProjectSSO from 'Model/Models/ProjectSso';
|
||||
import ProjectSSOService from 'CommonServer/Services/ProjectSsoService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import xml2js from 'xml2js';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import Email from 'Common/Types/Email';
|
||||
import User from 'Model/Models/User';
|
||||
import UserService from 'CommonServer/Services/UserService';
|
||||
import AuthenticationEmail from '../Utils/AuthenticationEmail';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import JSONWebToken from 'CommonServer/Utils/JsonWebToken';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DashboardRoute } from 'Common/ServiceRoute';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import TeamMember from 'Model/Models/TeamMember';
|
||||
import TeamMemberService from 'CommonServer/Services/TeamMemberService';
|
||||
import AccessTokenService from 'CommonServer/Services/AccessTokenService';
|
||||
import SSOUtil from '../Utils/SSO';
|
||||
import Exception from 'Common/Types/Exception/Exception';
|
||||
import Hostname from 'Common/Types/API/Hostname';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import DatabaseConfig from 'CommonServer/DatabaseConfig';
|
||||
import CookieUtil from 'CommonServer/Utils/Cookie';
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.get(
|
||||
'/sso/:projectId/:projectSsoId',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.params['projectId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Project ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.params['projectSsoId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Project SSO ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
const projectSSO: ProjectSSO | null =
|
||||
await ProjectSSOService.findOneBy({
|
||||
query: {
|
||||
projectId: new ObjectID(req.params['projectId']),
|
||||
_id: req.params['projectSsoId'],
|
||||
isEnabled: true,
|
||||
},
|
||||
select: {
|
||||
signOnURL: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectSSO) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SSO Config not found')
|
||||
);
|
||||
}
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!projectSSO.signOnURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Sign On URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, projectSSO.signOnURL);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/idp-login/:projectId/:projectSsoId',
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
const samlResponseBase64: string = req.body.SAMLResponse;
|
||||
|
||||
const samlResponse: string = Buffer.from(
|
||||
samlResponseBase64,
|
||||
'base64'
|
||||
).toString();
|
||||
|
||||
const response: JSONObject = await xml2js.parseStringPromise(
|
||||
samlResponse
|
||||
);
|
||||
|
||||
let issuerUrl: string = '';
|
||||
let email: Email | null = null;
|
||||
|
||||
if (!req.params['projectId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Project ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.params['projectSsoId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Project SSO ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
const projectSSO: ProjectSSO | null =
|
||||
await ProjectSSOService.findOneBy({
|
||||
query: {
|
||||
projectId: new ObjectID(req.params['projectId']),
|
||||
_id: req.params['projectSsoId'],
|
||||
isEnabled: true,
|
||||
},
|
||||
select: {
|
||||
signOnURL: true,
|
||||
issuerURL: true,
|
||||
publicCertificate: true,
|
||||
teams: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectSSO) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SSO Config not found')
|
||||
);
|
||||
}
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!projectSSO.issuerURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Issuer URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!projectSSO.signOnURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Sign on URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectSSO.publicCertificate) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Public Certificate not found')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
SSOUtil.isPayloadValid(response);
|
||||
|
||||
if (
|
||||
!SSOUtil.isSignatureValid(
|
||||
samlResponse,
|
||||
projectSSO.publicCertificate
|
||||
)
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException(
|
||||
'Signature is not valid or Public Certificate configured with this SSO provider is not valid'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
issuerUrl = SSOUtil.getIssuer(response);
|
||||
email = SSOUtil.getEmail(response);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Exception) {
|
||||
return Response.sendErrorResponse(req, res, err);
|
||||
}
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new ServerException()
|
||||
);
|
||||
}
|
||||
|
||||
if (projectSSO.issuerURL.toString() !== issuerUrl) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Issuer URL does not match')
|
||||
);
|
||||
}
|
||||
|
||||
// Check if he already belongs to the project, If he does - then log in.
|
||||
|
||||
let alreadySavedUser: User | null = await UserService.findOneBy({
|
||||
query: { email: email },
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isMasterAdmin: true,
|
||||
isEmailVerified: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
let isNewUser: boolean = false;
|
||||
|
||||
if (!alreadySavedUser) {
|
||||
// this should never happen because user is logged in before he signs in with SSO UNLESS he initiates the login though the IDP.
|
||||
|
||||
/// Create a user.
|
||||
|
||||
alreadySavedUser = await UserService.createByEmail(email, {
|
||||
isRoot: true,
|
||||
});
|
||||
|
||||
isNewUser = true;
|
||||
}
|
||||
|
||||
// If he does not then add him to teams that he should belong and log in.
|
||||
if (!alreadySavedUser.isEmailVerified && !isNewUser) {
|
||||
await AuthenticationEmail.sendVerificationEmail(
|
||||
alreadySavedUser
|
||||
);
|
||||
|
||||
return Response.render(req, res, '../Views/Message.ejs', {
|
||||
title: 'Email not verified.',
|
||||
message:
|
||||
'Email is not verified. We have sent you an email with the verification link. Please do not forget to check spam.',
|
||||
});
|
||||
}
|
||||
|
||||
// check if the user already belongs to the project
|
||||
const teamMemberCount: PositiveNumber =
|
||||
await TeamMemberService.countBy({
|
||||
query: {
|
||||
projectId: new ObjectID(
|
||||
req.params['projectId'] as string
|
||||
),
|
||||
userId: alreadySavedUser.id!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (teamMemberCount.toNumber() === 0) {
|
||||
// user not in project, add him to default teams.
|
||||
|
||||
if (!projectSSO.teams || projectSSO.teams.length === 0) {
|
||||
return Response.render(req, res, '../Views/Message.ejs', {
|
||||
title: 'No teams added.',
|
||||
message:
|
||||
'No teams have been added to this SSO config. Please contact your admin and have default teams added.',
|
||||
});
|
||||
}
|
||||
|
||||
for (const team of projectSSO.teams) {
|
||||
// add user to team
|
||||
let teamMember: TeamMember = new TeamMember();
|
||||
teamMember.projectId = new ObjectID(
|
||||
req.params['projectId'] as string
|
||||
);
|
||||
teamMember.userId = alreadySavedUser.id!;
|
||||
teamMember.hasAcceptedInvitation = true;
|
||||
teamMember.invitationAcceptedAt =
|
||||
OneUptimeDate.getCurrentDate();
|
||||
teamMember.teamId = team.id!;
|
||||
|
||||
teamMember = await TeamMemberService.create({
|
||||
data: teamMember,
|
||||
props: {
|
||||
isRoot: true,
|
||||
ignoreHooks: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewUser) {
|
||||
return Response.render(req, res, '../Views/Message.ejs', {
|
||||
title: 'You have not signed up so far.',
|
||||
message:
|
||||
'You need to sign up for an account on OneUptime with this email:' +
|
||||
email.toString() +
|
||||
'. Once you have signed up, you can use SSO to log in to your project.',
|
||||
});
|
||||
}
|
||||
|
||||
const projectId: ObjectID = new ObjectID(
|
||||
req.params['projectId'] as string
|
||||
);
|
||||
|
||||
const token: string = JSONWebToken.sign(
|
||||
{
|
||||
userId: alreadySavedUser.id!,
|
||||
projectId: projectId,
|
||||
email: email,
|
||||
isMasterAdmin: false,
|
||||
},
|
||||
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
|
||||
);
|
||||
|
||||
// Refresh Permissions for this user here.
|
||||
await AccessTokenService.refreshUserAllPermissions(
|
||||
alreadySavedUser.id!
|
||||
);
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserSSOKey(projectId),
|
||||
token,
|
||||
{
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
httpOnly: true,
|
||||
}
|
||||
);
|
||||
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(DashboardRoute.toString()).addRoute(
|
||||
'/' + req.params['projectId']
|
||||
),
|
||||
'sso_token=' + token
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
Response.sendErrorResponse(req, res, new ServerException());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,430 +0,0 @@
|
||||
import { EncryptionSecret } from 'CommonServer/EnvironmentConfig';
|
||||
import { FileRoute } from 'Common/ServiceRoute';
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from 'CommonServer/Utils/Express';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import StatusPagePrivateUserService from 'CommonServer/Services/StatusPagePrivateUserService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MailService from 'CommonServer/Services/MailService';
|
||||
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import Response from 'CommonServer/Utils/Response';
|
||||
import JSONWebToken from 'CommonServer/Utils/JsonWebToken';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import StatusPagePrivateUser from 'Model/Models/StatusPagePrivateUser';
|
||||
import StatusPage from 'Model/Models/StatusPage';
|
||||
import StatusPageService from 'CommonServer/Services/StatusPageService';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import Hostname from 'Common/Types/API/Hostname';
|
||||
import DatabaseConfig from 'CommonServer/DatabaseConfig';
|
||||
import CookieUtil from 'CommonServer/Utils/Cookie';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.post(
|
||||
'/logout/:statuspageid',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.params['statuspageid']) {
|
||||
throw new BadDataException('Status Page ID is required.');
|
||||
}
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(
|
||||
req.params['statuspageid'].toString()
|
||||
);
|
||||
|
||||
CookieUtil.removeCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(statusPageId)
|
||||
); // remove the cookie.
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/forgot-password',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
if (!data['email']) {
|
||||
throw new BadDataException('Email is required.');
|
||||
}
|
||||
|
||||
const user: StatusPagePrivateUser = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
StatusPagePrivateUser
|
||||
) as StatusPagePrivateUser;
|
||||
|
||||
if (!user.statusPageId) {
|
||||
throw new BadDataException('Status Page ID is required.');
|
||||
}
|
||||
|
||||
const statusPage: StatusPage | null =
|
||||
await StatusPageService.findOneById({
|
||||
id: user.statusPageId!,
|
||||
props: {
|
||||
isRoot: true,
|
||||
ignoreHooks: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
pageTitle: true,
|
||||
logoFileId: true,
|
||||
requireSsoForLogin: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPage) {
|
||||
throw new BadDataException('Status Page not found');
|
||||
}
|
||||
|
||||
if (statusPage.requireSsoForLogin) {
|
||||
throw new BadDataException(
|
||||
'Status Page supports authentication by SSO. You cannot use email and password for authentication.'
|
||||
);
|
||||
}
|
||||
|
||||
const statusPageName: string | undefined =
|
||||
statusPage.pageTitle || statusPage.name;
|
||||
|
||||
const statusPageURL: string =
|
||||
await StatusPageService.getStatusPageURL(statusPage.id!);
|
||||
|
||||
const alreadySavedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
email: user.email!,
|
||||
statusPageId: user.statusPageId!,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
email: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alreadySavedUser) {
|
||||
const token: string = ObjectID.generate().toString();
|
||||
await StatusPagePrivateUserService.updateOneBy({
|
||||
query: {
|
||||
_id: alreadySavedUser._id!,
|
||||
},
|
||||
data: {
|
||||
resetPasswordToken: token,
|
||||
resetPasswordExpires: OneUptimeDate.getOneDayAfter(),
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
toEmail: user.email!,
|
||||
subject: 'Password Reset Request for ' + statusPageName,
|
||||
templateType:
|
||||
EmailTemplateType.StatusPageForgotPassword,
|
||||
vars: {
|
||||
statusPageName: statusPageName!,
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute(
|
||||
'/image/' + statusPage.logoFileId
|
||||
)
|
||||
.toString()
|
||||
: '',
|
||||
homeURL: statusPageURL,
|
||||
tokenVerifyUrl: URL.fromString(statusPageURL)
|
||||
.addRoute('/reset-password/' + token)
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
projectId: statusPage.projectId!,
|
||||
}
|
||||
).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
}
|
||||
|
||||
throw new BadDataException(
|
||||
`No user is registered with ${user.email?.toString()}`
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/reset-password',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = JSONFunctions.deserialize(
|
||||
req.body['data']
|
||||
);
|
||||
|
||||
if (!data['statusPageId']) {
|
||||
throw new BadDataException('Status Page ID is required.');
|
||||
}
|
||||
|
||||
const user: StatusPagePrivateUser = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
StatusPagePrivateUser
|
||||
) as StatusPagePrivateUser;
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
statusPageId: new ObjectID(
|
||||
data['statusPageId'].toString()
|
||||
),
|
||||
resetPasswordToken:
|
||||
(user.resetPasswordToken as string) || '',
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
email: true,
|
||||
resetPasswordExpires: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alreadySavedUser) {
|
||||
throw new BadDataException(
|
||||
'Invalid link. Please go to forgot password page again and request a new link.'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
alreadySavedUser &&
|
||||
OneUptimeDate.hasExpired(alreadySavedUser.resetPasswordExpires!)
|
||||
) {
|
||||
throw new BadDataException(
|
||||
'Expired link. Please go to forgot password page again and request a new link.'
|
||||
);
|
||||
}
|
||||
|
||||
const statusPage: StatusPage | null =
|
||||
await StatusPageService.findOneById({
|
||||
id: new ObjectID(data['statusPageId'].toString()),
|
||||
props: {
|
||||
isRoot: true,
|
||||
ignoreHooks: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
pageTitle: true,
|
||||
logoFileId: true,
|
||||
requireSsoForLogin: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPage) {
|
||||
throw new BadDataException('Status Page not found');
|
||||
}
|
||||
|
||||
if (statusPage.requireSsoForLogin) {
|
||||
throw new BadDataException(
|
||||
'Status Page supports authentication by SSO. You cannot use email and password for authentication.'
|
||||
);
|
||||
}
|
||||
|
||||
const statusPageName: string | undefined =
|
||||
statusPage.pageTitle || statusPage.name;
|
||||
|
||||
const statusPageURL: string =
|
||||
await StatusPageService.getStatusPageURL(statusPage.id!);
|
||||
|
||||
await StatusPagePrivateUserService.updateOneById({
|
||||
id: alreadySavedUser.id!,
|
||||
data: {
|
||||
password: user.password!,
|
||||
resetPasswordToken: null!,
|
||||
resetPasswordExpires: null!,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol =
|
||||
await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail(
|
||||
{
|
||||
toEmail: alreadySavedUser.email!,
|
||||
subject: 'Password Changed.',
|
||||
templateType: EmailTemplateType.StatusPagePasswordChanged,
|
||||
vars: {
|
||||
homeURL: statusPageURL,
|
||||
statusPageName: statusPageName || '',
|
||||
logoUrl: statusPage.logoFileId
|
||||
? new URL(httpProtocol, host)
|
||||
.addRoute(FileRoute)
|
||||
.addRoute('/image/' + statusPage.logoFileId)
|
||||
.toString()
|
||||
: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
projectId: statusPage.projectId!,
|
||||
}
|
||||
).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return Response.sendEmptyResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const data: JSONObject = req.body['data'];
|
||||
|
||||
const user: StatusPagePrivateUser = BaseModel.fromJSON(
|
||||
data as JSONObject,
|
||||
StatusPagePrivateUser
|
||||
) as StatusPagePrivateUser;
|
||||
|
||||
if (!user.statusPageId) {
|
||||
throw new BadDataException('Status Page ID not found');
|
||||
}
|
||||
|
||||
const statusPage: StatusPage | null =
|
||||
await StatusPageService.findOneById({
|
||||
id: user.statusPageId,
|
||||
props: {
|
||||
isRoot: true,
|
||||
ignoreHooks: true,
|
||||
},
|
||||
select: {
|
||||
requireSsoForLogin: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPage) {
|
||||
throw new BadDataException('Status Page not found');
|
||||
}
|
||||
|
||||
if (statusPage.requireSsoForLogin) {
|
||||
throw new BadDataException(
|
||||
'Status Page supports authentication by SSO. You cannot use email and password for authentication.'
|
||||
);
|
||||
}
|
||||
|
||||
await user.password?.hashValue(EncryptionSecret);
|
||||
|
||||
const alreadySavedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: {
|
||||
email: user.email!,
|
||||
password: user.password!,
|
||||
statusPageId: user.statusPageId!,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
password: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (alreadySavedUser) {
|
||||
const token: string = JSONWebToken.sign(
|
||||
alreadySavedUser,
|
||||
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
|
||||
token,
|
||||
{
|
||||
httpOnly: true,
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
alreadySavedUser,
|
||||
StatusPagePrivateUser,
|
||||
{
|
||||
miscData: {
|
||||
token: token,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
throw new BadDataException(
|
||||
'Invalid login: Email or password does not match.'
|
||||
);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,308 +0,0 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from 'CommonServer/Utils/Express';
|
||||
import BadRequestException from 'Common/Types/Exception/BadRequestException';
|
||||
import ServerException from 'Common/Types/Exception/ServerException';
|
||||
import Response from 'CommonServer/Utils/Response';
|
||||
import StatusPageSsoService from 'CommonServer/Services/StatusPageSsoService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import xml2js from 'xml2js';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import Email from 'Common/Types/Email';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import JSONWebToken from 'CommonServer/Utils/JsonWebToken';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import SSOUtil from '../Utils/SSO';
|
||||
import Exception from 'Common/Types/Exception/Exception';
|
||||
import StatusPageSSO from 'Model/Models/StatusPageSso';
|
||||
import StatusPagePrivateUser from 'Model/Models/StatusPagePrivateUser';
|
||||
import StatusPagePrivateUserService from 'CommonServer/Services/StatusPagePrivateUserService';
|
||||
import HashedString from 'Common/Types/HashedString';
|
||||
import StatusPageService from 'CommonServer/Services/StatusPageService';
|
||||
import CookieUtil from 'CommonServer/Utils/Cookie';
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
router.get(
|
||||
'/status-page-sso/:statusPageId/:statusPageSsoId',
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (!req.params['statusPageId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Status Page ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.params['statusPageSsoId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Status Page SSO ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(
|
||||
req.params['statusPageId']
|
||||
);
|
||||
|
||||
const statusPageSSO: StatusPageSSO | null =
|
||||
await StatusPageSsoService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: req.params['statusPageSsoId'],
|
||||
isEnabled: true,
|
||||
},
|
||||
select: {
|
||||
signOnURL: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPageSSO) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SSO Config not found')
|
||||
);
|
||||
}
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!statusPageSSO.signOnURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Sign On URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
return Response.redirect(req, res, statusPageSSO.signOnURL);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/status-page-idp-login/:statusPageId/:statusPageSsoId',
|
||||
async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
|
||||
try {
|
||||
const samlResponseBase64: string = req.body.SAMLResponse;
|
||||
|
||||
const samlResponse: string = Buffer.from(
|
||||
samlResponseBase64,
|
||||
'base64'
|
||||
).toString();
|
||||
|
||||
const response: JSONObject = await xml2js.parseStringPromise(
|
||||
samlResponse
|
||||
);
|
||||
|
||||
let issuerUrl: string = '';
|
||||
let email: Email | null = null;
|
||||
|
||||
if (!req.params['statusPageId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Status Page ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!req.params['statusPageSsoId']) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Status Page SSO ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
const statusPageId: ObjectID = new ObjectID(
|
||||
req.params['statusPageId']
|
||||
);
|
||||
|
||||
const statusPageSSO: StatusPageSSO | null =
|
||||
await StatusPageSsoService.findOneBy({
|
||||
query: {
|
||||
statusPageId: statusPageId,
|
||||
_id: req.params['statusPageSsoId'],
|
||||
isEnabled: true,
|
||||
},
|
||||
select: {
|
||||
signOnURL: true,
|
||||
issuerURL: true,
|
||||
publicCertificate: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!statusPageSSO) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SSO Config not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!statusPageSSO.projectId) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('SSO Config Project ID not found')
|
||||
);
|
||||
}
|
||||
|
||||
const projectId: ObjectID = statusPageSSO.projectId;
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!statusPageSSO.issuerURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Issuer URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
// redirect to Identity Provider.
|
||||
|
||||
if (!statusPageSSO.signOnURL) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Sign on URL not found')
|
||||
);
|
||||
}
|
||||
|
||||
if (!statusPageSSO.publicCertificate) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Public Certificate not found')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
SSOUtil.isPayloadValid(response);
|
||||
|
||||
if (
|
||||
!SSOUtil.isSignatureValid(
|
||||
samlResponse,
|
||||
statusPageSSO.publicCertificate
|
||||
)
|
||||
) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Signature is not valid')
|
||||
);
|
||||
}
|
||||
|
||||
issuerUrl = SSOUtil.getIssuer(response);
|
||||
|
||||
email = SSOUtil.getEmail(response);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Exception) {
|
||||
return Response.sendErrorResponse(req, res, err);
|
||||
}
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new ServerException()
|
||||
);
|
||||
}
|
||||
|
||||
if (statusPageSSO.issuerURL.toString() !== issuerUrl) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadRequestException('Issuer URL does not match')
|
||||
);
|
||||
}
|
||||
|
||||
// Check if he already belongs to the project, If he does - then log in.
|
||||
|
||||
let alreadySavedUser: StatusPagePrivateUser | null =
|
||||
await StatusPagePrivateUserService.findOneBy({
|
||||
query: { email: email, statusPageId: statusPageId },
|
||||
select: {
|
||||
_id: true,
|
||||
email: true,
|
||||
statusPageId: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!alreadySavedUser) {
|
||||
/// Create a user.
|
||||
|
||||
alreadySavedUser = new StatusPagePrivateUser();
|
||||
alreadySavedUser.projectId = projectId;
|
||||
alreadySavedUser.statusPageId = statusPageId;
|
||||
alreadySavedUser.email = email;
|
||||
alreadySavedUser.password = new HashedString(
|
||||
ObjectID.generate().toString()
|
||||
);
|
||||
alreadySavedUser.isSsoUser = true;
|
||||
|
||||
alreadySavedUser = await StatusPagePrivateUserService.create({
|
||||
data: alreadySavedUser,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
}
|
||||
|
||||
const token: string = JSONWebToken.sign(
|
||||
alreadySavedUser,
|
||||
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
|
||||
);
|
||||
|
||||
CookieUtil.setCookie(
|
||||
res,
|
||||
CookieUtil.getUserTokenKey(alreadySavedUser.statusPageId!),
|
||||
token,
|
||||
{
|
||||
httpOnly: true,
|
||||
maxAge: OneUptimeDate.getMillisecondsInDays(
|
||||
new PositiveNumber(30)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// get status page URL.
|
||||
const statusPageURL: string =
|
||||
await StatusPageService.getStatusPageFirstURL(statusPageId);
|
||||
|
||||
return Response.redirect(
|
||||
req,
|
||||
res,
|
||||
URL.fromString(statusPageURL).addQueryParams({
|
||||
token: token,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
Response.sendErrorResponse(req, res, new ServerException());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,74 +0,0 @@
|
||||
#
|
||||
# OneUptime-identity Dockerfile
|
||||
#
|
||||
|
||||
# Pull base image nodejs image.
|
||||
FROM node:21.2-alpine3.18
|
||||
USER root
|
||||
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
|
||||
|
||||
|
||||
ARG GIT_SHA
|
||||
ARG APP_VERSION
|
||||
|
||||
ENV GIT_SHA=${GIT_SHA}
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
|
||||
# Install bash.
|
||||
RUN apk add bash && apk add curl
|
||||
|
||||
|
||||
# Install python
|
||||
RUN apk update && apk add --no-cache --virtual .gyp python3 make g++
|
||||
|
||||
#Use bash shell by default
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
|
||||
RUN mkdir /usr/src
|
||||
|
||||
WORKDIR /usr/src/Common
|
||||
COPY ./Common/package*.json /usr/src/Common/
|
||||
RUN npm install
|
||||
COPY ./Common /usr/src/Common
|
||||
|
||||
|
||||
WORKDIR /usr/src/Model
|
||||
COPY ./Model/package*.json /usr/src/Model/
|
||||
RUN npm install
|
||||
COPY ./Model /usr/src/Model
|
||||
|
||||
|
||||
|
||||
WORKDIR /usr/src/CommonServer
|
||||
COPY ./CommonServer/package*.json /usr/src/CommonServer/
|
||||
RUN npm install
|
||||
COPY ./CommonServer /usr/src/CommonServer
|
||||
|
||||
|
||||
|
||||
ENV PRODUCTION=true
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY ./Identity/package*.json /usr/src/app/
|
||||
RUN npm install
|
||||
|
||||
# Expose ports.
|
||||
# - 3087: OneUptime-backend
|
||||
EXPOSE 3087
|
||||
|
||||
{{ if eq .Env.ENVIRONMENT "development" }}
|
||||
#Run the app
|
||||
CMD [ "npm", "run", "dev" ]
|
||||
{{ else }}
|
||||
# Copy app source
|
||||
COPY ./Identity /usr/src/app
|
||||
# Bundle app source
|
||||
RUN npm run compile
|
||||
#Run the app
|
||||
CMD [ "npm", "start" ]
|
||||
{{ end }}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
{
|
||||
"saml2p:Response": {
|
||||
"$": {
|
||||
"Destination": "http://localhost/identity/idp-login/19462af5-a3c5-414d-bef8-20681c708579/56489843-5440-4305-a46a-c64215451b65",
|
||||
"ID": "id397086821029112924757763",
|
||||
"IssueInstant": "2023-03-05T17:44:26.808Z",
|
||||
"Version": "2.0",
|
||||
"xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
},
|
||||
"saml2:Issuer": [
|
||||
{
|
||||
"_": "http://www.okta.com/exk4fi36tv3SJZtRv697",
|
||||
"$": {
|
||||
"Format": "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:Signature": [
|
||||
{
|
||||
"$": {
|
||||
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#"
|
||||
},
|
||||
"ds:SignedInfo": [
|
||||
{
|
||||
"ds:CanonicalizationMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:SignatureMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:Reference": [
|
||||
{
|
||||
"$": {
|
||||
"URI": "#id397086821029112924757763"
|
||||
},
|
||||
"ds:Transforms": [
|
||||
{
|
||||
"ds:Transform": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ds:DigestMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:DigestValue": [
|
||||
"+tsuTZP7BGip7e83ZYPts5m5cYrzMqhrARClHNoRbpc="
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ds:SignatureValue": [
|
||||
"Bcq6j2LXUzrK7ZHSmvR/PkPrkNVmoeInyKxQOlAQ6CQPKSaEBn73jMlZU+AeaDmwwkb0UepX1yIiIkiX93202XDjeUxFe2QsZlKz7hm2JdZrTJTan1dg3sdSxwm3RAwGas03M7UDbtUuphdVfxO4aFavMAFlylXrvSN+dOE7pMpClSuIfk3ugmMysnfd0HlK7vosiD/GFEfUvpiVVyyzlJFdAijT1n4S5QkYPjBNJVkllwTiPTe7SVlmz/49fg16324UTczcuwOepFHNvqrskFT0T3pRDtFNdsnI94xpRYmTjJykdr59CshSElrZHZcne6zRkwb85pEn+qk3Npcg5A=="
|
||||
],
|
||||
"ds:KeyInfo": [
|
||||
{
|
||||
"ds:X509Data": [
|
||||
{
|
||||
"ds:X509Certificate": [
|
||||
"MIIDqjCCApKgAwIBAgIGAYao8ch8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG\nA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\nMBIGA1UECwwLU1NPUHJvdmlkZXIxFjAUBgNVBAMMDXRyaWFsLTk5NTU3NzUxHDAaBgkqhkiG9w0B\nCQEWDWluZm9Ab2t0YS5jb20wHhcNMjMwMzAzMTkyNjQzWhcNMzMwMzAzMTkyNzQzWjCBlTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTAL\nBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRYwFAYDVQQDDA10cmlhbC05OTU1Nzc1\nMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAx2HI6ri2hhgrdkBvLz3YJ1InOY3gQsbT/s/wtCw1dE9dmZnsurPSUPGZsf9Cof8WoIvq\nTPpTDjS0g4wx0mVjY6qejmVF4OFUCHrJPDNvO0Mey2cfK4tCBtvFtnUsntl1c2HCfdi/mMNVV3Di\nTATlWeCMf++HBHE65HKO8B+PCRHbt2jYYT1CtBGGk9yFP4SN0iBAFE+az5QAf86iW2ZYuUmkt87G\nYxBSuwj9h1rW9g6yr2sDX+9cXBPB4/4nq5lKBzaAt/O+UMXBoMiWP2xh+KyyccOv13xtDJpBhZDH\nEVQp5X10I1bm5/Y8QpXG9qEd1C6pa76w4VoPernnYYRa9QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBrE+qga2ZbF99L3wa0oy9sfSq9/6B6E8KODNeYOZ6OCTXij8EMcKYZGRXV22qHr7AwA0MVfYi5\ntt5jAuHTnKBPHaoYzYjQlu7/QXbte8WoximdliQraiD/zAps61Qj+c1Pctt1dwH3xu2Ppx0P3KvL\nBNE/LDMSADNpTL/o0jQnOzH9KP47iCI8lzdboHtegXuAnlCLj3uSYFy/W4KPm1FZHjZFkmmZz0SH\n/W2tTtkPfeSC+c0G8IhhUHLJ2QUEmH4Sk+l4PM5Jklb4RnABUKfiuel1zKlBE5hpWcn4cYWrHwCl\n1NKiHj2mkWIwVey813T/7M+nk93WK1G6psCleczN"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"saml2p:Status": [
|
||||
{
|
||||
"$": {
|
||||
"xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
},
|
||||
"saml2p:StatusCode": [
|
||||
{
|
||||
"$": {
|
||||
"Value": "urn:oasis:names:tc:SAML:2.0:status:Success"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"saml2:Assertion": [
|
||||
{
|
||||
"$": {
|
||||
"ID": "id397086821197064596402169",
|
||||
"IssueInstant": "2023-03-05T17:44:26.808Z",
|
||||
"Version": "2.0",
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
},
|
||||
"saml2:Issuer": [
|
||||
{
|
||||
"_": "http://www.okta.com/exk4fi36tv3SJZtRv697",
|
||||
"$": {
|
||||
"Format": "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:Signature": [
|
||||
{
|
||||
"$": {
|
||||
"xmlns:ds": "http://www.w3.org/2000/09/xmldsig#"
|
||||
},
|
||||
"ds:SignedInfo": [
|
||||
{
|
||||
"ds:CanonicalizationMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:SignatureMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:Reference": [
|
||||
{
|
||||
"$": {
|
||||
"URI": "#id397086821197064596402169"
|
||||
},
|
||||
"ds:Transforms": [
|
||||
{
|
||||
"ds:Transform": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ds:DigestMethod": [
|
||||
{
|
||||
"$": {
|
||||
"Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ds:DigestValue": [
|
||||
"eLop0iyvMdT62i2FD7hbULG3PfPuOsVaM/bBTgwBQJM="
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ds:SignatureValue": [
|
||||
"CkCV6PDaVUckuxCwI23p//eS+SXI6VwJXoLmftW8waF5wGIDLku9nXK+McbcroWHMBvyXa+hOeoFLEtb9YCy37yQMohKRvacKXI7waOx85egMYcZtGXMrK4BGt7m+A9Kqbu0LO0b52W3HyIJxXDlnJFYq8QnQzdX62KujnhtS61rjKoud/2TBzlu/dY/EAnLcCd1YlR737nnABMmRisAnf8E2NfEQ6eJV/Pf7EeO8jdB/QOqOFR2nt6N+oeBf6UG0Gsj5RBt9SD2FwmDYgB0s2jW5LYhw3bY0/6M8vUrADpraedIUg+M8+yByiHS6QRHJMWNdeJcZRu8RY4gkSrLUQ=="
|
||||
],
|
||||
"ds:KeyInfo": [
|
||||
{
|
||||
"ds:X509Data": [
|
||||
{
|
||||
"ds:X509Certificate": [
|
||||
"MIIDqjCCApKgAwIBAgIGAYao8ch8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG\nA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU\nMBIGA1UECwwLU1NPUHJvdmlkZXIxFjAUBgNVBAMMDXRyaWFsLTk5NTU3NzUxHDAaBgkqhkiG9w0B\nCQEWDWluZm9Ab2t0YS5jb20wHhcNMjMwMzAzMTkyNjQzWhcNMzMwMzAzMTkyNzQzWjCBlTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTAL\nBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRYwFAYDVQQDDA10cmlhbC05OTU1Nzc1\nMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAx2HI6ri2hhgrdkBvLz3YJ1InOY3gQsbT/s/wtCw1dE9dmZnsurPSUPGZsf9Cof8WoIvq\nTPpTDjS0g4wx0mVjY6qejmVF4OFUCHrJPDNvO0Mey2cfK4tCBtvFtnUsntl1c2HCfdi/mMNVV3Di\nTATlWeCMf++HBHE65HKO8B+PCRHbt2jYYT1CtBGGk9yFP4SN0iBAFE+az5QAf86iW2ZYuUmkt87G\nYxBSuwj9h1rW9g6yr2sDX+9cXBPB4/4nq5lKBzaAt/O+UMXBoMiWP2xh+KyyccOv13xtDJpBhZDH\nEVQp5X10I1bm5/Y8QpXG9qEd1C6pa76w4VoPernnYYRa9QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBrE+qga2ZbF99L3wa0oy9sfSq9/6B6E8KODNeYOZ6OCTXij8EMcKYZGRXV22qHr7AwA0MVfYi5\ntt5jAuHTnKBPHaoYzYjQlu7/QXbte8WoximdliQraiD/zAps61Qj+c1Pctt1dwH3xu2Ppx0P3KvL\nBNE/LDMSADNpTL/o0jQnOzH9KP47iCI8lzdboHtegXuAnlCLj3uSYFy/W4KPm1FZHjZFkmmZz0SH\n/W2tTtkPfeSC+c0G8IhhUHLJ2QUEmH4Sk+l4PM5Jklb4RnABUKfiuel1zKlBE5hpWcn4cYWrHwCl\n1NKiHj2mkWIwVey813T/7M+nk93WK1G6psCleczN"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"saml2:Subject": [
|
||||
{
|
||||
"$": {
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
},
|
||||
"saml2:NameID": [
|
||||
{
|
||||
"_": "simon.larsen@oneuptime.com",
|
||||
"$": {
|
||||
"Format": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
}
|
||||
}
|
||||
],
|
||||
"saml2:SubjectConfirmation": [
|
||||
{
|
||||
"$": {
|
||||
"Method": "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
||||
},
|
||||
"saml2:SubjectConfirmationData": [
|
||||
{
|
||||
"$": {
|
||||
"NotOnOrAfter": "2023-03-05T17:49:26.809Z",
|
||||
"Recipient": "http://localhost/identity/idp-login/19462af5-a3c5-414d-bef8-20681c708579/56489843-5440-4305-a46a-c64215451b65"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"saml2:Conditions": [
|
||||
{
|
||||
"$": {
|
||||
"NotBefore": "2023-03-05T17:39:26.809Z",
|
||||
"NotOnOrAfter": "2023-03-05T17:49:26.809Z",
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
},
|
||||
"saml2:AudienceRestriction": [
|
||||
{
|
||||
"saml2:Audience": [
|
||||
"oneuptime-org-test"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"saml2:AuthnStatement": [
|
||||
{
|
||||
"$": {
|
||||
"AuthnInstant": "2023-03-05T17:04:56.046Z",
|
||||
"SessionIndex": "id1678038266807.2057378521",
|
||||
"xmlns:saml2": "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
},
|
||||
"saml2:AuthnContext": [
|
||||
{
|
||||
"saml2:AuthnContextClassRef": [
|
||||
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<saml2p:Response Destination="http://localhost/identity/idp-login/19462af5-a3c5-414d-bef8-20681c708579/56489843-5440-4305-a46a-c64215451b65" ID="id393052211033737791644147" IssueInstant="2023-03-05T19:34:15.185Z" Version="2.0"
|
||||
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk4fi36tv3SJZtRv697
|
||||
</saml2:Issuer>
|
||||
<ds:Signature
|
||||
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:SignedInfo>
|
||||
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
||||
<ds:Reference URI="#id393052211033737791644147">
|
||||
<ds:Transforms>
|
||||
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
||||
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
</ds:Transforms>
|
||||
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||
<ds:DigestValue>r0iRM7DfiX6Kbmd7ecNjNzfveJzMAbdtldt/J/XcBh4=</ds:DigestValue>
|
||||
</ds:Reference>
|
||||
</ds:SignedInfo>
|
||||
<ds:SignatureValue>qo7/aSP6ZzgK30+mLJLdyADwALofCP53YGoW4ALagLegkUBkCjakr7pfTdxGjVB9wGLiEs+AYwurTgnqr90g04AdSkEYlFVqORBMQqd/0qR0ZE64TeoDcioF7nLH/71v88fdBGugv5i9YjJ1CGuSeA6rh+pFfJbLsTH9Ktaur3VcEKIY14RGQHFHYeXabBffNb2AQBhMVzgxTSmNIAT5fiaUTTgfw4oe+kNyVtY8jKrPxEqi9IYM4xiF6BhQDt79AZnwp1Acqof6iXyH5fCNufuv8SjEZyOSIMFOqbLyWIIlqini1UwF1jLlD+HvbZH3+rb2fIcFuVmcqD7KzOHU8g==</ds:SignatureValue>
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDqjCCApKgAwIBAgIGAYao8ch8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
|
||||
MBIGA1UECwwLU1NPUHJvdmlkZXIxFjAUBgNVBAMMDXRyaWFsLTk5NTU3NzUxHDAaBgkqhkiG9w0B
|
||||
CQEWDWluZm9Ab2t0YS5jb20wHhcNMjMwMzAzMTkyNjQzWhcNMzMwMzAzMTkyNzQzWjCBlTELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTAL
|
||||
BgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRYwFAYDVQQDDA10cmlhbC05OTU1Nzc1
|
||||
MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAx2HI6ri2hhgrdkBvLz3YJ1InOY3gQsbT/s/wtCw1dE9dmZnsurPSUPGZsf9Cof8WoIvq
|
||||
TPpTDjS0g4wx0mVjY6qejmVF4OFUCHrJPDNvO0Mey2cfK4tCBtvFtnUsntl1c2HCfdi/mMNVV3Di
|
||||
TATlWeCMf++HBHE65HKO8B+PCRHbt2jYYT1CtBGGk9yFP4SN0iBAFE+az5QAf86iW2ZYuUmkt87G
|
||||
YxBSuwj9h1rW9g6yr2sDX+9cXBPB4/4nq5lKBzaAt/O+UMXBoMiWP2xh+KyyccOv13xtDJpBhZDH
|
||||
EVQp5X10I1bm5/Y8QpXG9qEd1C6pa76w4VoPernnYYRa9QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQBrE+qga2ZbF99L3wa0oy9sfSq9/6B6E8KODNeYOZ6OCTXij8EMcKYZGRXV22qHr7AwA0MVfYi5
|
||||
tt5jAuHTnKBPHaoYzYjQlu7/QXbte8WoximdliQraiD/zAps61Qj+c1Pctt1dwH3xu2Ppx0P3KvL
|
||||
BNE/LDMSADNpTL/o0jQnOzH9KP47iCI8lzdboHtegXuAnlCLj3uSYFy/W4KPm1FZHjZFkmmZz0SH
|
||||
/W2tTtkPfeSC+c0G8IhhUHLJ2QUEmH4Sk+l4PM5Jklb4RnABUKfiuel1zKlBE5hpWcn4cYWrHwCl
|
||||
1NKiHj2mkWIwVey813T/7M+nk93WK1G6psCleczN</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</ds:Signature>
|
||||
<saml2p:Status
|
||||
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||
</saml2p:Status>
|
||||
<saml2:Assertion ID="id393052211179439228346088" IssueInstant="2023-03-05T19:34:15.185Z" Version="2.0"
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk4fi36tv3SJZtRv697
|
||||
</saml2:Issuer>
|
||||
<ds:Signature
|
||||
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:SignedInfo>
|
||||
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
||||
<ds:Reference URI="#id393052211179439228346088">
|
||||
<ds:Transforms>
|
||||
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
||||
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
||||
</ds:Transforms>
|
||||
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||
<ds:DigestValue>q58zNeULspCtjaeoJW4HFDrU1sbJDTBYB7jFvxTfBGI=</ds:DigestValue>
|
||||
</ds:Reference>
|
||||
</ds:SignedInfo>
|
||||
<ds:SignatureValue>Nhqd6JCCyLDkiDRFwXz8Y6nbTwb0CpH0SPYszpU6pvLTGMwU6jMiVuqhMFORUlb+wir/hgWLxS/dzv0QgudT/fMZ3FtjuC3TRYQJJaJHsC+rdhOBVdp4M0jZefSlSzFNyY9QC75QVkw1NsPe73pS6ldZ3f+Zx6QjKrXe+wUG6/5aBziQ+mVFA8I1URWAW767Uf3UazPsclYMirrt1mcsQHXQErlCg+Hf8adaZ1MLa5NfTLYucyXJiZKVg7gt8sXbB+2MgfboAVZS651Y/68v/zhR4EtZzanseaG9ICpm+fqUPkZ9r43xIlaHliTDrnScmrYM2PRX0iVsYNOLnhkXMQ==</ds:SignatureValue>
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIDqjCCApKgAwIBAgIGAYao8ch8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
|
||||
MBIGA1UECwwLU1NPUHJvdmlkZXIxFjAUBgNVBAMMDXRyaWFsLTk5NTU3NzUxHDAaBgkqhkiG9w0B
|
||||
CQEWDWluZm9Ab2t0YS5jb20wHhcNMjMwMzAzMTkyNjQzWhcNMzMwMzAzMTkyNzQzWjCBlTELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTAL
|
||||
BgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRYwFAYDVQQDDA10cmlhbC05OTU1Nzc1
|
||||
MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAx2HI6ri2hhgrdkBvLz3YJ1InOY3gQsbT/s/wtCw1dE9dmZnsurPSUPGZsf9Cof8WoIvq
|
||||
TPpTDjS0g4wx0mVjY6qejmVF4OFUCHrJPDNvO0Mey2cfK4tCBtvFtnUsntl1c2HCfdi/mMNVV3Di
|
||||
TATlWeCMf++HBHE65HKO8B+PCRHbt2jYYT1CtBGGk9yFP4SN0iBAFE+az5QAf86iW2ZYuUmkt87G
|
||||
YxBSuwj9h1rW9g6yr2sDX+9cXBPB4/4nq5lKBzaAt/O+UMXBoMiWP2xh+KyyccOv13xtDJpBhZDH
|
||||
EVQp5X10I1bm5/Y8QpXG9qEd1C6pa76w4VoPernnYYRa9QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQBrE+qga2ZbF99L3wa0oy9sfSq9/6B6E8KODNeYOZ6OCTXij8EMcKYZGRXV22qHr7AwA0MVfYi5
|
||||
tt5jAuHTnKBPHaoYzYjQlu7/QXbte8WoximdliQraiD/zAps61Qj+c1Pctt1dwH3xu2Ppx0P3KvL
|
||||
BNE/LDMSADNpTL/o0jQnOzH9KP47iCI8lzdboHtegXuAnlCLj3uSYFy/W4KPm1FZHjZFkmmZz0SH
|
||||
/W2tTtkPfeSC+c0G8IhhUHLJ2QUEmH4Sk+l4PM5Jklb4RnABUKfiuel1zKlBE5hpWcn4cYWrHwCl
|
||||
1NKiHj2mkWIwVey813T/7M+nk93WK1G6psCleczN</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</ds:Signature>
|
||||
<saml2:Subject
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">simon.larsen@oneuptime.com</saml2:NameID>
|
||||
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml2:SubjectConfirmationData NotOnOrAfter="2023-03-05T19:39:15.185Z" Recipient="http://localhost/identity/idp-login/19462af5-a3c5-414d-bef8-20681c708579/56489843-5440-4305-a46a-c64215451b65"/>
|
||||
</saml2:SubjectConfirmation>
|
||||
</saml2:Subject>
|
||||
<saml2:Conditions NotBefore="2023-03-05T19:29:15.185Z" NotOnOrAfter="2023-03-05T19:39:15.185Z"
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||
<saml2:AudienceRestriction>
|
||||
<saml2:Audience>oneuptime-org-test</saml2:Audience>
|
||||
</saml2:AudienceRestriction>
|
||||
</saml2:Conditions>
|
||||
<saml2:AuthnStatement AuthnInstant="2023-03-05T18:45:10.375Z" SessionIndex="id1678044855184.109954533"
|
||||
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
|
||||
<saml2:AuthnContext>
|
||||
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
|
||||
</saml2:AuthnContext>
|
||||
</saml2:AuthnStatement>
|
||||
</saml2:Assertion>
|
||||
</saml2p:Response>
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'ejs';
|
||||
import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase';
|
||||
import Express, { ExpressApplication } from 'CommonServer/Utils/Express';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import App from 'CommonServer/Utils/StartServer';
|
||||
import AuthenticationAPI from './API/Authentication';
|
||||
import SsoAPI from './API/SSO';
|
||||
import ResellerAPI from './API/Reseller';
|
||||
import StatusPageSsoAPI from './API/StatusPageSSO';
|
||||
import StatusPageAuthenticationAPI from './API/StatusPageAuthentication';
|
||||
import Redis from 'CommonServer/Infrastructure/Redis';
|
||||
import { ClickhouseAppInstance } from 'CommonServer/Infrastructure/ClickhouseDatabase';
|
||||
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
const APP_NAME: string = 'identity';
|
||||
|
||||
app.use([`/${APP_NAME}`, '/'], AuthenticationAPI);
|
||||
|
||||
app.use([`/${APP_NAME}`, '/'], ResellerAPI);
|
||||
|
||||
app.use([`/${APP_NAME}`, '/'], SsoAPI);
|
||||
|
||||
app.use([`/${APP_NAME}`, '/'], StatusPageSsoAPI);
|
||||
|
||||
app.use(
|
||||
[`/${APP_NAME}/status-page`, '/status-page'],
|
||||
StatusPageAuthenticationAPI
|
||||
);
|
||||
|
||||
const init: () => Promise<void> = async (): Promise<void> => {
|
||||
try {
|
||||
// init the app
|
||||
await App(APP_NAME);
|
||||
// connect to the database.
|
||||
await PostgresAppInstance.connect(
|
||||
PostgresAppInstance.getDatasourceOptions()
|
||||
);
|
||||
|
||||
// connect redis
|
||||
await Redis.connect();
|
||||
|
||||
await ClickhouseAppInstance.connect(
|
||||
ClickhouseAppInstance.getDatasourceOptions()
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('App Init Failed:');
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
init().catch((err: Error) => {
|
||||
logger.error(err);
|
||||
logger.info('Exiting node process');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { AccountsRoute } from 'Common/ServiceRoute';
|
||||
|
||||
import EmailVerificationTokenService from 'CommonServer/Services/EmailVerificationTokenService';
|
||||
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import EmailVerificationToken from 'Model/Models/EmailVerificationToken';
|
||||
|
||||
import MailService from 'CommonServer/Services/MailService';
|
||||
import EmailTemplateType from 'Common/Types/Email/EmailTemplateType';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
import User from 'Model/Models/User';
|
||||
import Hostname from 'Common/Types/API/Hostname';
|
||||
import Protocol from 'Common/Types/API/Protocol';
|
||||
import Email from 'Common/Types/Email';
|
||||
import DatabaseConfig from 'CommonServer/DatabaseConfig';
|
||||
|
||||
export default class AuthenticationEmail {
|
||||
public static async sendVerificationEmail(user: User): Promise<void> {
|
||||
const generatedToken: ObjectID = ObjectID.generate();
|
||||
|
||||
const emailVerificationToken: EmailVerificationToken =
|
||||
new EmailVerificationToken();
|
||||
emailVerificationToken.userId = user?.id as ObjectID;
|
||||
emailVerificationToken.email = user?.email as Email;
|
||||
emailVerificationToken.token = generatedToken;
|
||||
emailVerificationToken.expires = OneUptimeDate.getOneDayAfter();
|
||||
|
||||
await EmailVerificationTokenService.create({
|
||||
data: emailVerificationToken,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const host: Hostname = await DatabaseConfig.getHost();
|
||||
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
||||
|
||||
MailService.sendMail({
|
||||
toEmail: user.email!,
|
||||
subject: 'Please verify email.',
|
||||
templateType: EmailTemplateType.SignupWelcomeEmail,
|
||||
vars: {
|
||||
name: user.name?.toString() || '',
|
||||
tokenVerifyUrl: new URL(
|
||||
httpProtocol,
|
||||
host,
|
||||
new Route(AccountsRoute.toString()).addRoute(
|
||||
'/verify-email/' + generatedToken.toString()
|
||||
)
|
||||
).toString(),
|
||||
homeUrl: new URL(httpProtocol, host).toString(),
|
||||
},
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import { JSONArray, JSONObject } from 'Common/Types/JSON';
|
||||
import BadRequestException from 'Common/Types/Exception/BadRequestException';
|
||||
import Email from 'Common/Types/Email';
|
||||
import xmldom from 'xmldom';
|
||||
import xmlCrypto, { FileKeyInfo } from 'xml-crypto';
|
||||
import logger from 'CommonServer/Utils/Logger';
|
||||
|
||||
export default class SSOUtil {
|
||||
public static isPayloadValid(payload: JSONObject): void {
|
||||
if (
|
||||
!payload['saml2p:Response'] &&
|
||||
!payload['samlp:Response'] &&
|
||||
!payload['samlp:Response']
|
||||
) {
|
||||
throw new BadRequestException('SAML Response not found.');
|
||||
}
|
||||
|
||||
payload =
|
||||
(payload['saml2p:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject);
|
||||
|
||||
const issuers: JSONArray =
|
||||
(payload['saml2:Issuer'] as JSONArray) ||
|
||||
(payload['saml:Issuer'] as JSONArray);
|
||||
|
||||
if (issuers.length === 0) {
|
||||
throw new BadRequestException('Issuers not found');
|
||||
}
|
||||
|
||||
const issuer: JSONObject | string | undefined = issuers[0];
|
||||
|
||||
if (typeof issuer === 'string') {
|
||||
return issuer;
|
||||
}
|
||||
if (!issuer) {
|
||||
throw new BadRequestException('Issuer not found');
|
||||
}
|
||||
|
||||
const issuerUrl: string = issuer['_'] as string;
|
||||
|
||||
if (!issuerUrl) {
|
||||
throw new BadRequestException(
|
||||
'Issuer URL not found in SAML response'
|
||||
);
|
||||
}
|
||||
|
||||
const samlAssertion: JSONArray =
|
||||
(payload['saml2:Assertion'] as JSONArray) ||
|
||||
(payload['saml:Assertion'] as JSONArray);
|
||||
|
||||
if (!samlAssertion || samlAssertion.length === 0) {
|
||||
throw new BadRequestException('SAML Assertion not found');
|
||||
}
|
||||
|
||||
const samlSubject: JSONArray =
|
||||
((samlAssertion[0] as JSONObject)['saml2:Subject'] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)['saml:Subject'] as JSONArray);
|
||||
|
||||
if (!samlSubject || samlSubject.length === 0) {
|
||||
throw new BadRequestException('SAML Subject not found');
|
||||
}
|
||||
|
||||
const samlNameId: JSONArray =
|
||||
((samlSubject[0] as JSONObject)['saml2:NameID'] as JSONArray) ||
|
||||
((samlSubject[0] as JSONObject)['saml:NameID'] as JSONArray);
|
||||
|
||||
if (!samlNameId || samlNameId.length === 0) {
|
||||
throw new BadRequestException('SAML NAME ID not found');
|
||||
}
|
||||
|
||||
const emailString: string = (samlNameId[0] as JSONObject)[
|
||||
'_'
|
||||
] as string;
|
||||
|
||||
if (!emailString) {
|
||||
if (!samlNameId || samlNameId.length === 0) {
|
||||
throw new BadRequestException('SAML Email not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static isSignatureValid(
|
||||
samlPayload: string,
|
||||
certificate: string
|
||||
): boolean {
|
||||
try {
|
||||
const dom: Document = new xmldom.DOMParser().parseFromString(
|
||||
samlPayload
|
||||
);
|
||||
const signature: Element | undefined = dom.getElementsByTagNameNS(
|
||||
'http://www.w3.org/2000/09/xmldsig#',
|
||||
'Signature'
|
||||
)[0];
|
||||
const sig: xmlCrypto.SignedXml = new xmlCrypto.SignedXml();
|
||||
|
||||
sig.keyInfoProvider = {
|
||||
getKeyInfo: function (_key: any) {
|
||||
return `<X509Data><X509Certificate>${certificate}</X509Certificate></X509Data>`;
|
||||
},
|
||||
getKey: function () {
|
||||
return certificate;
|
||||
} as any,
|
||||
} as FileKeyInfo;
|
||||
|
||||
sig.loadSignature(signature!.toString());
|
||||
const res: boolean = sig.checkSignature(samlPayload);
|
||||
|
||||
return res;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static getEmail(payload: JSONObject): Email {
|
||||
if (!payload['saml2p:Response'] && !payload['samlp:Response']) {
|
||||
throw new BadRequestException('SAML Response not found.');
|
||||
}
|
||||
|
||||
payload =
|
||||
(payload['saml2p:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject);
|
||||
|
||||
const samlAssertion: JSONArray =
|
||||
(payload['saml2:Assertion'] as JSONArray) ||
|
||||
(payload['saml:Assertion'] as JSONArray);
|
||||
|
||||
if (!samlAssertion || samlAssertion.length === 0) {
|
||||
throw new BadRequestException('SAML Assertion not found');
|
||||
}
|
||||
|
||||
const samlSubject: JSONArray =
|
||||
((samlAssertion[0] as JSONObject)['saml2:Subject'] as JSONArray) ||
|
||||
((samlAssertion[0] as JSONObject)['saml:Subject'] as JSONArray);
|
||||
|
||||
if (!samlSubject || samlSubject.length === 0) {
|
||||
throw new BadRequestException('SAML Subject not found');
|
||||
}
|
||||
|
||||
const samlNameId: JSONArray =
|
||||
((samlSubject[0] as JSONObject)['saml2:NameID'] as JSONArray) ||
|
||||
((samlSubject[0] as JSONObject)['saml:NameID'] as JSONArray);
|
||||
|
||||
if (!samlNameId || samlNameId.length === 0) {
|
||||
throw new BadRequestException('SAML NAME ID not found');
|
||||
}
|
||||
|
||||
const emailString: string = (samlNameId[0] as JSONObject)[
|
||||
'_'
|
||||
] as string;
|
||||
|
||||
return new Email(emailString.trim());
|
||||
}
|
||||
|
||||
public static getIssuer(payload: JSONObject): string {
|
||||
if (!payload['saml2p:Response'] && !payload['samlp:Response']) {
|
||||
throw new BadRequestException('SAML Response not found.');
|
||||
}
|
||||
|
||||
payload =
|
||||
(payload['saml2p:Response'] as JSONObject) ||
|
||||
(payload['samlp:Response'] as JSONObject);
|
||||
|
||||
const issuers: JSONArray =
|
||||
(payload['saml2:Issuer'] as JSONArray) ||
|
||||
(payload['saml:Issuer'] as JSONArray);
|
||||
|
||||
if (issuers.length === 0) {
|
||||
throw new BadRequestException('Issuers not found');
|
||||
}
|
||||
|
||||
const issuer: JSONObject | string | undefined = issuers[0];
|
||||
|
||||
if (typeof issuer === 'string') {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
if (!issuer) {
|
||||
throw new BadRequestException('Issuer not found');
|
||||
}
|
||||
|
||||
const issuerUrl: string = issuer['_'] as string;
|
||||
|
||||
if (!issuerUrl) {
|
||||
throw new BadRequestException(
|
||||
'Issuer URL not found in SAML response'
|
||||
);
|
||||
}
|
||||
|
||||
return issuerUrl.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<%- include('./Partials/Head.ejs') -%>
|
||||
<title> <%= title %> </title>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
|
||||
<div class="h-full">
|
||||
|
||||
<main class="mx-auto max-w-7xl pb-10 lg:py-12 lg:px-8">
|
||||
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md flex justify-center mt-10 mb-10">
|
||||
<img
|
||||
class="mx-auto h-12 w-auto"
|
||||
src="/img/3-transparent.svg"
|
||||
alt="OneUptime"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="">
|
||||
<!-- Payment details -->
|
||||
<div class="space-y-6 sm:px-6 lg:col-span-9 lg:px-0">
|
||||
<section aria-labelledby="payment-details-heading">
|
||||
<form action="#" method="POST">
|
||||
<div class="shadow sm:overflow-hidden sm:rounded-md">
|
||||
<div class="bg-white py-6 px-4 sm:p-6">
|
||||
<div>
|
||||
<h2 id="payment-details-heading"
|
||||
class="text-lg font-medium leading-6 text-gray-900"><%= title %></h2>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= message %></p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,103 +0,0 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
|
||||
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||
background: transparent;
|
||||
bottom: 0;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
background: transparent;
|
||||
bottom: 0;
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({
|
||||
'gtm.start':
|
||||
new Date().getTime(), event: 'gtm.js'
|
||||
}); var f = d.getElementsByTagName(s)[0],
|
||||
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
|
||||
'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', 'GTM-PKQD5WH');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.async-hide {
|
||||
opacity: 0 !important
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="/img/favicons/favicon.ico">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicons/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="/img/favicons/safari-pinned-tab.svg" color="#32335b">
|
||||
<meta name="msapplication-TileColor" content="#32335b">
|
||||
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
|
||||
<link rel="icon" href="/img/ou-wb.svg">
|
||||
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
|
||||
<link rel="canonical" href="/">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
|
||||
<meta property="og:url" content="https://oneuptime.com">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description"
|
||||
content="OneUptime monitors websites, API's, and servers and alerts your team if something goes wrong. It also keeps your customers updated about any downtime. ">
|
||||
<meta property="og:image" content="https://oneuptime.com/img/hou-wb.svg">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="twitter:image" content="/img/ou-wb.svg">
|
||||
<meta name="twitter:site" content="@oneuptimeinc">
|
||||
<meta name="twitter:title" content="OneUptime - One Complete Observability platform.">
|
||||
<meta name="twitter:description"
|
||||
content="OneUptime monitors websites, API's, and servers and alerts your team if something goes wrong. It also keeps your customers updated about any downtime.">
|
||||
|
||||
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Corporation",
|
||||
"name": "OneUptime",
|
||||
"url": "https://www.oneuptime.com",
|
||||
"logo": "https://oneuptime.com/img/ou-bb.svg",
|
||||
"sameAs": [
|
||||
"https://www.facebook.com/oneuptimeinc",
|
||||
"https://twitter.com/OneUptimeInc",
|
||||
"https://www.linkedin.com/company/oneuptime"
|
||||
],
|
||||
"description": "OneUptime monitors websites, API's, and servers and alerts your team if something goes wrong. It also keeps your customers updated about any downtime."
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"watch": ["./","../Common", "../CommonServer", "../Model"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
}
|
||||
4214
Identity/package-lock.json
generated
4214
Identity/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "identity",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --require ts-node/register Index.ts",
|
||||
"compile": "tsc",
|
||||
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
|
||||
"dev": "npx nodemon",
|
||||
"audit": "npm audit --audit-level=low",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"test": "jest --detectOpenHandles",
|
||||
"coverage": "jest --detectOpenHandles --coverage"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"Common": "file:../Common",
|
||||
"CommonServer": "file:../CommonServer",
|
||||
"ejs": "^3.1.9",
|
||||
"Model": "file:../Model",
|
||||
"ts-node": "^10.9.1",
|
||||
"xml-crypto": "^3.2.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmldom": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/xml-crypto": "^1.4.5",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"@types/node": "^17.0.31",
|
||||
"jest": "^28.1.0",
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
{
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
// same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
// "module": "es2022" /* Specify what module code is generated. */,
|
||||
"rootDir": "", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
"types": ["node"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "build/dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
"strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -2,9 +2,6 @@ upstream accounts {
|
||||
server ${SERVER_ACCOUNTS_HOSTNAME}:${ACCOUNTS_PORT} weight=10 max_fails=3 fail_timeout=30s;
|
||||
}
|
||||
|
||||
upstream identity {
|
||||
server ${SERVER_IDENTITY_HOSTNAME}:${IDENTITY_PORT} weight=10 max_fails=3 fail_timeout=30s;
|
||||
}
|
||||
|
||||
upstream dashboard-api {
|
||||
server ${SERVER_DASHBOARD_API_HOSTNAME}:${DASHBOARD_API_PORT} weight=10 max_fails=3 fail_timeout=30s;
|
||||
@@ -102,7 +99,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page-sso/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page-sso/;
|
||||
}
|
||||
|
||||
location /status-page-identity-api/ {
|
||||
@@ -116,7 +113,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page/;
|
||||
}
|
||||
|
||||
# Acme Verification.
|
||||
@@ -200,7 +197,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page-sso/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page-sso/;
|
||||
}
|
||||
|
||||
location /status-page-identity-api/ {
|
||||
@@ -214,7 +211,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page/;
|
||||
}
|
||||
|
||||
location /status-page {
|
||||
@@ -300,7 +297,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page-sso/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page-sso/;
|
||||
}
|
||||
|
||||
location /status-page-identity-api/ {
|
||||
@@ -314,7 +311,7 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://identity/status-page/;
|
||||
proxy_pass http://dashboard-api/api/identity/status-page/;
|
||||
}
|
||||
|
||||
|
||||
@@ -423,7 +420,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://identity;
|
||||
proxy_pass http://dashboard-api/api/identity;
|
||||
}
|
||||
|
||||
location /reference {
|
||||
|
||||
@@ -81,9 +81,7 @@ SERVER_WORKFLOW_HOSTNAME=workflow
|
||||
SERVER_ALERT_HOSTNAME=alert
|
||||
SERVER_INGESTOR_HOSTNAME=ingestor
|
||||
SERVER_TEST_SERVER_HOSTNAME=test-server
|
||||
SERVER_FILE_HOSTNAME=file
|
||||
SERVER_HOME_HOSTNAME=home
|
||||
SERVER_IDENTITY_HOSTNAME=identity
|
||||
SERVER_NOTIFICATION_HOSTNAME=hostname
|
||||
SERVER_WORKERS_HOSTNAME=workers
|
||||
SERVER_STATUS_PAGE_HOSTNAME=status-page
|
||||
@@ -101,9 +99,7 @@ ALERT_PORT=3088
|
||||
INGESTOR_PORT=3400
|
||||
PROBE_PORT=3500
|
||||
TEST_SERVER_PORT=3800
|
||||
FILE_PORT=3125
|
||||
HOME_PORT=1444
|
||||
IDENTITY_PORT=3087
|
||||
NOTIFICATION_PORT=3191
|
||||
REALTIME_PORT=3300
|
||||
WORKERS_PORT=3452
|
||||
|
||||
@@ -23,9 +23,7 @@ x-common-variables: &common-variables
|
||||
SERVER_ALERT_HOSTNAME: alert
|
||||
SERVER_INGESTOR_HOSTNAME: ingestor
|
||||
SERVER_TEST_SERVER_HOSTNAME: test-server
|
||||
SERVER_FILE_HOSTNAME: file
|
||||
SERVER_HOME_HOSTNAME: home
|
||||
SERVER_IDENTITY_HOSTNAME: identity
|
||||
SERVER_NOTIFICATION_HOSTNAME: notification
|
||||
SERVER_WORKERS_HOSTNAME: workers
|
||||
SERVER_STATUS_PAGE_HOSTNAME: status-page
|
||||
@@ -43,9 +41,7 @@ x-common-variables: &common-variables
|
||||
INGESTOR_PORT: ${INGESTOR_PORT}
|
||||
PROBE_PORT: ${PROBE_PORT}
|
||||
TEST_SERVER_PORT: ${TEST_SERVER_PORT}
|
||||
FILE_PORT: ${FILE_PORT}
|
||||
HOME_PORT: ${HOME_PORT}
|
||||
IDENTITY_PORT: ${IDENTITY_PORT}
|
||||
NOTIFICATION_PORT: ${NOTIFICATION_PORT}
|
||||
REALTIME_PORT: ${REALTIME_PORT}
|
||||
WORKERS_PORT: ${WORKERS_PORT}
|
||||
@@ -173,8 +169,6 @@ services:
|
||||
environment:
|
||||
<<: *common-ui-variables
|
||||
PORT: ${ACCOUNTS_PORT}
|
||||
depends_on:
|
||||
- identity
|
||||
|
||||
|
||||
admin-dashboard:
|
||||
@@ -184,9 +178,6 @@ services:
|
||||
environment:
|
||||
<<: *common-ui-variables
|
||||
PORT: ${ADMIN_DASHBOARD_PORT}
|
||||
depends_on:
|
||||
- identity
|
||||
- accounts
|
||||
|
||||
dashboard:
|
||||
networks:
|
||||
@@ -195,9 +186,6 @@ services:
|
||||
environment:
|
||||
<<: *common-ui-variables
|
||||
PORT: ${DASHBOARD_PORT}
|
||||
depends_on:
|
||||
- identity
|
||||
- accounts
|
||||
|
||||
|
||||
|
||||
@@ -312,23 +300,6 @@ services:
|
||||
- ingestor
|
||||
links:
|
||||
- ingestor
|
||||
|
||||
|
||||
identity:
|
||||
networks:
|
||||
- oneuptime
|
||||
restart: always
|
||||
environment:
|
||||
<<: *common-server-variables
|
||||
PORT: ${IDENTITY_PORT}
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
- notification
|
||||
links:
|
||||
- redis
|
||||
- postgres
|
||||
- notification
|
||||
|
||||
|
||||
otel-collector:
|
||||
@@ -378,7 +349,6 @@ services:
|
||||
|
||||
ingress:
|
||||
depends_on:
|
||||
- identity
|
||||
- accounts
|
||||
- dashboard-api
|
||||
- dashboard
|
||||
|
||||
@@ -297,34 +297,7 @@ services:
|
||||
network: host
|
||||
context: .
|
||||
dockerfile: ./Probe/Dockerfile
|
||||
|
||||
|
||||
identity:
|
||||
ports:
|
||||
- '9132:9229' # Debugging port.
|
||||
volumes:
|
||||
- ./Identity:/usr/src/app
|
||||
# Use node modules of the container and not host system.
|
||||
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
|
||||
- /usr/src/app/node_modules/
|
||||
- ./Common:/usr/src/Common
|
||||
- ./Model:/usr/src/Model
|
||||
- ./CommonServer:/usr/src/CommonServer
|
||||
- ./CommonUI:/usr/src/CommonUI
|
||||
- /usr/src/Common/node_modules/
|
||||
- /usr/src/CommonUI/node_modules/
|
||||
- /usr/src/CommonServer/node_modules/
|
||||
- /usr/src/Model/node_modules/
|
||||
extends:
|
||||
file: ./docker-compose.base.yml
|
||||
service: identity
|
||||
build:
|
||||
network: host
|
||||
context: .
|
||||
dockerfile: ./Identity/Dockerfile
|
||||
|
||||
|
||||
|
||||
ingestor:
|
||||
volumes:
|
||||
- ./Ingestor:/usr/src/app
|
||||
|
||||
@@ -100,14 +100,6 @@ services:
|
||||
file: ./docker-compose.base.yml
|
||||
service: probe-2
|
||||
|
||||
identity:
|
||||
image: oneuptime/identity:${APP_TAG}
|
||||
extends:
|
||||
file: ./docker-compose.base.yml
|
||||
service: identity
|
||||
|
||||
|
||||
|
||||
ingestor:
|
||||
image: oneuptime/ingestor:${APP_TAG}
|
||||
extends:
|
||||
|
||||
Reference in New Issue
Block a user