[INIT] Initial Project Structure
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
34
.forgejo/workflows/deploy-edge.yml
Normal file
|
@ -0,0 +1,34 @@
|
|||
name: Deploy Edge
|
||||
run-name: ${{ forgejo.actor }} Deploy Edge
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
# Additional Deploy Envars
|
||||
GARDEN_DEPLOY_ACTION: cairo
|
||||
CAIRO_ALLOW_REGISTRATION: ${{ vars.CAIRO_ALLOW_REGISTRATION }}
|
||||
|
||||
jobs:
|
||||
deploy-edge:
|
||||
steps:
|
||||
# Setup Oasis
|
||||
- name: Oasis Setup
|
||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
|
||||
with:
|
||||
deploy-env: edge
|
||||
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
|
||||
extra-secret-paths: /dashboard,/alexandria
|
||||
extra-secret-envs: edge,edge
|
||||
# Deploy to Edge
|
||||
- name: Deploy to Edge env
|
||||
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
|
||||
working-directory: ${{ env.OASIS_WORKSPACE }}
|
||||
# Alert via Discord
|
||||
- name: Discord Alert
|
||||
if: always()
|
||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@discord-status
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: deployments
|
||||
header: DEPLOY EDGE
|
43
.forgejo/workflows/qa-api-tests.yml
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: QA API Tests
|
||||
run-name: ${{ forgejo.actor }} QA API Test
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
REPO_DIR: ${{ forgejo.workspace }}/cairo
|
||||
GARDEN_LINK_ACTION: build.cairo-image
|
||||
|
||||
jobs:
|
||||
qa-api-tests:
|
||||
steps:
|
||||
# Setup Oasis
|
||||
- name: Oasis Setup
|
||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
|
||||
with:
|
||||
deploy-env: ci
|
||||
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_CI_READ_TOKEN }}
|
||||
extra-secret-paths: /dashboard,/alexandria
|
||||
extra-secret-envs: ci,ci
|
||||
# Test Code
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ env.REPO_DIR }}
|
||||
# Garden tests
|
||||
- name: Link Repo code to Garden
|
||||
run: garden link action $GARDEN_LINK_ACTION $REPO_DIR --env usw-ci
|
||||
working-directory: ${{ env.OASIS_WORKSPACE }}
|
||||
# Cubit CI Tests
|
||||
- name: Run Cubit tests in CI env
|
||||
run: garden workflow qa-api-tests --env usw-ci --var ci-ttl=25m
|
||||
working-directory: ${{ env.OASIS_WORKSPACE }}
|
||||
# Discord Alert
|
||||
- name: Discord Alert
|
||||
if: always()
|
||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@discord-status
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: ci
|
||||
header: QA API Tests
|
||||
additional-content: "CI Namespace: `${{env.CI_NAMESPACE}}`"
|
16
.forgejo/workflows/s3-repo-backup.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: S3 Repo Backup
|
||||
run-name: ${{ forgejo.actor }} S3 Repo Backup
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
s3-repo-backup:
|
||||
steps:
|
||||
- name: S3 Backup
|
||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@s3-backup
|
||||
with:
|
||||
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
|
||||
- name: Status Alert
|
||||
if: always()
|
||||
run: echo "The Job ended with status ${{ job.status }}."
|
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Deploy ignores
|
||||
dist/
|
||||
build/
|
||||
node_modules
|
||||
# Env files
|
||||
.env
|
||||
.env.dev
|
||||
.env.prod
|
28
.helmignore
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
node_modules/
|
||||
src/
|
||||
build/
|
||||
public/
|
||||
lib/
|
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
@dunemask:registry=https://forgejo.dunemask.dev/api/packages/dunemask/npm/
|
13
.prettierrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
printWidth: 130
|
||||
semi: true
|
||||
singleQuote: false
|
||||
trailingComma: all
|
||||
bracketSpacing: true
|
||||
arrowParens: always
|
||||
requirePragma: false
|
||||
insertPragma: false
|
||||
|
||||
overrides:
|
||||
- files: ["lib/router/ClientErrors.ts", "lib/router/routes/*.ts"]
|
||||
options:
|
||||
printWidth: 250
|
24
Chart.yaml
Normal file
|
@ -0,0 +1,24 @@
|
|||
apiVersion: v2
|
||||
name: cairo
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.0.4"
|
21
Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
|||
FROM node:20-bookworm-slim
|
||||
WORKDIR /dunemask/net/cairo
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
COPY .npmrc .
|
||||
RUN npm ci
|
||||
# Copy server build resources over
|
||||
COPY tsconfig.json .
|
||||
COPY tsconfig.server.json .
|
||||
COPY lib lib
|
||||
COPY prisma prisma
|
||||
RUN npm run db:generate
|
||||
# Copy react build resources over
|
||||
COPY public public
|
||||
COPY src src
|
||||
COPY index.html .
|
||||
COPY vite.config.ts .
|
||||
# Build Project
|
||||
RUN npm run package:full
|
||||
|
||||
CMD ["npm","start"]
|
504
LICENSE
Normal file
|
@ -0,0 +1,504 @@
|
|||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Cairo
|
||||
Typescript Authentication & Authorization Server
|
37
index.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Elysium App Authentication" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/icons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#f1ce36" />
|
||||
<link rel="shortcut icon" href="/icons/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#ffc40d" />
|
||||
<meta name="msapplication-config" content="/icons/browserconfig.xml" />
|
||||
<meta name="theme-color" content="#333333" />
|
||||
<title>Cairo</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
32
lib/Cairo.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Import Core Modules
|
||||
import { INFO, OK, logInfo } from "@dunemask/vix/logging";
|
||||
import { Vixpress } from "@dunemask/vix";
|
||||
import { MappedRoute, routePreviews } from "@dunemask/vix/express";
|
||||
import figlet from "figlet";
|
||||
import AppRouter from "./vix/AppRouter.js";
|
||||
import PostgresService from "./database/PostgresService.js";
|
||||
import AppInitService from "./services/app-init.service.js";
|
||||
|
||||
export default class Cairo extends Vixpress {
|
||||
static PostgresService: string = "pg";
|
||||
static AppInitService: string = "app-init";
|
||||
constructor(port?: number) {
|
||||
super("Cairo", port ?? Number(process.env.CAIRO_DEV_PORT ?? 52000));
|
||||
this.setService(Cairo.PostgresService, PostgresService);
|
||||
this.setService(Cairo.AppInitService, AppInitService);
|
||||
this.setRouter(AppRouter);
|
||||
}
|
||||
|
||||
protected async preconfigure(): Promise<void> {
|
||||
logInfo(figlet.textSync(this.title, "Cosmike"));
|
||||
}
|
||||
|
||||
protected async onStart() {
|
||||
const previews = routePreviews(this.app, (r: MappedRoute, methodDisplay: string) => {
|
||||
const authSection = r.routeMetadata?.authType === "user" ? "🔒" : " ";
|
||||
return `${methodDisplay} ${authSection} ${r.path}`;
|
||||
});
|
||||
for (const p of previews) INFO("ROUTE", p);
|
||||
OK("SERVER", `${this.title} server running on ${this.port} 🚀`);
|
||||
}
|
||||
}
|
7
lib/app.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import "dotenv-expand/config";
|
||||
import Cairo from "./Cairo";
|
||||
import { assertRequired } from "./config";
|
||||
assertRequired();
|
||||
const cairo = new Cairo();
|
||||
await cairo.start();
|
49
lib/config.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import DefaultRolePolicies from "./vix/AppPolicies";
|
||||
|
||||
const requiredEnvars: string[] = ["CAIRO_KEYPAIR_KEY"];
|
||||
|
||||
const encodedEnvar = (envar: string | undefined) => (!!envar ? Buffer.from(envar, "base64").toString("utf8") : envar);
|
||||
|
||||
export function assertRequired() {
|
||||
for (const e of requiredEnvars) if (!process.env[e]) throw Error(`Envar '${e}' is required!`);
|
||||
}
|
||||
|
||||
export default {
|
||||
Server: {
|
||||
basePath: "/cairo/",
|
||||
projectSlug: "$cairo",
|
||||
projectName: "$cairo",
|
||||
rootPassword: process.env.CAIRO_ROOT_PASSWORD,
|
||||
},
|
||||
RolePolicy: {
|
||||
Root: {
|
||||
id: "ck1ro7ekp000203zu5gn3d9cr",
|
||||
name: "Root",
|
||||
policies: DefaultRolePolicies.Root,
|
||||
},
|
||||
Admin: {
|
||||
id: "ck1ro7bm0000103z5h45sswqs",
|
||||
name: "Admin",
|
||||
policies: DefaultRolePolicies.Admin,
|
||||
},
|
||||
User: {
|
||||
id: "ck1ro7g3e000303z52ee63nqs",
|
||||
name: "User",
|
||||
policies: DefaultRolePolicies.User,
|
||||
},
|
||||
},
|
||||
SigningOptions: {
|
||||
HashRounds: 12,
|
||||
Version: "0.0.1-alpha",
|
||||
Issuer: encodedEnvar(process.env.CAIRO_HOSTNAME) ?? "https://cairo.dunemask.net",
|
||||
Keys: {
|
||||
KeyPair: encodedEnvar(process.env.CAIRO_KEYPAIR_KEY) ?? "keypair-key",
|
||||
},
|
||||
Subjects: {
|
||||
User: "user",
|
||||
Cargo: "cargo",
|
||||
Runner: "runner",
|
||||
Pod: "pod",
|
||||
},
|
||||
},
|
||||
};
|
54
lib/database/PostgresService.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { VixpressService } from "@dunemask/vix";
|
||||
import { VERB } from "@dunemask/vix/logging";
|
||||
import UsersTableService from "./tables/UsersTableService.js";
|
||||
import RolePolicyTableService from "./tables/RolePolicyTableService.js";
|
||||
import ProjectTableService from "./tables/ProjectTableService.js";
|
||||
import KeyPairTableService from "./tables/KeyPairTableService.js";
|
||||
|
||||
export class DBPrismaClient extends PrismaClient {
|
||||
private async queryUniqueOrThrow<T = unknown>(data: unknown): Promise<T | undefined> {
|
||||
if (!Array.isArray(data)) throw Error("Returned non-array!");
|
||||
if (data.length > 1) throw Error("Non unique value found!");
|
||||
return data.length === 1 ? data[0] : undefined;
|
||||
}
|
||||
async $queryRawUnique<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Promise<T | undefined> {
|
||||
const data = await this.$queryRaw(query, ...values);
|
||||
return this.queryUniqueOrThrow<T>(data);
|
||||
}
|
||||
|
||||
async $queryRawUnsafeUnique<T = unknown>(query: string, ...values: any[]): Promise<T | undefined> {
|
||||
const data = await this.$queryRawUnsafe(query, ...values);
|
||||
return this.queryUniqueOrThrow<T>(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default class PostgresService extends VixpressService {
|
||||
declare pg: DBPrismaClient;
|
||||
declare users: UsersTableService;
|
||||
declare rolePolicy: RolePolicyTableService;
|
||||
declare project: ProjectTableService;
|
||||
declare keypair: KeyPairTableService;
|
||||
|
||||
async configureService() {
|
||||
this.pg = new DBPrismaClient({
|
||||
errorFormat: "pretty",
|
||||
log: ["warn", "error", "info", { emit: "event", level: "query" }],
|
||||
});
|
||||
this.users = new UsersTableService(this.pg);
|
||||
this.rolePolicy = new RolePolicyTableService(this.pg);
|
||||
this.project = new ProjectTableService(this.pg);
|
||||
this.keypair = new KeyPairTableService(this.pg);
|
||||
}
|
||||
|
||||
async startService() {
|
||||
VERB("POSTGRES", "Connecting to postgres....");
|
||||
await this.pg.$connect();
|
||||
await this.project.$upsertDefaultProject();
|
||||
await this.keypair.$upsertDefaultKeyPairs();
|
||||
await this.rolePolicy.$upsertDefaultAuthorities();
|
||||
const user = await this.users.$upsertDefaultRootUser();
|
||||
if (!!user) VERB("APP INIT", `Created identity 'root' with password '${user.password}'!`);
|
||||
if (!!user) VERB("APP INIT", "This will not be shown again!");
|
||||
}
|
||||
}
|
9
lib/database/TableService.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { DBPrismaClient } from "./PostgresService";
|
||||
|
||||
export default abstract class TableService {
|
||||
declare pg: DBPrismaClient;
|
||||
protected abstract table: string;
|
||||
constructor(pg: DBPrismaClient) {
|
||||
this.pg = pg;
|
||||
}
|
||||
}
|
60
lib/database/tables/KeyPairTableService.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CKeyPairContract } from "@lib/contracts/keypair.contracts";
|
||||
import { KeyPairErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { encrypt, generateKeypair } from "@lib/svc/crypt.service";
|
||||
import { KeyPair, KeyPairType } from "@prisma/client";
|
||||
|
||||
declare type Custom = "Custom"; // Make sure this matches KeyPairType.Custom;
|
||||
|
||||
export default class KeyPairTableService extends TableService {
|
||||
protected table = "KeyPair";
|
||||
async byId(keypairId: string) {
|
||||
const keypair = this.pg.keyPair.findUnique({ where: { id: keypairId } });
|
||||
if (!keypair) throw KeyPairErrors.NotFoundKeypair;
|
||||
}
|
||||
|
||||
async byUsage(projectIdentity: string, usage: Custom): Promise<KeyPair[]>;
|
||||
async byUsage(projectIdentity: string, usage: Exclude<KeyPairType, Custom>): Promise<KeyPair | null>;
|
||||
async byUsage(projectIdentity: string, usage: KeyPairType): Promise<KeyPair[] | KeyPair | null> {
|
||||
const projectOr = { OR: [{ id: projectIdentity }, { slug: projectIdentity }] };
|
||||
const projectInclude = { keyPairs: { where: { usage: KeyPairType.UserToken } } };
|
||||
const project = await this.pg.project.findFirst({ where: projectOr, include: projectInclude });
|
||||
if (!project) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
const keypairs = project.keyPairs;
|
||||
if (usage !== KeyPairType.Custom && keypairs.length > 1)
|
||||
throw new Error(`Multiple keypairs found for project ${projectIdentity} and usage ${usage}`);
|
||||
if (usage !== KeyPairType.Custom && keypairs.length === 0) return null;
|
||||
if (usage !== KeyPairType.Custom) return keypairs[0];
|
||||
return keypairs;
|
||||
}
|
||||
|
||||
async $upsertDefaultKeyPairs() {
|
||||
const projectSlug = config.Server.projectSlug;
|
||||
const cairoProject = await this.pg.project.findUnique({ where: { slug: projectSlug } });
|
||||
if (!cairoProject) throw new Error("Cairo Project Not Found!");
|
||||
const projectId = cairoProject.id;
|
||||
await this.upsertProjecttDefaultKeyPairs(projectId);
|
||||
}
|
||||
|
||||
async upsertProjecttDefaultKeyPairs(projectId: string) {
|
||||
const storeKeypair = this.create.bind(this);
|
||||
const keyTypes = Object.values(KeyPairType).filter((kp) => kp !== KeyPairType.Custom);
|
||||
await Promise.all(
|
||||
keyTypes.map(async (kp) => {
|
||||
const existingKp = await this.byUsage(projectId, kp);
|
||||
if (!!existingKp) return;
|
||||
const { publicKey, privateKey } = await generateKeypair();
|
||||
const [encryptedPrivateKey, encryptedPublicKey] = await Promise.all([
|
||||
encrypt(privateKey, config.SigningOptions.Keys.KeyPair),
|
||||
encrypt(publicKey, config.SigningOptions.Keys.KeyPair),
|
||||
]);
|
||||
return storeKeypair({ encryptedPrivateKey, encryptedPublicKey, projectId, usage: kp });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async create(keypair: CKeyPairContract["Create"]) {
|
||||
return this.pg.keyPair.create({ data: keypair });
|
||||
}
|
||||
}
|
34
lib/database/tables/ProjectTableService.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CProjectContract } from "@lib/types/ContractTypes";
|
||||
import { ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { VERB } from "@dunemask/vix/logging";
|
||||
|
||||
export default class ProjectTableService extends TableService {
|
||||
protected table = "Project";
|
||||
async byId(projectId: string) {
|
||||
return this.pg.project.findUnique({ where: { id: projectId } });
|
||||
}
|
||||
|
||||
async bySlug(slug: string) {
|
||||
return this.pg.project.findUnique({ where: { slug } });
|
||||
}
|
||||
|
||||
async $upsertDefaultProject() {
|
||||
const { projectSlug: slug, projectName: name } = config.Server;
|
||||
const createOptions = { slug, name, parentProject: slug };
|
||||
const existingProject = await this.pg.project.findUnique({ where: { slug } });
|
||||
if (!!existingProject) return VERB("PROJECT", "Default project already exists!");
|
||||
VERB("PROJECT", "Default project not found! Creating now!");
|
||||
const proj = await this.pg.project.upsert({ where: { slug }, create: createOptions, update: createOptions });
|
||||
await this.pg.project.update({ where: { id: proj.id }, data: { parentProject: proj.id } }); // Use ProjectID instead of slug
|
||||
}
|
||||
|
||||
async create(project: CProjectContract["Create"] & { parentProject: string }) {
|
||||
const existingProject = await this.pg.project.findMany({ where: { id: project.slug } });
|
||||
if (existingProject.length > 1) throw ProjectErrors.BadRequestSlugInvalid;
|
||||
return this.pg.project.create({ data: project }).catch(() => {
|
||||
throw ProjectErrors.ConflictNonUnique;
|
||||
});
|
||||
}
|
||||
}
|
37
lib/database/tables/RolePolicyTableService.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import TableService from "../TableService";
|
||||
import { AuthorityType } from "@prisma/client";
|
||||
import { Policy, PolicyDefault } from "@lib/Policies";
|
||||
import config from "@lib/config";
|
||||
import { CRolePolicyContract } from "@lib/contracts/role-policy.contracts";
|
||||
|
||||
export default class RolePolicyTableService extends TableService {
|
||||
protected table = "RolePolicy";
|
||||
async byId(id: string) {
|
||||
return this.pg.rolePolicy.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async $upsertDefaultAuthorities() {
|
||||
const projectSlug = config.Server.projectSlug;
|
||||
const cairoProject = await this.pg.project.findUnique({ where: { slug: projectSlug } });
|
||||
if (!cairoProject) throw new Error("Cairo Project Not Found!");
|
||||
const project = cairoProject.id;
|
||||
const $chk = ({ id, name, policies }: PolicyDefault) => this.$upsertDefaultsAuthority(project, name, id, policies);
|
||||
await Promise.all(Object.values(config.RolePolicy).map($chk));
|
||||
}
|
||||
|
||||
private async $upsertDefaultsAuthority(projectId: string, name: string, id: string, userPolicies: Policy[]) {
|
||||
const rootAuthority = config.RolePolicy.Root.id;
|
||||
const authorityType = id === rootAuthority ? AuthorityType.Root : AuthorityType.RolePolicy;
|
||||
const authority = id === rootAuthority ? name : rootAuthority; // Set Root Authority to root if root
|
||||
const policies = Policy.asStrings(userPolicies);
|
||||
return this.pg.rolePolicy.upsert({
|
||||
where: { id },
|
||||
create: { projectId, id, name, policies, authority, authorityType },
|
||||
update: { projectId, name, policies, authority, authorityType },
|
||||
});
|
||||
}
|
||||
|
||||
async create(rp: CRolePolicyContract["Create"]) {
|
||||
return this.pg.rolePolicy.create({ data: rp });
|
||||
}
|
||||
}
|
63
lib/database/tables/UsersTableService.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CUserContract } from "@lib/types/ContractTypes";
|
||||
import { hashText } from "@lib/modules/auth/auth.service";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import { UserErrors } from "@lib/vix/ClientErrors";
|
||||
|
||||
// prettier-ignore
|
||||
const generateBase64Password = (length: number = 32): string => Array.from(crypto.getRandomValues(new Uint8Array(length)), byte => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.charAt(byte % 64)).join('');
|
||||
|
||||
export default class UsersTableService extends TableService {
|
||||
protected table = "User";
|
||||
async byId(userId: string) {
|
||||
return this.pg.user.findUnique({ where: { id: userId }, include: { rolePolicy: true, project: true } });
|
||||
}
|
||||
|
||||
async byUsername(username: string, projectId: string) {
|
||||
return this.pg.user.findUnique({ where: { projectId_username: { projectId, username } } });
|
||||
}
|
||||
|
||||
async byEmail(email: string, projectId: string) {
|
||||
return this.pg.user.findUnique({ where: { projectId_email: { projectId, email } } });
|
||||
}
|
||||
|
||||
async $upsertDefaultRootUser() {
|
||||
const project = await this.pg.project.findUnique({ where: { slug: config.Server.projectSlug } });
|
||||
if (!project) throw new Error("Cairo Project Not Found!");
|
||||
const rolePolicyId = config.RolePolicy.Root.id;
|
||||
return this.$upsertRootUser(project.id, rolePolicyId);
|
||||
}
|
||||
|
||||
async $upsertRootUser(projectId: string, rolePolicyId: string) {
|
||||
const root = await this.pg.user.findUnique({ where: { projectId_username: { username: "root", projectId } } });
|
||||
if (!!root) return;
|
||||
const password = config.Server.rootPassword ?? generateBase64Password();
|
||||
const hash = await hashText(password);
|
||||
const user = await this.pg.user.create({ data: { projectId, username: "root", email: "root", hash, rolePolicyId } });
|
||||
return { ...user, password };
|
||||
}
|
||||
|
||||
async create(options: CUserContract["Create"]) {
|
||||
const { hash, projectId, rolePolicyId } = options;
|
||||
const username = options.username?.toLowerCase();
|
||||
const email = options.email?.toLowerCase() ?? undefined;
|
||||
const [existingUsername, existingEmail] = await Promise.all([
|
||||
this.byUsername(username, projectId),
|
||||
!!email ? this.byEmail(email, projectId) : undefined,
|
||||
]);
|
||||
if (!existingUsername || !existingEmail) throw UserErrors.ConflictIdentityTaken;
|
||||
const userData = { projectId, username, email, hash, rolePolicyId };
|
||||
return this.pg.user.create({ data: userData, include: { rolePolicy: true } });
|
||||
}
|
||||
|
||||
async byIdentity(projectIdentity: string, identity: string) {
|
||||
const username = identity.toLowerCase();
|
||||
const email = identity.toLowerCase();
|
||||
const OrUser = { OR: [{ username }, { email }] };
|
||||
const OrProject = { project: { OR: [{ id: projectIdentity }, { slug: projectIdentity }] } };
|
||||
const projectInclude = { include: { keyPairs: { where: { usage: KeyPairType.UserToken } } } };
|
||||
const AND = [OrUser, OrProject];
|
||||
return this.pg.user.findFirst({ where: { AND }, include: { rolePolicy: true, project: projectInclude } });
|
||||
}
|
||||
}
|
22
lib/middlewares/policy-guard.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Request, Response, NextFunction, Router, Express } from "express";
|
||||
import userGuard from "./user-guard";
|
||||
import { MetadataRouter } from "@dunemask/vix/express";
|
||||
import { Policy } from "@lib/Policies";
|
||||
import { AuthErrors } from "@lib/vix/ClientErrors";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
|
||||
export default function policyMiddlewareGuard(requiredPolicies: Policy[]) {
|
||||
const middlewares: MetadataRouter = Router({ mergeParams: true });
|
||||
|
||||
async function policyAuthMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
const { user, policies: userPolicies } = req as UserRequest;
|
||||
if (!user) throw AuthErrors.UnauthorizedRequest;
|
||||
if (!userPolicies) throw AuthErrors.UnauthorizedRequest;
|
||||
if (!Policy.multiAuthorizedTo(userPolicies, requiredPolicies)) throw AuthErrors.ForbiddenPermissions;
|
||||
if (!next) return res.sendStatus(200);
|
||||
next();
|
||||
}
|
||||
middlewares.routeMetadata = { authType: "policy" };
|
||||
middlewares.use([userGuard(), policyAuthMiddleware]);
|
||||
return middlewares;
|
||||
}
|
41
lib/middlewares/user-guard.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { AuthorizedTokenRequest, MetadataRouter, tokenAuthMiddleware } from "@dunemask/vix/express";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import { getUserTokenId } from "@lib/modules/auth/auth.service";
|
||||
import { Policy, PolicyComputeType } from "@lib/Policies";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
import { AuthErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { Request, Response, NextFunction, Router, Express } from "express";
|
||||
import expressBearerToken from "express-bearer-token";
|
||||
import type PostgresService from "@lib/database/PostgresService.js";
|
||||
import { KeyPairType, User } from "@prisma/client";
|
||||
|
||||
export default function userGuard() {
|
||||
const middlewares: MetadataRouter = Router({ mergeParams: true });
|
||||
async function userGuardMiddleware(req: Request, _res: Response, next: NextFunction) {
|
||||
const { token } = req as AuthorizedTokenRequest;
|
||||
if (!token) throw AuthErrors.UnauthorizedRequiredToken;
|
||||
|
||||
const PostgresService = req.app.get(Cairo.PostgresService) as PostgresService;
|
||||
const { project } = req.params;
|
||||
if (!project) throw AuthErrors.UnauthorizedRequiredProject;
|
||||
|
||||
const userKeypair = await PostgresService.keypair.byUsage(project, KeyPairType.UserToken);
|
||||
if (!userKeypair) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
|
||||
const id = await getUserTokenId(token, userKeypair.encryptedPublicKey);
|
||||
if (!id) throw AuthErrors.UnauthorizedRequest;
|
||||
const user = await PostgresService.users.byId(id);
|
||||
if (!user) throw AuthErrors.UnauthorizedRequiredUser;
|
||||
const policies = Policy.parseResourcePolicies<Resource>(user.rolePolicy.policies as PolicyComputeType);
|
||||
const projectData = { ...user.project };
|
||||
delete (user as Partial<typeof user>).project;
|
||||
(req as UserRequest).user = user;
|
||||
(req as UserRequest).policies = policies;
|
||||
(req as UserRequest).project = projectData;
|
||||
next();
|
||||
}
|
||||
middlewares.routeMetadata = { authType: "user" };
|
||||
middlewares.use([expressBearerToken(), userGuardMiddleware]);
|
||||
return middlewares;
|
||||
}
|
48
lib/modules/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Request, Response, Express } from "express";
|
||||
import { VixpressController } from "@dunemask/vix";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import type PostgresService from "@lib/database/PostgresService";
|
||||
import { CAuthContract, AuthContract } from "@lib/contracts/auth.contracts";
|
||||
import { ContractRouteContext } from "@dunemask/vix/express";
|
||||
import { AuthErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { getUserToken, hashCompare } from "./auth.service";
|
||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { ResourcePolicy } from "@dunemask/vix/util";
|
||||
|
||||
type LoginCRC = ContractRouteContext<{
|
||||
RequestBodyContract: typeof AuthContract.Login;
|
||||
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
||||
}>;
|
||||
|
||||
export default class AuthController extends VixpressController {
|
||||
declare pg: PostgresService;
|
||||
constructor(app: Express) {
|
||||
super(app);
|
||||
this.pg = this.app.get(Cairo.PostgresService);
|
||||
}
|
||||
|
||||
verify = (_req: Request, res: Response) => res.sendStatus(200);
|
||||
|
||||
async login(crc: LoginCRC): Promise<CAuthContract["LoginCredentials"]> {
|
||||
const { identity, password } = crc.reqBody;
|
||||
const { project } = crc.reqParams;
|
||||
const user = await this.pg.users.byIdentity(project, identity);
|
||||
if (!user?.rolePolicy?.policies) throw AuthErrors.UnauthorizedRequest;
|
||||
const authorized = await hashCompare(password, user.hash);
|
||||
if (!authorized) throw AuthErrors.UnauthorizedRequest;
|
||||
const projectKeyPairs = user.project.keyPairs;
|
||||
if (projectKeyPairs.length !== 1) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
const token = await getUserToken(user.id, user.project.keyPairs[0].encryptedPrivateKey);
|
||||
const policies = user.rolePolicy.policies;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
return { token, user: userData, policies };
|
||||
}
|
||||
|
||||
async credentials(crc: ContractRouteContext): Promise<CAuthContract["Credentials"]> {
|
||||
const { user, policies } = crc.req as UserRequest;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
return { user: userData, policies: ResourcePolicy.asStrings(policies) };
|
||||
}
|
||||
}
|
26
lib/modules/auth/auth.router.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { VixpressRoute } from "@dunemask/vix";
|
||||
import { contract } from "@dunemask/vix/express";
|
||||
import RouteGuard from "@lib/vix/RouteGuard";
|
||||
import { Router } from "express";
|
||||
import AuthController from "./auth.controller";
|
||||
import { AuthContract } from "@lib/contracts/auth.contracts";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
|
||||
export class AuthRoute extends VixpressRoute {
|
||||
async configureRoutes(router: Router) {
|
||||
const jsonOpts = { limit: "20mb" };
|
||||
const cBase = { json: jsonOpts, reqParams: ProjectContract.ProjectParams };
|
||||
// Controllers
|
||||
const authController = this.useController(AuthController);
|
||||
|
||||
// Configuration
|
||||
const loginCreds = { ...cBase, reqBody: AuthContract.Login, resBody: AuthContract.LoginCredentials };
|
||||
const credRes = { ...cBase, resBody: AuthContract.Credentials };
|
||||
// Middleware
|
||||
|
||||
// Routes
|
||||
router.get("/verify", RouteGuard.User, authController.verify);
|
||||
router.post("/login", contract(authController.login, loginCreds));
|
||||
router.get("/credentials", RouteGuard.User, contract(authController.credentials, credRes));
|
||||
}
|
||||
}
|
36
lib/modules/auth/auth.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import { signToken, verifyToken } from "@lib/svc/token.service";
|
||||
import config from "@lib/config";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
const { HashRounds } = config.SigningOptions;
|
||||
|
||||
export async function getUserToken(id: string, encryptedPrivateKey: string) {
|
||||
const privateKey = await decrypt(encryptedPrivateKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenPayload = {
|
||||
iss: config.SigningOptions.Issuer,
|
||||
sub: [config.SigningOptions.Subjects.User],
|
||||
aud: [config.SigningOptions.Issuer],
|
||||
id,
|
||||
};
|
||||
return signToken(tokenPayload, privateKey);
|
||||
}
|
||||
|
||||
export async function userTokenLogin(token: string, encryptedPublicKey: string): Promise<boolean> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return !!verifyToken(token, publicKey);
|
||||
}
|
||||
|
||||
export async function getUserTokenId(token: string, encryptedPublicKey: string): Promise<string | undefined> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenData = verifyToken(token, publicKey);
|
||||
if (!tokenData) return undefined;
|
||||
return tokenData.id;
|
||||
}
|
||||
|
||||
export async function hashText(password: string) {
|
||||
return bcrypt.hash(password, HashRounds);
|
||||
}
|
||||
|
||||
export async function hashCompare(password: string, hash: string) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
48
lib/modules/projects/project.controller.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Express } from "express";
|
||||
import { VixpressController } from "@dunemask/vix";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import type PostgresService from "@lib/database/PostgresService";
|
||||
import { ContractRouteContext } from "@dunemask/vix/express";
|
||||
import { ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||
import { CProjectContract, ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
import config from "@lib/config";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { PolicyString } from "@lib/Policies";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
|
||||
type CreateCRC = ContractRouteContext<{
|
||||
RequestBodyContract: typeof ProjectContract.Create;
|
||||
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
||||
}>;
|
||||
|
||||
export default class ProjectController extends VixpressController {
|
||||
declare pg: PostgresService;
|
||||
constructor(app: Express) {
|
||||
super(app);
|
||||
this.pg = this.app.get(Cairo.PostgresService);
|
||||
}
|
||||
|
||||
async create(crc: CreateCRC): Promise<CProjectContract["CreateResponse"]> {
|
||||
const { project: parentProject } = crc.req as UserRequest;
|
||||
const proj = await this.pg.project.create({ ...crc.reqBody, parentProject: parentProject.id });
|
||||
const rolePolicy = await this.pg.rolePolicy.create({
|
||||
name: `${crc.reqBody.slug} Project Root`,
|
||||
authority: config.RolePolicy.Root.id,
|
||||
projectId: proj.id,
|
||||
policies: [`${Resource.CairoProjectRoot}.root`] as PolicyString[],
|
||||
});
|
||||
const [user] = await Promise.all([
|
||||
this.pg.users.$upsertRootUser(proj.id, rolePolicy.id),
|
||||
this.pg.keypair.upsertProjecttDefaultKeyPairs(proj.id),
|
||||
]);
|
||||
const kp = await this.pg.keypair.byUsage(proj.id, KeyPairType.UserToken);
|
||||
if (!kp) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
if (!user) throw ProjectErrors.UnexpectedRootUserError;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
const publicKey = await decrypt(kp.encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return { user: userData, project: proj, publicKey };
|
||||
}
|
||||
}
|
22
lib/modules/projects/project.router.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { VixpressRoute } from "@dunemask/vix";
|
||||
import { contract } from "@dunemask/vix/express";
|
||||
import { Router } from "express";
|
||||
import ProjectController from "./project.controller";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import RouteGuard from "@lib/vix/RouteGuard";
|
||||
|
||||
export class ProjectRoute extends VixpressRoute {
|
||||
async configureRoutes(router: Router) {
|
||||
const jsonOpts = { limit: "20mb" };
|
||||
const cBase = { json: jsonOpts, reqParams: ProjectContract.ProjectParams };
|
||||
// Controllers
|
||||
const projController = this.useController(ProjectController);
|
||||
|
||||
// Configuration
|
||||
const projCreate = { ...cBase, reqBody: ProjectContract.Create };
|
||||
// Middleware
|
||||
|
||||
// Routes
|
||||
router.post("/create", RouteGuard.MangeProjectsCreate, contract(projController.create, projCreate));
|
||||
}
|
||||
}
|
36
lib/modules/projects/project.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import { signToken, verifyToken } from "@lib/svc/token.service";
|
||||
import config from "@lib/config";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
const { HashRounds } = config.SigningOptions;
|
||||
|
||||
export async function getUserToken(id: string, encryptedPrivateKey: string) {
|
||||
const privateKey = await decrypt(encryptedPrivateKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenPayload = {
|
||||
iss: config.SigningOptions.Issuer,
|
||||
sub: [config.SigningOptions.Subjects.User],
|
||||
aud: [config.SigningOptions.Issuer],
|
||||
id,
|
||||
};
|
||||
return signToken(tokenPayload, privateKey);
|
||||
}
|
||||
|
||||
export async function userTokenLogin(token: string, encryptedPublicKey: string): Promise<boolean> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return !!verifyToken(token, publicKey);
|
||||
}
|
||||
|
||||
export async function getUserTokenId(token: string, encryptedPublicKey: string): Promise<string | undefined> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenData = verifyToken(token, publicKey);
|
||||
if (!tokenData) return undefined;
|
||||
return tokenData.id;
|
||||
}
|
||||
|
||||
export async function hashText(password: string) {
|
||||
return bcrypt.hash(password, HashRounds);
|
||||
}
|
||||
|
||||
export async function hashCompare(password: string, hash: string) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
9
lib/services/app-init.service.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { VixpressService } from "@dunemask/vix";
|
||||
import { OK, VERB } from "@dunemask/vix/logging";
|
||||
|
||||
export default class AppInitService extends VixpressService {
|
||||
async startService() {
|
||||
VERB("APP INIT", "Running init services....");
|
||||
OK("APP INIT", "Done!");
|
||||
}
|
||||
}
|
37
lib/services/crypt.service.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import crypto, { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||
|
||||
export async function generateKeypair() {
|
||||
return crypto.generateKeyPairSync("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function encrypt(plaintext: string, hexKey: string): Promise<string> {
|
||||
const key = Buffer.from(hexKey, "hex");
|
||||
const algorithm = "aes-256-cbc"; // Encryption algorithm
|
||||
const iv = randomBytes(16); // Initialization vector
|
||||
|
||||
const cipher = createCipheriv(algorithm, key, iv);
|
||||
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
// Combine IV and encrypted text
|
||||
return iv.toString("hex") + ":" + encrypted;
|
||||
}
|
||||
|
||||
// Decrypt function
|
||||
export async function decrypt(encryptedText: string, hexKey: string): Promise<string> {
|
||||
const key = Buffer.from(hexKey, "hex");
|
||||
const algorithm = "aes-256-cbc";
|
||||
const textParts = encryptedText.split(":");
|
||||
const iv = Buffer.from(textParts[0], "hex");
|
||||
const encrypted = textParts[1];
|
||||
|
||||
const decipher = createDecipheriv(algorithm, key, iv);
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
15
lib/services/token.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import jwt, { Secret, SignOptions } from "jsonwebtoken";
|
||||
|
||||
export function signToken(payload: object, signingKey: Secret, options: SignOptions = {}) {
|
||||
return jwt.sign(payload, signingKey, {
|
||||
...{ algorithm: "RS256" },
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyToken(token: string, signingKey: Secret) {
|
||||
return jwtVerify(token, signingKey) ?? undefined;
|
||||
}
|
||||
|
||||
const jwtVerify = (token: string, key: Secret): any =>
|
||||
jwt.verify(token, key, (err: any, decoded: any) => (!err && decoded) || null);
|
9
lib/types/ApiRequests.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Policy } from "@lib/Policies";
|
||||
import { AuthorizedTokenRequest } from "@dunemask/vix/express";
|
||||
import { Project, User } from "@prisma/client";
|
||||
|
||||
export interface UserRequest extends AuthorizedTokenRequest {
|
||||
user: User;
|
||||
policies: Policy[];
|
||||
project: Project;
|
||||
}
|
5
lib/types/ContractTypes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type { CAuthContract } from "./contracts/auth.contracts";
|
||||
export type { CDatabaseContract } from "./contracts/database.contracts";
|
||||
export type { CUserContract } from "./contracts/user.contracts";
|
||||
export type { CProjectContract } from "./contracts/project.contracts";
|
||||
export type { CKeyPairContract } from "./contracts/keypair.contracts";
|
40
lib/types/contracts/auth.contracts.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
import { DatabaseContractRes } from "./database.contracts";
|
||||
|
||||
// ======================================= Re-used Contracts -======================================
|
||||
|
||||
const Credentials = y.object({
|
||||
user: DatabaseContractRes.User.required(),
|
||||
policies: y.array(y.string()).required(),
|
||||
});
|
||||
|
||||
// ====================================== Responses Contracts ======================================
|
||||
|
||||
export const AuthContractRes = defineContractExport("CAuthContractRes", {
|
||||
Credentials,
|
||||
LoginCredentials: y
|
||||
.object({
|
||||
token: y.string().required(),
|
||||
})
|
||||
.concat(Credentials),
|
||||
});
|
||||
|
||||
// ======================================= Request Contracts =======================================
|
||||
|
||||
export const AuthContractReq = defineContractExport("CAuthContractReq", {
|
||||
Login: y.object({
|
||||
identity: y.string().required(),
|
||||
password: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ===================================== Combined Declarations =====================================
|
||||
|
||||
export const AuthContract = defineContractExport("CAuthContract", { ...AuthContractRes, ...AuthContractReq });
|
||||
|
||||
// ======================================= Type Declarations =======================================
|
||||
|
||||
export type CAuthContractRes = ContractTypeDefinitions<typeof AuthContractRes>;
|
||||
export type CAuthContractReq = ContractTypeDefinitions<typeof AuthContractReq>;
|
||||
export type CAuthContract = ContractTypeDefinitions<typeof AuthContract>;
|
35
lib/types/contracts/database.contracts.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const DatabaseContractRes = defineContractExport("CDatabaseContractRes", {
|
||||
User: y.object({
|
||||
username: y.string().required(),
|
||||
email: y.string().nullable(),
|
||||
hash: antiRequired,
|
||||
rolePolicyId: y.string().required(),
|
||||
}),
|
||||
Project: y.object({
|
||||
id: y.string().required(),
|
||||
slug: y.string().required(),
|
||||
parentProject: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const DatabaseContractReq = defineContractExport("CDatabaseContractReq", {});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const DatabaseContract = defineContractExport("CDatabaseContract", { ...DatabaseContractRes, ...DatabaseContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CDatabaseContractRes = ContractTypeDefinitions<typeof DatabaseContractRes>;
|
||||
export type CDatabaseContractReq = ContractTypeDefinitions<typeof DatabaseContractReq>;
|
||||
export type CDatabaseContract = ContractTypeDefinitions<typeof DatabaseContract>;
|
31
lib/types/contracts/keypair.contracts.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !!value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const KeyPairContractRes = defineContractExport("CKeyPairContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const KeyPairContractReq = defineContractExport("CKeyPairContractReq", {
|
||||
Create: y.object({
|
||||
projectId: y.string().required(),
|
||||
usage: y.string().oneOf(Object.values(KeyPairType)).required(),
|
||||
encryptedPublicKey: y.string().required(),
|
||||
encryptedPrivateKey: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const KeyPairContract = defineContractExport("CKeyPairContract", { ...KeyPairContractRes, ...KeyPairContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CKeyPairContractRes = ContractTypeDefinitions<typeof KeyPairContractRes>;
|
||||
export type CKeyPairContractReq = ContractTypeDefinitions<typeof KeyPairContractReq>;
|
||||
export type CKeyPairContract = ContractTypeDefinitions<typeof KeyPairContract>;
|
36
lib/types/contracts/project.contracts.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
import { DatabaseContract } from "./database.contracts";
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const ProjectContractRes = defineContractExport("CProjectContractRes", {
|
||||
CreateResponse: y.object({
|
||||
project: DatabaseContract.Project,
|
||||
user: DatabaseContract.User,
|
||||
publicKey: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const ProjectContractReq = defineContractExport("CProjectContractReq", {
|
||||
ProjectParams: y.object({
|
||||
project: y.string().required(),
|
||||
}),
|
||||
Create: y.object({
|
||||
slug: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const ProjectContract = defineContractExport("CProjectContract", { ...ProjectContractRes, ...ProjectContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CProjectContractRes = ContractTypeDefinitions<typeof ProjectContractRes>;
|
||||
export type CProjectContractReq = ContractTypeDefinitions<typeof ProjectContractReq>;
|
||||
export type CProjectContract = ContractTypeDefinitions<typeof ProjectContract>;
|
34
lib/types/contracts/role-policy.contracts.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import { AuthorityType } from "@prisma/client";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const RolePolicyContractRes = defineContractExport("CRolePolicyContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const RolePolicyContractReq = defineContractExport("CRolePolicyContractReq", {
|
||||
Create: y.object({
|
||||
authority: y.string().required(),
|
||||
authorityType: y.string().oneOf(Object.values(AuthorityType)),
|
||||
projectId: y.string().required(),
|
||||
name: y.string().required(),
|
||||
policies: y.array(y.string().required()).required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const RolePolicyContract = defineContractExport("CRolePolicyContract", {
|
||||
...RolePolicyContractRes,
|
||||
...RolePolicyContractReq,
|
||||
});
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CRolePolicyContractRes = ContractTypeDefinitions<typeof RolePolicyContractRes>;
|
||||
export type CRolePolicyContractReq = ContractTypeDefinitions<typeof RolePolicyContractReq>;
|
||||
export type CRolePolicyContract = ContractTypeDefinitions<typeof RolePolicyContract>;
|
29
lib/types/contracts/user.contracts.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ContractTypeDefinitions, defineContractExport } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const UserContractRes = defineContractExport("CUserContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const UserContractReq = defineContractExport("CUserContractReq", {
|
||||
Create: y.object({
|
||||
projectId: y.string().required(),
|
||||
username: y.string().required(),
|
||||
email: y.string(),
|
||||
hash: y.string().required(),
|
||||
rolePolicyId: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const UserContract = defineContractExport("CUserContract", { ...UserContractRes, ...UserContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CUserContractRes = ContractTypeDefinitions<typeof UserContractRes>;
|
||||
export type CUserContractReq = ContractTypeDefinitions<typeof UserContractReq>;
|
||||
export type CUserContract = ContractTypeDefinitions<typeof UserContract>;
|
16
lib/util/mailing.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import sgMail from "@sendgrid/mail";
|
||||
|
||||
const { CAIRO_BOT_EMAIL: botEmail, CAIRO_SENDGRID_KEY: sendgridApiKey } = process.env;
|
||||
|
||||
// Configure API Key
|
||||
sgMail.setApiKey(sendgridApiKey ?? "");
|
||||
if (!botEmail && !!sendgridApiKey) throw Error("Bot Email wasn't defined but API key was!");
|
||||
|
||||
const from = botEmail ?? "donotreply@dunemask.dev";
|
||||
const ignoreMessage = `If you did not sign up for a cairo account, please ignore this email!`;
|
||||
|
||||
export const sendMessage = (to: string, subject: string, text: string) =>
|
||||
sgMail.send({ from, to, subject, text: text + `\n${ignoreMessage}` });
|
||||
|
||||
export const sendHtml = (to: string, subject: string, html: string) =>
|
||||
sgMail.send({ from, to, subject, html: html + `<p>${ignoreMessage}</p>` });
|
15
lib/vix/AppGuards.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { IAMResource, ManagementResource } from "./AppResources";
|
||||
import { Policy, PolicyType } from "./AppPolicies";
|
||||
|
||||
function appPolicies(...userPolicies: PolicyType[] | PolicyType[][]) {
|
||||
const policies = userPolicies.length === 1 && Array.isArray(userPolicies[0]) ? userPolicies[0] : (userPolicies as PolicyType[]);
|
||||
const requiredPolicies = Policy.parseResourcePolicies(policies);
|
||||
return requiredPolicies;
|
||||
}
|
||||
|
||||
export default class AppGuard {
|
||||
static IAMRoot = appPolicies(`${IAMResource.Root}.root`);
|
||||
static IAMAuthenticated = appPolicies(Object.values(IAMResource).map((iam) => `${iam}.root`) as PolicyType[]);
|
||||
static ManageProjects = appPolicies(`${ManagementResource.ManageProject}.*`);
|
||||
static CreateProjects = appPolicies(`${ManagementResource.ManageProject}.create`);
|
||||
}
|
26
lib/vix/AppPolicies.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { ResourcePolicy, ResourcePolicyComputeType, ResourcePolicyString, ResourcePolicyType } from "@dunemask/vix/util";
|
||||
import { IAMResource, ManagementResource, Resource } from "./AppResources";
|
||||
|
||||
export const Policy = ResourcePolicy<Resource>;
|
||||
export declare type Policy = ResourcePolicy<Resource>;
|
||||
export declare type PolicyType = ResourcePolicyType<Resource>;
|
||||
export declare type PolicyComputeType = ResourcePolicyComputeType<Resource>;
|
||||
export declare type PolicyString = ResourcePolicyString<Resource>;
|
||||
export declare type PolicyDefault = { id: string; name: string; policies: Policy[] };
|
||||
|
||||
export default class DefaultRolePolicies {
|
||||
static Root = $unsafeGetRootPolicy();
|
||||
static Admin = Policy.multiple<Resource>(
|
||||
`${IAMResource.Admin}.root`,
|
||||
`${ManagementResource.ManageProject}.root`,
|
||||
`${ManagementResource.ManageUser}.root`,
|
||||
);
|
||||
|
||||
static User = Policy.multiple<Resource>(`${IAMResource.User}.root`);
|
||||
}
|
||||
|
||||
function $unsafeGetRootPolicy(): Policy[] {
|
||||
const policies: PolicyString[] = [];
|
||||
for (const resource of Object.values(Resource)) policies.push(`${resource}.root`);
|
||||
return Policy.multiple<Resource>(...policies) as Policy[];
|
||||
}
|
20
lib/vix/AppResources.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export enum IAMResource {
|
||||
Root = "root",
|
||||
Admin = "admin",
|
||||
User = "user",
|
||||
CairoProjectRoot = "cairo-project-root",
|
||||
}
|
||||
|
||||
export enum ManagementResource {
|
||||
ManageAdmin = "manage-admin",
|
||||
ManageUser = "manage-user",
|
||||
ManageProject = "manage-project",
|
||||
}
|
||||
|
||||
export enum OtherResource {
|
||||
Random = "Random",
|
||||
}
|
||||
|
||||
type ResourceEnums<T extends Record<string, string>> = T[keyof T];
|
||||
export const Resource = { ...IAMResource, ...ManagementResource, ...OtherResource };
|
||||
export type Resource = ResourceEnums<typeof Resource>;
|
15
lib/vix/AppRouter.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import "express-async-errors";
|
||||
import config from "@lib/config";
|
||||
import { VixpressRouter } from "@dunemask/vix";
|
||||
import { AuthRoute } from "@lib/modules/auth/auth.router";
|
||||
import { ProjectRoute } from "@lib/modules/projects/project.router";
|
||||
|
||||
export default class AppRouter extends VixpressRouter {
|
||||
protected routerImportUrl = import.meta.url; // Used to configure the relative static route
|
||||
protected baseUrl = config.Server.basePath; // Path for static assets
|
||||
async configureRoutes() {
|
||||
// API Routes go here:
|
||||
await this.useRoute("/api/:project/auth", AuthRoute);
|
||||
await this.useRoute("/api/:project", ProjectRoute);
|
||||
}
|
||||
}
|
25
lib/vix/ClientErrors.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ClientError } from "@dunemask/vix/bridge";
|
||||
|
||||
export class AuthErrors {
|
||||
static readonly UnauthorizedRequiredProject = new ClientError(401, "Project required!");
|
||||
static readonly UnauthorizedRequiredToken = new ClientError(401, "Token required!");
|
||||
static readonly UnauthorizedRequiredUser = new ClientError(401, "User not set!");
|
||||
static readonly UnauthorizedRole = new ClientError(403, "Insufficient Privileges");
|
||||
static readonly UnauthorizedRequest = new ClientError(401, "Unauthorized!");
|
||||
static readonly ForbiddenPermissions = new ClientError(403, "Insufficient privileges!");
|
||||
}
|
||||
|
||||
export class UserErrors {
|
||||
static readonly ConflictIdentityTaken = new ClientError(409, "Identity taken!");
|
||||
}
|
||||
|
||||
export class ProjectErrors {
|
||||
static readonly BadRequestSlugInvalid = new ClientError(400, "Project slug invalid!");
|
||||
static readonly BadRequestProjectIncomplete = new ClientError(400, "Project incomplete!");
|
||||
static readonly UnexpectedRootUserError = new ClientError(500, "Error creating root user!");
|
||||
static readonly ConflictNonUnique = new ClientError(409, "Slug already taken!");
|
||||
}
|
||||
|
||||
export class KeyPairErrors {
|
||||
static readonly NotFoundKeypair = new ClientError(400, "Keypair not found!");
|
||||
}
|
9
lib/vix/RouteGuard.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import policyMiddlewareGuard from "@lib/middlewares/policy-guard";
|
||||
import userGuard from "@lib/middlewares/user-guard";
|
||||
import AppGuard from "./AppGuards";
|
||||
|
||||
export default class RouteGuard {
|
||||
static User = userGuard();
|
||||
static ManageProjectsRead = policyMiddlewareGuard(AppGuard.ManageProjects);
|
||||
static MangeProjectsCreate = policyMiddlewareGuard(AppGuard.CreateProjects);
|
||||
}
|
6913
package-lock.json
generated
Normal file
96
package.json
Normal file
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"name": "cairo",
|
||||
"version": "0.0.4",
|
||||
"description": "Typescript Authentication & Authorization Server",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node dist/app.js",
|
||||
"start:dev": "concurrently \"CAIRO_DEV_PORT=52025 nodemon lib/app.ts\" \" CAIRO_VITE_DEV_PORT=52000 CAIRO_VITE_BACKEND_URL=http://localhost:52025 vite\" -n s,v -p -c green,yellow",
|
||||
"build:server": "esbuild `find lib \\( -name '*.ts' \\)` --tsconfig=tsconfig.server.json --outdir=build/server && tsc-alias -p tsconfig.server.json",
|
||||
"build:all": "rm -Rf build && concurrently --kill-others-on-fail \"vite build\" \"npm run build:server\" -n s,v -c cyan,yellow",
|
||||
"package:dist": "mkdir -p dist && mv build/server/* dist/ && mv build/vite dist/static && rm -Rf build",
|
||||
"package:full": "rm -Rf dist && npm run build:all && npm run package:dist",
|
||||
"format": "prettier -w src lib vite.config.ts tsconfig*.json && prisma format",
|
||||
"tsc": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"tsc -p tsconfig.server.json --noEmit\" -n s,v -c cyan,yellow",
|
||||
"generate:api": "vix --generate-api --vixpress-path lib/Cairo.ts",
|
||||
"db:generate": "prisma generate",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"Cairo",
|
||||
"Dunemask",
|
||||
"Authentication"
|
||||
],
|
||||
"author": "Dunemask",
|
||||
"license": "LGPL-2.1",
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/figlet": "^1.5.8",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^22.5.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"esbuild": "^0.23.1",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsx": "^4.17.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.2",
|
||||
"vite-bundle-analyzer": "^0.10.6",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dunemask/vix": "^0.0.1-alpha.0",
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/material": "^5.16.7",
|
||||
"@prisma/client": "^5.18.0",
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cron": "^3.1.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"express": "^4.19.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-bearer-token": "^2.4.0",
|
||||
"figlet": "^1.7.0",
|
||||
"framer-motion": "^11.3.30",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"prisma": "^5.18.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"lib"
|
||||
],
|
||||
"ext": "ts",
|
||||
"execMap": {
|
||||
"ts": "tsx --tsconfig tsconfig.server.json"
|
||||
}
|
||||
}
|
||||
}
|
75
prisma/migrations/20240824171333_schema_init/migration.sql
Normal file
|
@ -0,0 +1,75 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AuthorityType" AS ENUM ('Root', 'User', 'RolePolicy');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "KeyPairType" AS ENUM ('UserToken', 'Custom');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Project" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"parentProject" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
|
||||
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"hash" TEXT NOT NULL,
|
||||
"rolePolicyId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RolePolicy" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"authority" TEXT NOT NULL,
|
||||
"authorityType" "AuthorityType" NOT NULL DEFAULT 'RolePolicy',
|
||||
"name" TEXT NOT NULL,
|
||||
"policies" TEXT[],
|
||||
|
||||
CONSTRAINT "RolePolicy_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "KeyPair" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"usage" "KeyPairType" NOT NULL,
|
||||
"name" TEXT,
|
||||
"encryptedPrivateKey" TEXT NOT NULL,
|
||||
"encryptedPublicKey" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "KeyPair_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_id_key" ON "User"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_projectId_username_key" ON "User"("projectId", "username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_projectId_email_key" ON "User"("projectId", "email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_rolePolicyId_fkey" FOREIGN KEY ("rolePolicyId") REFERENCES "RolePolicy"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RolePolicy" ADD CONSTRAINT "RolePolicy_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "KeyPair" ADD CONSTRAINT "KeyPair_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
3
prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
73
prisma/schema.prisma
Normal file
|
@ -0,0 +1,73 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("CAIRO_POSTGRES_URI")
|
||||
}
|
||||
|
||||
// Models
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
parentProject String
|
||||
name String?
|
||||
users User[]
|
||||
rolePolicies RolePolicy[]
|
||||
keyPairs KeyPair[]
|
||||
}
|
||||
|
||||
// User
|
||||
model User {
|
||||
id String @id @unique @default(cuid())
|
||||
username String
|
||||
email String?
|
||||
hash String
|
||||
rolePolicyId String
|
||||
projectId String
|
||||
|
||||
// Relations
|
||||
rolePolicy RolePolicy @relation(fields: [rolePolicyId], references: [id])
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
|
||||
// Unique constraints
|
||||
@@unique([projectId, username])
|
||||
@@unique([projectId, email])
|
||||
}
|
||||
|
||||
model RolePolicy {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
authority String
|
||||
authorityType AuthorityType @default(RolePolicy)
|
||||
name String
|
||||
policies String[]
|
||||
|
||||
// Relations
|
||||
users User[]
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
}
|
||||
|
||||
model KeyPair {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
usage KeyPairType // Application Level Uniqueness for non-custom usages. For example, there can only be 1 UserToken Keypair
|
||||
name String?
|
||||
encryptedPrivateKey String
|
||||
encryptedPublicKey String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
|
||||
// Application Level Uniqueness for non-custom usages. For example, there can only be 1 UserToken Keypair
|
||||
}
|
||||
|
||||
enum AuthorityType {
|
||||
Root
|
||||
User
|
||||
RolePolicy
|
||||
}
|
||||
|
||||
enum KeyPairType {
|
||||
UserToken
|
||||
Custom
|
||||
}
|
BIN
public/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
public/icons/apple-touch-icon-114x114.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
public/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
public/icons/apple-touch-icon-144x144.png
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
public/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
public/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/icons/apple-touch-icon-57x57.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/apple-touch-icon-72x72.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 11 KiB |
9
public/icons/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/icons/mstile-150x150.png"/>
|
||||
<TileColor>#ffc40d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
public/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/icons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
165
public/icons/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1711 4941 c-37 -4 -66 -11 -68 -18 -3 -7 -21 -15 -41 -19 -20 -4
|
||||
-44 -12 -52 -19 -8 -8 -27 -17 -42 -20 -16 -4 -28 -11 -28 -15 0 -4 -30 -20
|
||||
-67 -35 -38 -16 -74 -37 -81 -47 -7 -10 -20 -18 -27 -18 -8 0 -20 -9 -27 -20
|
||||
-7 -11 -18 -20 -25 -20 -7 0 -13 -3 -13 -7 0 -11 -50 -33 -72 -33 -10 0 -24
|
||||
-11 -30 -25 -6 -14 -20 -25 -31 -25 -11 0 -30 -11 -43 -25 -13 -14 -27 -25
|
||||
-32 -25 -5 0 -15 -6 -21 -14 -6 -7 -24 -16 -40 -19 -16 -3 -36 -13 -44 -22 -9
|
||||
-8 -18 -15 -22 -15 -4 0 -13 -7 -21 -15 -9 -8 -27 -18 -42 -21 -15 -4 -35 -15
|
||||
-44 -24 -9 -10 -39 -23 -67 -30 l-51 -12 0 -174 c0 -133 3 -174 13 -174 25 0
|
||||
92 29 114 48 12 11 38 23 57 26 20 4 36 11 36 16 0 6 9 10 20 10 10 0 23 6 27
|
||||
13 4 6 25 20 46 29 20 10 37 22 37 28 0 5 7 10 15 10 8 0 24 9 35 20 11 11 29
|
||||
20 39 20 11 0 28 10 38 23 10 13 38 33 63 45 25 12 53 31 62 42 10 11 26 20
|
||||
37 20 10 0 24 7 31 15 7 8 20 15 29 15 9 0 22 9 29 20 6 11 28 26 47 34 19 8
|
||||
35 19 35 24 0 5 18 12 40 16 22 3 40 11 40 16 0 6 8 10 17 10 23 0 85 23 100
|
||||
36 40 38 360 36 398 -3 8 -7 46 -24 86 -38 40 -13 75 -28 78 -33 3 -4 27 -18
|
||||
53 -30 26 -12 52 -30 57 -41 6 -11 27 -25 46 -31 19 -6 54 -29 78 -51 23 -21
|
||||
46 -39 51 -39 4 0 17 -11 28 -25 11 -14 28 -25 38 -25 10 0 25 -8 32 -18 7 -9
|
||||
26 -25 41 -35 16 -10 36 -26 45 -35 27 -27 79 -62 92 -62 7 0 21 -11 32 -25
|
||||
11 -14 26 -25 33 -25 8 0 22 -11 31 -25 9 -14 24 -25 33 -25 9 0 25 -7 35 -15
|
||||
11 -8 28 -15 38 -15 9 0 25 -8 35 -19 10 -10 36 -22 57 -25 22 -4 49 -14 60
|
||||
-22 11 -8 37 -14 58 -14 21 0 38 -4 38 -10 0 -6 42 -10 105 -10 58 0 105 4
|
||||
105 9 0 4 23 11 50 14 28 3 60 13 72 22 13 8 32 15 45 15 12 0 26 6 30 14 4 7
|
||||
26 19 48 26 49 16 79 35 125 77 19 18 40 33 46 33 7 0 14 6 17 13 3 8 21 22
|
||||
41 31 20 9 36 21 36 27 0 14 50 59 66 59 8 0 14 7 14 15 0 8 11 17 25 21 15 4
|
||||
28 16 31 28 3 12 19 30 35 40 16 10 37 28 45 42 9 13 24 24 33 24 9 0 25 13
|
||||
34 30 10 16 31 37 47 47 17 9 30 23 30 29 0 6 9 14 20 17 19 5 20 14 20 177
|
||||
l0 172 -22 -6 c-13 -3 -26 -6 -30 -6 -5 0 -8 -7 -8 -15 0 -8 -16 -21 -36 -30
|
||||
-20 -8 -38 -24 -41 -35 -3 -11 -9 -20 -12 -20 -4 0 -30 -20 -57 -45 -27 -25
|
||||
-54 -45 -61 -45 -7 0 -13 -5 -13 -11 0 -14 -25 -39 -40 -39 -6 0 -16 -11 -22
|
||||
-25 -6 -14 -16 -25 -21 -25 -6 0 -17 -7 -26 -15 -44 -39 -60 -53 -96 -80 -22
|
||||
-17 -46 -38 -53 -47 -7 -10 -19 -18 -26 -18 -7 0 -23 -10 -36 -22 -14 -13 -45
|
||||
-34 -70 -47 -25 -13 -49 -30 -53 -37 -4 -8 -18 -14 -30 -14 -13 0 -28 -5 -34
|
||||
-11 -6 -6 -44 -15 -84 -20 -41 -5 -75 -14 -77 -19 -5 -13 -89 -13 -94 0 -1 5
|
||||
-40 15 -85 21 -46 7 -83 16 -83 21 0 4 -11 8 -25 8 -14 0 -33 9 -42 20 -10 11
|
||||
-23 20 -29 20 -6 0 -23 9 -37 20 -14 11 -30 20 -36 20 -6 0 -20 9 -31 20 -11
|
||||
11 -27 20 -36 20 -18 0 -44 23 -44 39 0 6 -9 11 -20 11 -11 0 -20 4 -20 8 0 5
|
||||
-12 15 -27 23 -16 8 -34 22 -41 32 -7 9 -22 17 -33 17 -11 0 -22 7 -25 15 -4
|
||||
8 -12 15 -20 15 -8 0 -14 7 -14 15 0 8 -7 15 -15 15 -8 0 -27 16 -42 35 -15
|
||||
19 -36 35 -46 35 -19 0 -34 10 -75 51 -15 15 -37 30 -49 34 -13 3 -23 12 -23
|
||||
18 0 7 -28 23 -61 37 -34 14 -63 30 -66 37 -3 6 -17 15 -33 18 -16 3 -34 12
|
||||
-40 20 -6 8 -26 17 -43 21 -18 3 -35 11 -39 17 -14 22 -286 33 -447 18z"/>
|
||||
<path d="M5050 4782 c0 -4 -17 -16 -37 -27 -20 -11 -49 -31 -63 -46 -15 -14
|
||||
-31 -24 -37 -22 -5 2 -21 -7 -35 -19 -30 -27 -76 -57 -100 -65 -10 -3 -18 -14
|
||||
-18 -24 0 -10 -11 -26 -25 -36 -14 -10 -39 -30 -57 -45 -17 -15 -39 -30 -49
|
||||
-33 -9 -3 -24 -17 -33 -30 -9 -14 -23 -25 -31 -25 -7 0 -20 -8 -27 -18 -7 -9
|
||||
-26 -25 -41 -35 -16 -10 -36 -26 -46 -35 -26 -26 -79 -62 -89 -62 -6 0 -17
|
||||
-11 -26 -25 -9 -14 -22 -25 -29 -25 -11 0 -54 -34 -117 -92 -25 -23 -45 -37
|
||||
-87 -59 -13 -7 -23 -15 -23 -19 0 -4 -19 -20 -42 -36 -24 -16 -49 -37 -56 -46
|
||||
-7 -10 -19 -18 -25 -18 -7 0 -23 -10 -37 -23 -14 -12 -43 -36 -65 -52 -22 -17
|
||||
-51 -40 -65 -52 -14 -13 -30 -23 -37 -23 -7 0 -17 -9 -23 -21 -6 -11 -20 -23
|
||||
-30 -27 -19 -7 -71 -47 -122 -94 -16 -16 -31 -28 -33 -28 -7 0 -73 -47 -91
|
||||
-64 -9 -10 -29 -25 -43 -34 -14 -9 -32 -25 -39 -34 -7 -10 -19 -18 -25 -18 -7
|
||||
0 -34 -20 -62 -44 -27 -25 -51 -41 -53 -35 -2 5 -17 9 -33 9 -16 0 -29 5 -29
|
||||
10 0 6 -7 10 -16 10 -9 0 -22 6 -28 13 -6 7 -38 24 -71 37 -33 13 -66 32 -73
|
||||
42 -7 10 -21 18 -31 18 -10 0 -29 13 -42 29 -13 15 -35 32 -49 36 -23 7 -45
|
||||
24 -88 68 -9 9 -21 17 -27 17 -5 0 -21 12 -34 25 -13 14 -29 25 -36 25 -7 0
|
||||
-29 15 -48 33 -95 88 -137 117 -167 117 -11 0 -20 6 -20 14 0 7 -16 21 -35 30
|
||||
-19 9 -40 25 -47 34 -7 10 -39 26 -72 36 -32 10 -67 27 -77 37 -11 11 -27 19
|
||||
-37 19 -9 0 -49 9 -87 20 -101 29 -261 28 -380 -2 -49 -13 -95 -28 -101 -33
|
||||
-6 -6 -31 -17 -55 -24 -24 -7 -52 -17 -61 -22 -10 -5 -23 -9 -29 -9 -7 0 -22
|
||||
-11 -35 -25 -13 -14 -30 -25 -37 -25 -8 0 -25 -11 -38 -25 -13 -13 -32 -25
|
||||
-41 -25 -9 0 -20 -4 -23 -10 -3 -5 -14 -10 -23 -10 -10 0 -26 -11 -36 -25 -10
|
||||
-14 -27 -25 -37 -25 -10 0 -28 -13 -41 -28 -13 -16 -43 -37 -68 -46 -24 -10
|
||||
-55 -28 -69 -42 -13 -13 -30 -24 -37 -24 -8 0 -14 -4 -14 -10 0 -5 -9 -10 -20
|
||||
-10 -11 0 -20 -7 -20 -15 0 -8 -7 -15 -15 -15 -8 0 -15 -4 -15 -10 0 -5 -13
|
||||
-10 -28 -10 -16 0 -37 -7 -48 -15 -10 -8 -25 -15 -32 -15 -7 0 -22 -9 -34 -20
|
||||
-12 -11 -32 -20 -45 -20 l-23 0 0 -170 0 -170 29 0 c34 0 92 19 106 35 6 7 26
|
||||
18 45 26 19 7 45 24 58 36 13 13 31 23 40 23 17 0 82 35 92 50 3 4 27 20 55
|
||||
36 27 16 58 37 67 46 10 10 26 18 37 18 10 0 30 11 45 25 15 14 31 25 37 25 5
|
||||
0 9 4 9 10 0 5 11 12 25 16 14 3 30 14 35 24 5 10 23 21 40 25 16 4 30 10 30
|
||||
15 0 5 14 11 30 15 17 4 30 10 30 14 0 3 29 20 65 36 36 16 65 32 65 36 0 4
|
||||
15 9 33 12 17 3 55 10 82 16 125 25 374 3 414 -36 7 -8 27 -17 43 -20 16 -3
|
||||
37 -16 47 -29 10 -13 25 -24 32 -24 8 0 23 -7 33 -15 11 -8 28 -15 39 -15 10
|
||||
0 21 -9 24 -20 3 -11 16 -23 29 -26 13 -3 28 -12 35 -20 6 -8 19 -14 29 -14 9
|
||||
0 22 -11 28 -25 6 -14 20 -25 29 -25 10 0 31 -13 46 -30 16 -16 35 -30 42 -30
|
||||
8 0 24 -13 37 -28 13 -16 44 -39 71 -51 26 -13 47 -29 47 -36 0 -7 16 -20 36
|
||||
-30 19 -9 38 -23 40 -30 3 -7 23 -20 44 -29 53 -21 59 -35 24 -62 -16 -13 -38
|
||||
-32 -49 -42 -11 -11 -37 -29 -57 -41 -20 -12 -39 -28 -42 -36 -3 -8 -12 -15
|
||||
-19 -15 -8 0 -22 -9 -32 -20 -10 -11 -24 -20 -32 -20 -7 0 -13 -4 -13 -10 0
|
||||
-5 -12 -17 -26 -26 -23 -16 -28 -16 -60 -1 -19 9 -34 21 -34 27 0 5 -7 10 -15
|
||||
10 -8 0 -19 9 -25 20 -6 11 -20 20 -31 20 -11 0 -22 7 -26 16 -3 8 -31 27 -61
|
||||
41 -31 14 -62 32 -70 39 -7 8 -25 17 -40 21 -15 3 -37 17 -49 30 -12 12 -31
|
||||
23 -41 23 -11 0 -39 7 -62 15 -61 21 -399 21 -460 0 -23 -8 -51 -15 -61 -15
|
||||
-10 0 -29 -10 -42 -22 -14 -13 -44 -29 -68 -36 -23 -7 -48 -19 -53 -26 -6 -7
|
||||
-32 -21 -59 -30 -26 -10 -47 -24 -47 -32 0 -8 -5 -14 -12 -14 -6 0 -26 -11
|
||||
-44 -25 -18 -14 -39 -25 -47 -25 -8 0 -17 -7 -21 -15 -3 -8 -13 -15 -23 -15
|
||||
-22 -1 -53 -23 -53 -39 0 -6 -7 -11 -16 -11 -9 0 -27 -9 -41 -20 -14 -11 -31
|
||||
-20 -38 -20 -7 0 -15 -9 -18 -20 -3 -13 -14 -20 -29 -20 -12 0 -34 -9 -48 -20
|
||||
-14 -11 -31 -20 -37 -20 -7 0 -13 -5 -13 -10 0 -6 -12 -15 -28 -21 -45 -18
|
||||
-97 -43 -102 -50 -3 -4 -21 -11 -40 -16 l-35 -9 -3 -172 c-2 -129 1 -172 10
|
||||
-172 24 0 83 26 105 46 13 12 40 24 61 28 21 3 49 15 62 26 13 12 43 29 67 40
|
||||
24 11 43 26 43 34 0 9 8 16 18 16 9 0 26 7 36 15 11 8 28 15 38 15 9 0 22 9
|
||||
28 20 6 11 16 20 23 20 7 0 24 11 39 25 15 14 31 25 36 25 5 0 15 7 22 15 7 8
|
||||
21 15 32 15 10 0 29 11 42 25 13 14 30 25 38 25 8 0 23 9 33 20 10 11 22 20
|
||||
27 20 5 0 18 8 29 19 11 10 38 21 60 25 21 3 39 11 39 16 0 6 11 10 25 10 14
|
||||
0 25 4 25 9 0 19 183 61 263 61 89 0 257 -41 257 -62 0 -5 6 -8 13 -8 26 0
|
||||
130 -47 139 -64 5 -9 17 -16 27 -16 10 0 24 -6 30 -14 10 -11 8 -16 -9 -21
|
||||
-11 -4 -31 -15 -43 -26 -12 -11 -38 -33 -57 -50 -19 -17 -47 -37 -61 -45 -15
|
||||
-7 -29 -20 -32 -29 -4 -8 -13 -15 -20 -15 -8 0 -22 -9 -32 -20 -10 -11 -22
|
||||
-20 -26 -20 -4 0 -19 -10 -32 -22 -46 -44 -109 -93 -128 -100 -11 -4 -22 -14
|
||||
-25 -23 -4 -8 -11 -15 -18 -15 -6 0 -24 -12 -41 -28 -39 -35 -102 -82 -110
|
||||
-82 -4 0 -16 -11 -27 -25 -11 -14 -26 -25 -33 -25 -8 0 -20 -8 -27 -18 -7 -11
|
||||
-32 -31 -56 -46 -23 -14 -42 -30 -42 -35 0 -4 -10 -13 -23 -20 -53 -28 -57
|
||||
-31 -155 -118 -20 -18 -43 -33 -49 -33 -7 0 -18 -9 -25 -20 -7 -11 -17 -20
|
||||
-21 -20 -9 0 -43 -25 -104 -77 -21 -18 -43 -33 -49 -33 -7 0 -19 -11 -28 -25
|
||||
-9 -14 -23 -25 -32 -25 -8 0 -17 -6 -20 -14 -3 -8 -12 -12 -20 -9 -8 3 -14 1
|
||||
-14 -5 0 -8 -52 -46 -83 -60 -5 -2 -19 -14 -32 -26 -35 -31 -90 -76 -106 -88
|
||||
-11 -7 -11 -11 -1 -15 6 -2 12 -11 12 -19 0 -8 5 -14 10 -14 6 0 10 -9 10 -20
|
||||
0 -11 5 -20 10 -20 6 0 10 -7 10 -15 0 -8 5 -15 11 -15 16 0 39 -26 39 -45 0
|
||||
-16 22 -45 35 -45 13 0 35 -29 35 -45 0 -19 23 -45 39 -45 6 0 11 -6 11 -13 0
|
||||
-7 9 -19 20 -27 11 -8 20 -25 20 -37 0 -13 5 -23 10 -23 6 0 22 -11 35 -25 14
|
||||
-13 25 -29 25 -35 0 -5 5 -10 11 -10 11 0 39 -25 39 -35 0 -8 27 -35 35 -35
|
||||
10 0 35 -28 35 -39 0 -6 5 -11 10 -11 6 0 10 -9 10 -20 0 -11 7 -20 15 -20 8
|
||||
0 15 -5 15 -11 0 -11 25 -39 35 -39 10 0 35 -28 35 -39 0 -6 9 -11 20 -11 22
|
||||
0 70 -41 70 -60 0 -5 5 -10 10 -10 6 0 22 -11 35 -25 14 -13 25 -29 25 -35 0
|
||||
-5 5 -10 10 -10 14 0 60 -46 60 -60 0 -5 7 -10 15 -10 8 0 15 -4 15 -10 0 -5
|
||||
9 -10 20 -10 11 0 20 -4 20 -10 0 -5 7 -10 15 -10 8 0 15 -5 15 -10 0 -14 46
|
||||
-60 60 -60 5 0 10 -7 10 -15 0 -8 9 -15 20 -15 11 0 20 -4 20 -10 0 -5 7 -10
|
||||
15 -10 8 0 15 -5 15 -11 0 -16 26 -39 45 -39 19 0 45 -23 45 -39 0 -6 7 -11
|
||||
15 -11 8 0 15 -4 15 -10 0 -5 9 -10 20 -10 11 0 20 -4 20 -10 0 -5 7 -10 15
|
||||
-10 8 0 15 -7 15 -15 0 -8 6 -15 13 -15 7 0 19 -9 27 -20 8 -11 25 -20 37 -20
|
||||
13 0 23 -5 23 -11 0 -17 27 -39 49 -39 12 0 21 -4 21 -10 0 -5 11 -10 25 -10
|
||||
16 0 25 -6 25 -15 0 -9 9 -15 25 -15 14 0 25 -4 25 -10 0 -5 9 -10 20 -10 11
|
||||
0 20 -4 20 -10 0 -5 7 -10 15 -10 8 0 15 -6 15 -14 0 -19 65 -29 124 -21 29 5
|
||||
46 12 46 21 0 8 7 14 15 14 8 0 15 5 15 10 0 6 9 10 20 10 11 0 20 5 20 10 0
|
||||
6 11 10 25 10 14 0 25 5 25 11 0 16 26 39 45 39 19 0 45 23 45 39 0 6 10 11
|
||||
23 11 12 0 29 9 37 20 8 11 25 20 37 20 13 0 23 5 23 11 0 16 26 39 45 39 19
|
||||
0 45 23 45 39 0 6 7 11 15 11 8 0 15 5 15 10 0 6 9 10 20 10 11 0 20 5 20 10
|
||||
0 6 7 10 15 10 8 0 15 6 15 13 0 7 9 19 20 27 11 8 20 20 20 27 0 7 11 13 25
|
||||
13 14 0 25 5 25 10 0 6 5 10 11 10 11 0 39 25 39 35 0 8 27 35 35 35 10 0 35
|
||||
28 35 39 0 6 9 11 20 11 22 0 70 41 70 60 0 5 5 10 10 10 14 0 60 46 60 60 0
|
||||
5 5 10 10 10 14 0 60 46 60 60 0 5 5 10 10 10 14 0 60 46 60 60 0 5 5 10 10
|
||||
10 14 0 60 46 60 60 0 5 5 10 10 10 19 0 60 48 60 70 0 11 7 20 15 20 8 0 15
|
||||
6 15 13 0 18 34 57 49 57 6 0 11 6 11 13 0 7 9 19 20 27 11 8 20 25 20 37 0
|
||||
13 5 23 10 23 14 0 60 46 60 60 0 5 7 10 15 10 8 0 15 9 15 20 0 11 5 20 10
|
||||
20 6 0 10 5 10 11 0 14 25 39 39 39 6 0 11 9 11 21 0 19 21 49 35 49 13 0 35
|
||||
29 35 45 0 19 23 45 39 45 6 0 11 10 11 23 0 12 9 29 20 37 11 8 20 25 20 37
|
||||
0 13 5 23 11 23 17 0 39 27 39 49 0 14 8 23 25 27 18 4 25 13 25 30 0 13 5 24
|
||||
10 24 6 0 10 9 10 21 0 22 22 49 39 49 6 0 11 11 11 25 0 14 5 25 10 25 6 0
|
||||
10 9 10 20 0 11 5 20 10 20 6 0 10 11 10 25 0 16 6 25 15 25 9 0 15 9 15 23 0
|
||||
12 9 29 20 37 11 8 20 25 20 37 0 14 6 23 15 23 9 0 15 9 15 25 0 14 5 25 10
|
||||
25 6 0 10 9 10 20 0 11 5 20 10 20 6 0 10 11 10 25 0 16 6 25 15 25 10 0 15
|
||||
11 15 35 0 19 5 35 10 35 6 0 10 11 10 25 0 14 5 25 10 25 6 0 10 9 10 20 0
|
||||
11 7 20 15 20 9 0 15 9 15 25 0 14 5 25 10 25 6 0 10 16 10 35 0 19 5 35 10
|
||||
35 6 0 10 11 10 25 0 16 6 25 15 25 9 0 15 9 15 25 0 14 5 25 10 25 6 0 10 16
|
||||
10 35 0 24 5 35 15 35 10 0 15 11 15 35 0 19 5 35 10 35 6 0 10 16 10 35 0 19
|
||||
5 35 10 35 6 0 10 9 10 20 0 11 7 20 15 20 11 0 15 12 15 50 0 28 5 50 10 50
|
||||
6 0 10 16 10 35 0 19 5 35 10 35 6 0 10 16 10 35 0 24 5 35 15 35 11 0 15 12
|
||||
15 45 0 25 5 45 10 45 6 0 10 16 10 35 0 19 5 35 10 35 6 0 10 23 10 50 0 38
|
||||
4 50 15 50 11 0 15 12 15 50 0 28 5 50 10 50 6 0 10 23 10 50 0 28 5 50 10 50
|
||||
6 0 10 25 10 55 0 42 3 55 15 55 12 0 15 14 15 70 0 40 4 70 10 70 6 0 10 35
|
||||
10 85 0 50 4 85 10 85 6 0 10 38 10 95 0 78 3 95 15 95 13 0 15 23 15 150 0
|
||||
93 4 150 10 150 6 0 10 58 10 155 0 131 -2 155 -15 155 -8 0 -15 -3 -15 -8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
19
public/icons/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Cairo",
|
||||
"short_name": "Cairo",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/cairo/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/cairo/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#333333",
|
||||
"background_color": "#333333",
|
||||
"display": "standalone"
|
||||
}
|
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
27
src/App.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { ReactNode } from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import useInitHooks from "@src/hooks/init-hooks";
|
||||
import theme from "@src/util/theme";
|
||||
import { AuthProvider } from "@src/ctx/AuthContext";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import Viewport from "./Viewport";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<InitProvider>
|
||||
<Viewport />
|
||||
</InitProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</ChakraProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function InitProvider(props: { children: ReactNode }) {
|
||||
useInitHooks();
|
||||
return props.children;
|
||||
}
|
59
src/Portal.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { lazy, LazyExoticComponent, ReactNode, Suspense } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { CenteredLoadingSpinner } from "./components/common/Loading";
|
||||
import { CenteredErrorFallback } from "./components/common/Fallback";
|
||||
import { Links, rootLink } from "./util/links";
|
||||
|
||||
// Lazy Views
|
||||
const ProjectView = lazy(() => import("@src/views/ProjectView"));
|
||||
|
||||
// Static Views
|
||||
import AutoRedirect from "./views/AutoRedirect";
|
||||
import AuthenticateView from "./views/AuthenticateView";
|
||||
import { useAuth } from "./ctx/AuthContext";
|
||||
import AuthorizedView from "./views/AuthorizedView";
|
||||
|
||||
declare type Portal = { path: Links; view: ReactNode };
|
||||
|
||||
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
|
||||
const { auth } = useAuth();
|
||||
const Component = props.view;
|
||||
if (!!auth) return <Component />;
|
||||
return <AutoRedirect />;
|
||||
}
|
||||
|
||||
const lazyPortals: Portal[] = [{ path: Links.ProjectView, view: <Auth view={ProjectView} /> }];
|
||||
|
||||
// Raw Routes
|
||||
const rawPortals: Portal[] = [
|
||||
{ path: Links.Authenticate, view: <AuthenticateView /> },
|
||||
{ path: Links.Authorized, view: <Auth view={AuthorizedView} /> },
|
||||
];
|
||||
|
||||
export default function Portal() {
|
||||
return (
|
||||
<Routes>
|
||||
{lazyPortals.map((p: Portal, i) => (
|
||||
<Route
|
||||
key={i}
|
||||
path={rootLink(p.path)}
|
||||
element={
|
||||
<Suspense fallback={<CenteredLoadingSpinner />}>
|
||||
<ErrorBoundary fallback={<CenteredErrorFallback />} key={p.path}>
|
||||
<Suspense fallback={<CenteredLoadingSpinner />} key={p.path}>
|
||||
{p.view}
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{rawPortals.map((p: Portal, i) => (
|
||||
<Route key={i} path={rootLink(p.path)} element={p.view} />
|
||||
))}
|
||||
<Route path={"*"} element={<AutoRedirect />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
17
src/Viewport.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Box } from "@chakra-ui/react";
|
||||
import MainMenu, { useMainMenu } from "./components/MainMenu";
|
||||
import Portal from "./Portal";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
export default function Viewport() {
|
||||
const [minified, toggleMinified, drawerWidth] = useMainMenu();
|
||||
return (
|
||||
<Box>
|
||||
<MainMenu minified={minified} toggleMinified={toggleMinified} width={drawerWidth} />
|
||||
<Box width={`calc(100% - ${drawerWidth})`} marginLeft={drawerWidth}>
|
||||
<Portal />
|
||||
</Box>
|
||||
<ToastContainer />
|
||||
</Box>
|
||||
);
|
||||
}
|
34
src/components/MainHeader.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, Box, Flex, useBreakpointValue } from "@chakra-ui/react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
interface LinkChunk {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export default function MainHeader() {
|
||||
const { sectionId, moduleId } = useParams();
|
||||
const linkChunks: LinkChunk[] = [{ name: "Hi", link: "https://dunemask.net" }];
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
position="fixed"
|
||||
top="0"
|
||||
shadow="md" // Optional: Add shadow for better separation from other content
|
||||
>
|
||||
<Flex p={4} width="100%">
|
||||
<Breadcrumb separator="›">
|
||||
{linkChunks.map((lc, i) => (
|
||||
<BreadcrumbItem key={i}>
|
||||
<BreadcrumbLink as={Link} to={lc.link}>
|
||||
{lc.name}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
102
src/components/MainMenu.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Box, Flex, IconButton, Text, List, ListItem, ListIcon, Tooltip, Divider, Icon, Slide } from "@chakra-ui/react";
|
||||
import { Links } from "../util/links";
|
||||
import { GuardedContent } from "../util/guards";
|
||||
import { MdSettings, MdMenu, MdLogout, MdLock } from "react-icons/md";
|
||||
import AppGuard from "@lib/Guards";
|
||||
import { useAuth } from "@src/ctx/AuthContext";
|
||||
import { Policy } from "@lib/Policies";
|
||||
|
||||
const layouts = [
|
||||
{
|
||||
name: "Settings",
|
||||
link: Links.Settings,
|
||||
icon: MdSettings,
|
||||
guards: [...AppGuard.IAMAuthenticated],
|
||||
},
|
||||
];
|
||||
|
||||
export const drawerWidth = "230px";
|
||||
export const miniDrawerWidth = "5rem";
|
||||
|
||||
export function useMainMenu(defaultState: boolean = false): [boolean, () => void, string] {
|
||||
const [minified, setMinified] = useState<boolean>(defaultState);
|
||||
const toggleMinified = () => setMinified(!minified);
|
||||
const width = minified ? miniDrawerWidth : drawerWidth;
|
||||
return [minified, toggleMinified, width];
|
||||
}
|
||||
|
||||
export default function MainMenu(props: { minified: boolean; toggleMinified: () => void; width: string }) {
|
||||
const { logout, activePolicies, auth } = useAuth();
|
||||
const menus = layouts.filter(({ guards: g }) => g.length === 0 || Policy.multiAuthorizedTo(activePolicies, g));
|
||||
const menuButtonSx = { height: "3rem", _hover: { bg: "rgba(0,0,0,.1)" }, w: "100%" };
|
||||
|
||||
const slideMotion = { animate: { width: props.width }, initial: { width: props.width } };
|
||||
|
||||
return (
|
||||
<Box position="fixed" zIndex={1}>
|
||||
<Slide direction="left" in={props.minified} motionProps={slideMotion}>
|
||||
<Box w={props.width} maxW={props.width} bg="background.paper" h="100%">
|
||||
<Box p={4} height="100%" display="flex" flexDirection="column">
|
||||
<Flex mb={4} h="48px">
|
||||
<IconButton
|
||||
icon={<Icon as={MdMenu} />}
|
||||
onClick={props.toggleMinified}
|
||||
aria-label="Toggle menu"
|
||||
w="48px"
|
||||
h="100%"
|
||||
p="0"
|
||||
/>
|
||||
<GuardedContent guard={!props.minified}>
|
||||
<Text ml="1rem" lineHeight="48px">
|
||||
Cairo
|
||||
</Text>
|
||||
<img
|
||||
src={`${import.meta.env.BASE_URL}icons/favicon.ico`}
|
||||
alt=""
|
||||
style={{ height: "1.5rem", marginLeft: ".75rem", marginTop: "auto", marginBottom: "auto" }}
|
||||
/>
|
||||
</GuardedContent>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<List spacing={3} mt={2}>
|
||||
{menus.map((menuItem, index) => (
|
||||
<ListItem key={index} w="100%">
|
||||
<Tooltip label={menuItem.name} placement="right" isDisabled={props.minified}>
|
||||
<Flex as={Link} to={menuItem.link} align="center" px={4} py={2} borderRadius="md" {...menuButtonSx}>
|
||||
<ListIcon as={menuItem.icon} />
|
||||
<GuardedContent guard={!props.minified}>
|
||||
<Text ml={2}>{menuItem.name}</Text>
|
||||
</GuardedContent>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</ListItem>
|
||||
))}
|
||||
<GuardedContent guard={auth}>
|
||||
<ListItem w="100%">
|
||||
<Flex as="button" onClick={logout} align="center" px={4} py={2} borderRadius="md" {...menuButtonSx}>
|
||||
<ListIcon as={MdLogout} />
|
||||
<GuardedContent guard={!props.minified}>
|
||||
<Text ml={2}>Logout</Text>
|
||||
</GuardedContent>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
</GuardedContent>
|
||||
<GuardedContent guard={!auth}>
|
||||
<ListItem w="100%">
|
||||
<Flex as={Link} to={Links.Authenticate} align="center" px={4} py={2} borderRadius="md" {...menuButtonSx}>
|
||||
<ListIcon as={MdLock} />
|
||||
<GuardedContent guard={!props.minified}>
|
||||
<Text ml={2}>Login</Text>
|
||||
</GuardedContent>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
</GuardedContent>
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</Slide>
|
||||
</Box>
|
||||
);
|
||||
}
|
13
src/components/common/Fallback.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Flex, FlexProps, TextProps, Text } from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
|
||||
export function CenteredErrorFallback(props: { TextProps?: TextProps; FlexProps?: FlexProps }) {
|
||||
return (
|
||||
<Flex flexDir="column" alignItems="center" justifyContent="center" p="20px" h="100vh" {...props.FlexProps}>
|
||||
<Text {...props.TextProps}>
|
||||
Oops! Looks like something broke!
|
||||
<br /> Please try again later!
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
34
src/components/common/Inputs.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Icon, IconButton, IconButtonProps, Input, InputGroup, InputProps, InputRightElement } from "@chakra-ui/react";
|
||||
import { SyntheticEvent, useEffect, useState } from "react";
|
||||
import { FaEye, FaEyeSlash } from "react-icons/fa";
|
||||
|
||||
interface PasswordInputProps extends InputProps {
|
||||
value: string;
|
||||
setPassword: (password: string) => void;
|
||||
InputProps?: InputProps;
|
||||
IconButtonProps?: IconButtonProps;
|
||||
}
|
||||
|
||||
export function PasswordInput(props: PasswordInputProps) {
|
||||
const { setPassword, value } = props;
|
||||
const [visible, setVisible] = useState<boolean>();
|
||||
const toggleVisible = () => setVisible(!visible);
|
||||
|
||||
const inputType = visible ? "text" : "password";
|
||||
const inputIcon = visible ? FaEyeSlash : FaEye;
|
||||
const passwordChange = (e: SyntheticEvent) => setPassword((e.target as HTMLInputElement).value);
|
||||
const iconButtonProps: IconButtonProps = { ...props.IconButtonProps, "aria-label": "Show Password" };
|
||||
|
||||
const combinedInputProps: InputProps = { ...props.InputProps, value, onChange: passwordChange };
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<Input placeholder="Password" type={inputType} onChange={passwordChange} {...combinedInputProps} />
|
||||
<InputRightElement>
|
||||
<IconButton _hover={{ bg: "inherit", opacity: ".8" }} onClick={toggleVisible} {...iconButtonProps}>
|
||||
<Icon as={inputIcon} />
|
||||
</IconButton>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
12
src/components/common/Loading.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Flex, FlexProps, Spinner, SpinnerProps } from "@chakra-ui/react";
|
||||
export function LoadingSpinner(props: SpinnerProps) {
|
||||
return <Spinner height="300px" width="300px" {...props} />;
|
||||
}
|
||||
|
||||
export function CenteredLoadingSpinner(props: { SpinnerProps?: SpinnerProps; FlexProps?: FlexProps }) {
|
||||
return (
|
||||
<Flex flexDir="column" alignItems="center" justifyContent="center" p="20px" h="100vh" {...props.FlexProps}>
|
||||
<Spinner height="250px" width="250px" mb="-40px" {...props.SpinnerProps} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
220
src/ctx/AuthContext.tsx
Normal file
|
@ -0,0 +1,220 @@
|
|||
import { ReactNode, createContext, useContext, useMemo, useReducer } from "react";
|
||||
import { apiRequest } from "@dunemask/vix/bridge";
|
||||
import { Policy, PolicyString } from "@lib/Policies";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||
|
||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
||||
const credentialApiPath = `/${project}/auth/credentials`;
|
||||
|
||||
export enum AuthStorageKeys {
|
||||
USER = "user",
|
||||
USER_TOKEN = "user-token",
|
||||
SPOOF_USER = "spoof-user",
|
||||
SPOOF_TOKEN = "spoof-token",
|
||||
}
|
||||
|
||||
export interface CredentialDTO {
|
||||
user: CDatabaseContract["User"];
|
||||
policies: PolicyString[];
|
||||
}
|
||||
|
||||
export interface LoginCredentialDTO extends CredentialDTO {
|
||||
token: string;
|
||||
}
|
||||
|
||||
type LoginAction = { type: AuthAction.LOGIN; token: string; user: any; policies: Policy[] };
|
||||
type LogoutAction = { type: AuthAction.LOGOUT };
|
||||
type SpoofAction = { type: AuthAction.SPOOF; token: string; user: any; policies: Policy[] };
|
||||
type UnspoofAction = { type: AuthAction.UNSPOOF };
|
||||
type LoadingAction = { type: AuthAction.LOADING; loading: boolean };
|
||||
type InitializeAction = { type: AuthAction.INTIALIZE; initialized: boolean };
|
||||
type Action = LoginAction | LogoutAction | SpoofAction | UnspoofAction | LoadingAction | InitializeAction;
|
||||
|
||||
enum AuthAction {
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
SPOOF,
|
||||
UNSPOOF,
|
||||
LOADING,
|
||||
INTIALIZE,
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user?: CDatabaseContract["User"];
|
||||
auth: boolean;
|
||||
token?: string;
|
||||
userPolicies: Policy[];
|
||||
spoofUser?: CDatabaseContract["User"];
|
||||
spoofToken?: string;
|
||||
spoofPolicies: Policy[];
|
||||
activeUser?: CDatabaseContract["User"];
|
||||
activeToken?: string;
|
||||
activePolicies: Policy[];
|
||||
loading: boolean;
|
||||
initialized: false;
|
||||
}
|
||||
|
||||
interface AuthContextType extends AuthState {
|
||||
logout: () => void;
|
||||
login: (userData: CDatabaseContract["User"], token: string, policies?: Policy[]) => Promise<void>;
|
||||
spoof: (userData: CDatabaseContract["User"], token: string, policies?: Policy[]) => Promise<void>;
|
||||
unspoof: () => void;
|
||||
authInit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const initialState: AuthContextType = {
|
||||
auth: false,
|
||||
userPolicies: [],
|
||||
spoofPolicies: [],
|
||||
activePolicies: [],
|
||||
logout: () => {},
|
||||
login: async () => {},
|
||||
spoof: async () => {},
|
||||
unspoof: () => {},
|
||||
authInit: async () => {},
|
||||
loading: false,
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType>(initialState);
|
||||
|
||||
export const useAuth = () => useContext<AuthContextType>(AuthContext);
|
||||
|
||||
function onLogout(state: AuthState): AuthState {
|
||||
window.localStorage.removeItem(AuthStorageKeys.USER);
|
||||
window.localStorage.removeItem(AuthStorageKeys.USER_TOKEN);
|
||||
window.localStorage.removeItem(AuthStorageKeys.SPOOF_USER);
|
||||
window.localStorage.removeItem(AuthStorageKeys.SPOOF_TOKEN);
|
||||
return {
|
||||
...state,
|
||||
auth: false,
|
||||
token: undefined,
|
||||
spoofToken: undefined,
|
||||
user: undefined,
|
||||
spoofUser: undefined,
|
||||
activeUser: undefined,
|
||||
activeToken: undefined,
|
||||
activePolicies: [],
|
||||
};
|
||||
}
|
||||
|
||||
function onLogin(state: AuthState, action: LoginAction): AuthState {
|
||||
const { token, user, policies: userPolicies } = action;
|
||||
window.localStorage.setItem(AuthStorageKeys.USER, JSON.stringify(user));
|
||||
window.localStorage.setItem(AuthStorageKeys.USER_TOKEN, token);
|
||||
const [activeToken, activeUser, activePolicies] = [token, user, userPolicies];
|
||||
return { ...state, auth: true, token, user, activeToken, activeUser, activePolicies, userPolicies };
|
||||
}
|
||||
|
||||
function onSpoof(state: AuthState, action: SpoofAction): AuthState {
|
||||
const { token: spoofToken, user: spoofUser, policies: spoofPolicies } = action;
|
||||
window.localStorage.setItem(AuthStorageKeys.SPOOF_USER, JSON.stringify(spoofUser));
|
||||
window.localStorage.setItem(AuthStorageKeys.SPOOF_TOKEN, spoofToken);
|
||||
const [activeToken, activeUser, activePolicies] = [spoofToken, spoofUser, spoofPolicies];
|
||||
return { ...state, auth: true, spoofToken, spoofUser, activeToken, activeUser, activePolicies, spoofPolicies };
|
||||
}
|
||||
|
||||
function onUnspoof(state: AuthState): AuthState {
|
||||
const { token, user } = state;
|
||||
window.localStorage.removeItem(AuthStorageKeys.SPOOF_USER);
|
||||
window.localStorage.removeItem(AuthStorageKeys.SPOOF_TOKEN);
|
||||
const [activeToken, activeUser, activePolicies] = [token, user, state.userPolicies];
|
||||
return {
|
||||
...state,
|
||||
auth: true,
|
||||
spoofToken: undefined,
|
||||
spoofUser: undefined,
|
||||
activeToken,
|
||||
activeUser,
|
||||
spoofPolicies: [],
|
||||
activePolicies,
|
||||
};
|
||||
}
|
||||
|
||||
const onLoading = (state: AuthState, action: LoadingAction) => ({ ...state, loading: action.loading }) as AuthState;
|
||||
|
||||
const onInitialized = (state: AuthState, action: InitializeAction) =>
|
||||
({ ...state, initialized: action.initialized }) as AuthState;
|
||||
|
||||
function authReducer(state: AuthState, action: Action): AuthState {
|
||||
if (action.type === AuthAction.LOGIN) return onLogin(state, action);
|
||||
if (action.type === AuthAction.SPOOF) return onSpoof(state, action);
|
||||
if (action.type === AuthAction.LOGOUT) return onLogout(state);
|
||||
if (action.type === AuthAction.UNSPOOF) return onUnspoof(state);
|
||||
if (action.type === AuthAction.LOADING) return onLoading(state, action);
|
||||
if (action.type === AuthAction.INTIALIZE) return onInitialized(state, action);
|
||||
return state;
|
||||
}
|
||||
|
||||
export async function getPolicies(token: string): Promise<Policy[]> {
|
||||
const extraHeaders = { Authorization: `Bearer ${token}` };
|
||||
const credentials = await apiRequest({ subpath: credentialApiPath, jsonify: true, extraHeaders });
|
||||
if (!credentials) return [];
|
||||
const { policies } = credentials as CredentialDTO;
|
||||
return Policy.parseResourcePolicies<Resource>(policies);
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||
|
||||
async function login(userData: CDatabaseContract["User"], token: string, policies?: Policy[]) {
|
||||
dispatch({ type: AuthAction.LOADING, loading: true });
|
||||
const userPolicies = !!policies ? policies : await getPolicies(token);
|
||||
dispatch({ type: AuthAction.LOGIN, policies: userPolicies, user: userData, token });
|
||||
dispatch({ type: AuthAction.LOADING, loading: false });
|
||||
}
|
||||
|
||||
async function spoof(userData: CDatabaseContract["User"], token: string, policies?: Policy[]) {
|
||||
dispatch({ type: AuthAction.LOADING, loading: true });
|
||||
const userPolicies = !!policies ? policies : await getPolicies(token);
|
||||
dispatch({ type: AuthAction.SPOOF, policies: userPolicies, user: userData, token });
|
||||
dispatch({ type: AuthAction.LOADING, loading: false });
|
||||
}
|
||||
|
||||
const logout = () => dispatch({ type: AuthAction.LOGOUT });
|
||||
const unspoof = () => dispatch({ type: AuthAction.UNSPOOF });
|
||||
|
||||
const credFail = (handler: () => void) => () => (handler(), [undefined, undefined]);
|
||||
|
||||
async function fetchCredentials(token: string): Promise<[user: CDatabaseContract["User"], rp: Policy[]]> {
|
||||
const extraHeaders = getAuthHeader(token);
|
||||
const credentials = await apiRequest({ subpath: credentialApiPath, jsonify: true, extraHeaders });
|
||||
if (!credentials) throw Error("Could not authenticate!");
|
||||
const { user, policies } = credentials as CredentialDTO;
|
||||
if (!user || !policies) throw Error("Could not authenticate!");
|
||||
const rp = Policy.parseResourcePolicies<Resource>(policies);
|
||||
return [user as CDatabaseContract["User"], rp as Policy[]];
|
||||
}
|
||||
|
||||
async function authInit() {
|
||||
const userToken = getUserToken();
|
||||
const spoofToken = getSpoofToken();
|
||||
const [[user, userPolicies], [spoofUser, spoofPolicies]] = await Promise.all([
|
||||
!!userToken ? fetchCredentials(userToken).catch(credFail(logout)) : [],
|
||||
!!spoofToken ? fetchCredentials(spoofToken).catch(credFail(unspoof)) : [],
|
||||
]);
|
||||
|
||||
if (!!user && !!userToken) await login(user, userToken, userPolicies);
|
||||
if (!!spoofUser && !!spoofToken) await spoof(spoofUser, spoofToken, spoofPolicies);
|
||||
dispatch({ type: AuthAction.INTIALIZE, initialized: true });
|
||||
}
|
||||
|
||||
const context: AuthContextType = {
|
||||
...state,
|
||||
logout,
|
||||
login,
|
||||
spoof,
|
||||
unspoof,
|
||||
authInit,
|
||||
};
|
||||
|
||||
const contextValue = useMemo(() => context, [state, dispatch]);
|
||||
|
||||
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const getUserToken = () => window.localStorage.getItem(AuthStorageKeys.USER_TOKEN) ?? undefined;
|
||||
export const getSpoofToken = () => window.localStorage.getItem(AuthStorageKeys.SPOOF_TOKEN) ?? undefined;
|
||||
export const getActiveUserToken = () => getSpoofToken() ?? getUserToken() ?? undefined;
|
||||
export const getAuthHeader = (token?: string) => (!!token ? { Authorization: `Bearer ${token}` } : undefined);
|
9
src/hooks/init-hooks.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { useAuth } from "@src/ctx/AuthContext";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function useInitHooks() {
|
||||
const { authInit } = useAuth();
|
||||
useEffect(function initHooks() {
|
||||
authInit();
|
||||
}, []);
|
||||
}
|
8
src/index.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
const appRoot = document.getElementById("root");
|
||||
if (!appRoot) throw Error("Root not found!");
|
||||
const root = createRoot(appRoot);
|
||||
|
||||
root.render(<App />);
|
22
src/util/api/GeneratedRequests.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* ----- Vix Auto Generated Routes ----- */
|
||||
import { CAuthContract, CProjectContract } from "@vix/ContractTypes";
|
||||
import { apiRequest } from "@dunemask/vix/bridge";
|
||||
import { authenticatedApiRequest } from "./requests";
|
||||
|
||||
export const getProjectAuthVerify = (project: string) =>
|
||||
authenticatedApiRequest({ subpath: `/${project}/auth/verify`, method: "GET", jsonify: true });
|
||||
|
||||
export const postProjectAuthLogin = (project: string, login: CAuthContract["Login"]) =>
|
||||
apiRequest<CAuthContract["LoginCredentials"]>({
|
||||
subpath: `/${project}/auth/login`,
|
||||
method: "POST",
|
||||
json: login,
|
||||
jsonify: true,
|
||||
});
|
||||
|
||||
export const getProjectAuthCredentials = (project: string) =>
|
||||
authenticatedApiRequest<CAuthContract["Credentials"]>({
|
||||
subpath: `/${project}/auth/credentials`,
|
||||
method: "GET",
|
||||
jsonify: true,
|
||||
});
|
9
src/util/api/requests.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { apiRequest, ApiRequestArgs } from "@dunemask/vix/bridge";
|
||||
import { getAuthHeader } from "@src/ctx/AuthContext";
|
||||
|
||||
export async function authenticatedApiRequest<K = any>(apiRequestArgs: ApiRequestArgs): Promise<K> {
|
||||
const extraHeaders = apiRequestArgs.extraHeaders ?? {};
|
||||
const authHeaders = getAuthHeader();
|
||||
apiRequestArgs.extraHeaders = { ...extraHeaders, ...authHeaders };
|
||||
return apiRequest(apiRequestArgs);
|
||||
}
|
39
src/util/guards.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Policy } from "@lib/Policies";
|
||||
import { useAuth } from "@src/ctx/AuthContext";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export enum GUARD_TARGETS {
|
||||
ACTIVE = "active",
|
||||
SPOOF = "spoof",
|
||||
USER = "user",
|
||||
}
|
||||
|
||||
interface GuardedContentProps {
|
||||
guard: Policy[] | boolean;
|
||||
children: ReactNode | ReactNode[];
|
||||
target?: GUARD_TARGETS;
|
||||
}
|
||||
|
||||
export function useGuardTarget(target = GUARD_TARGETS.ACTIVE) {
|
||||
const { activePolicies, userPolicies, spoofPolicies } = useAuth();
|
||||
const policyMapping = {
|
||||
[GUARD_TARGETS.ACTIVE]: activePolicies,
|
||||
[GUARD_TARGETS.SPOOF]: spoofPolicies,
|
||||
[GUARD_TARGETS.USER]: userPolicies,
|
||||
};
|
||||
return policyMapping[target];
|
||||
}
|
||||
|
||||
export default function useContentGuard(required: Policy[], target = GUARD_TARGETS.ACTIVE) {
|
||||
const policies = useGuardTarget(target);
|
||||
return Policy.multiAuthorizedTo(policies, required);
|
||||
}
|
||||
|
||||
export function GuardedContent(props: GuardedContentProps) {
|
||||
const guardTarget = props.target ?? GUARD_TARGETS.ACTIVE;
|
||||
const policies = useGuardTarget(guardTarget);
|
||||
if (props.guard === false) return undefined;
|
||||
if (props.guard === true) return props.children;
|
||||
if (Policy.multiAuthorizedTo(policies, props.guard)) return props.children;
|
||||
return undefined;
|
||||
}
|
26
src/util/links.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export enum StaticLinks {
|
||||
Authenticate = "/authenticate",
|
||||
AutoRedirect = "/auto-redirect",
|
||||
ProjectView = "/projects",
|
||||
Settings = "/settings",
|
||||
Authorized = "/authorized",
|
||||
}
|
||||
|
||||
export enum DynamicLinks {}
|
||||
|
||||
type LinkEnums<T extends Record<string, string>> = T[keyof T];
|
||||
export const Links = { ...StaticLinks, ...DynamicLinks };
|
||||
export type Links = LinkEnums<typeof Links>;
|
||||
export const rootLink = (l: Links) => import.meta.env.BASE_URL + l.substring(1);
|
||||
export function useLinkNav() {
|
||||
const nav = useNavigate();
|
||||
const linkNav = (l: Links) => nav(rootLink(l));
|
||||
return linkNav;
|
||||
}
|
||||
|
||||
export function useAutoRedirect() {
|
||||
const nav = useLinkNav();
|
||||
return () => nav(Links.AutoRedirect);
|
||||
}
|
86
src/util/theme.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { extendTheme, ThemeConfig } from "@chakra-ui/react";
|
||||
|
||||
const themeConfig: ThemeConfig = {
|
||||
initialColorMode: "dark",
|
||||
useSystemColorMode: false,
|
||||
};
|
||||
|
||||
const theme = extendTheme({
|
||||
config: themeConfig,
|
||||
colors: {
|
||||
primary: {
|
||||
"50": "#fef7e8",
|
||||
"100": "#fadda3",
|
||||
"200": "#f5bc4c",
|
||||
"300": "#d99a1d",
|
||||
"400": "#c28a1a",
|
||||
"500": "#f3ac20",
|
||||
"600": "#8a6212",
|
||||
"700": "#6f4f0f",
|
||||
"800": "#5e420c",
|
||||
"900": "#443009",
|
||||
},
|
||||
secondary: "#ffbc03",
|
||||
background: {
|
||||
default: "#101010",
|
||||
paper: "#222222",
|
||||
},
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif",
|
||||
},
|
||||
fontSizes: {
|
||||
"4xl": "4rem",
|
||||
"3xl": "3rem",
|
||||
"2xl": "2rem",
|
||||
xl: "1.5rem",
|
||||
lg: "1.2rem",
|
||||
md: "1rem",
|
||||
},
|
||||
components: {
|
||||
Modal: {
|
||||
baseStyle: {
|
||||
dialog: {
|
||||
borderRadius: "18px",
|
||||
},
|
||||
closeButton: {
|
||||
borderRadius: "full",
|
||||
bg: "primary",
|
||||
_hover: {
|
||||
bg: "secondary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Button: {
|
||||
baseStyle: {
|
||||
textTransform: "none",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
sizes: {
|
||||
md: {
|
||||
borderRadius: "8px",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
solid: (props: { colorMode: string }) => ({
|
||||
bg: props.colorMode === "dark" ? "primary" : "secondary",
|
||||
color: "white",
|
||||
_hover: {
|
||||
bg: props.colorMode === "dark" ? "secondary" : "primary",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: (props: { colorMode: string }) => ({
|
||||
body: {
|
||||
bg: props.colorMode === "dark" ? "#1c1c1c" : "#eee", // Adjust based on colorMode
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
92
src/views/AuthenticateView.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { Box, Button, Heading, Input, Stack, Image, Text } from "@chakra-ui/react";
|
||||
import { Policy } from "@lib/Policies";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
import { SyntheticEvent, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useAuth } from "@src/ctx/AuthContext";
|
||||
import { useAutoRedirect } from "@src/util/links";
|
||||
import { ClientError } from "@dunemask/vix/bridge";
|
||||
import { AuthErrors } from "@lib/vix/ClientErrors";
|
||||
import { PasswordInput } from "@src/components/common/Inputs";
|
||||
import { ResourcePolicyType } from "@dunemask/vix/util";
|
||||
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
|
||||
|
||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
||||
|
||||
export default function AuthenticateView() {
|
||||
const { auth, login } = useAuth();
|
||||
const autoRedirect = useAutoRedirect();
|
||||
const [identity, setIdentity] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
|
||||
|
||||
function submitCredentials() {
|
||||
const loginPromise = postProjectAuthLogin(project, { identity: identity, password }).then(async (creds) => {
|
||||
if (!creds.token) return toast.error("Server didn't provide token!");
|
||||
await login(
|
||||
creds.user,
|
||||
creds.token,
|
||||
Policy.parseResourcePolicies<Resource>(creds.policies as ResourcePolicyType<Resource>[]),
|
||||
);
|
||||
autoRedirect();
|
||||
});
|
||||
toast.promise(loginPromise, {
|
||||
pending: "Logging in",
|
||||
success: "Logged in successfully!",
|
||||
error: {
|
||||
render({ data }) {
|
||||
const clientError = data as ClientError;
|
||||
if (clientError.isError(AuthErrors.UnauthorizedRequest)) return "Incorrect credentials!";
|
||||
console.error(data);
|
||||
return "Error logging in!";
|
||||
},
|
||||
},
|
||||
});
|
||||
setPassword("");
|
||||
}
|
||||
|
||||
function detectEnter(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") submitCredentials();
|
||||
}
|
||||
if (auth) return;
|
||||
return (
|
||||
<Box width="100%" height="90vh" display="flex" alignItems="center" justifyContent="center">
|
||||
<Box p="2rem" width="100%" maxWidth="350px" boxShadow="md" borderRadius="md" bg="background.paper">
|
||||
<Stack spacing="4" align="center" w="100%">
|
||||
<Stack spacing="2" textAlign="center" w="100%">
|
||||
<Heading size="md" display="flex" alignItems="center">
|
||||
Sign in
|
||||
<Image src={`${import.meta.env.BASE_URL}icons/android-chrome-512x512.png`} boxSize="24px" ml="1rem" alt="Logo" />
|
||||
</Heading>
|
||||
<Text fontSize="sm" w="100%" textAlign="left">
|
||||
Please enter your credentials below
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack spacing="4" w="100%">
|
||||
<Input
|
||||
value={identity}
|
||||
placeholder="Identity"
|
||||
onChange={identityChange}
|
||||
isRequired
|
||||
autoFocus
|
||||
width="100%"
|
||||
borderColor="primary"
|
||||
/>
|
||||
<PasswordInput value={password} setPassword={setPassword} isRequired />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing="4" mt="4" justifyContent="space-between" w="100%">
|
||||
<Button
|
||||
onClick={submitCredentials}
|
||||
variant="outline"
|
||||
colorScheme="primary"
|
||||
ml="auto"
|
||||
isDisabled={!identity || !password}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
11
src/views/AuthorizedView.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Flex, Text, Center } from "@chakra-ui/react";
|
||||
|
||||
export default function AuthorizedView() {
|
||||
return (
|
||||
<Flex h="100vh" w="100%">
|
||||
<Center w="100%">
|
||||
<Text>Your account is authorized!</Text>
|
||||
</Center>
|
||||
</Flex>
|
||||
);
|
||||
}
|
35
src/views/AutoRedirect.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Center, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||
import AppGuard from "@lib/Guards";
|
||||
import { useAuth } from "@src/ctx/AuthContext";
|
||||
import useContentGuard, { GuardedContent } from "@src/util/guards";
|
||||
import { Links, rootLink } from "@src/util/links";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
export default function AutoRedirect() {
|
||||
const manageProjects = useContentGuard(AppGuard.ManageProjects);
|
||||
const { auth, initialized, loading } = useAuth();
|
||||
if (!initialized || loading) return <RedirectLoader />;
|
||||
if (!auth) return <Navigate to={rootLink(Links.Authenticate)} />;
|
||||
if (manageProjects) return <Navigate to={rootLink(Links.ProjectView)} />;
|
||||
return <Navigate to={rootLink(Links.Authorized)} />;
|
||||
}
|
||||
|
||||
export function RedirectLoader() {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
setTimeout(() => setVisible(true), 1500);
|
||||
}, []);
|
||||
return (
|
||||
<Center m="auto" h="100vh">
|
||||
<Flex flexWrap="wrap">
|
||||
<GuardedContent guard={visible}>
|
||||
<Spinner size="xl" color="primary" m="auto" />
|
||||
<Text w="100%" textAlign="center" mt="1.5rem">
|
||||
Loading
|
||||
</Text>
|
||||
</GuardedContent>
|
||||
</Flex>
|
||||
</Center>
|
||||
);
|
||||
}
|
11
src/views/ProjectView.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Flex, Text, Center } from "@chakra-ui/react";
|
||||
|
||||
export default function ProjectView() {
|
||||
return (
|
||||
<Flex h="100vh" w="100%">
|
||||
<Center w="100%">
|
||||
<Text>Project Management</Text>
|
||||
</Center>
|
||||
</Flex>
|
||||
);
|
||||
}
|
22
templates/NOTES.txt
Normal file
|
@ -0,0 +1,22 @@
|
|||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cairo.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "cairo.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cairo.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "cairo.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
62
templates/_helpers.tpl
Normal file
|
@ -0,0 +1,62 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "cairo.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "cairo.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "cairo.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "cairo.labels" -}}
|
||||
helm.sh/chart: {{ include "cairo.chart" . }}
|
||||
{{ include "cairo.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "cairo.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "cairo.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "cairo.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "cairo.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
74
templates/deployment.yaml
Normal file
|
@ -0,0 +1,74 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cairo.fullname" . }}
|
||||
labels:
|
||||
{{- include "cairo.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "cairo.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "cairo.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "cairo.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
env:
|
||||
{{- toYaml .Values.containerEnv | nindent 12 }}
|
||||
{{- with .Values.dataClaim }}
|
||||
volumeMounts:
|
||||
- name: cairo-data
|
||||
mountPath: /dunemask/net/cairo/data
|
||||
{{- end}}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.containerPort | default 52000 }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
{{- with .Values.dataClaim }}
|
||||
- name: cairo-data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
28
templates/hpa.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "cairo.fullname" . }}
|
||||
labels:
|
||||
{{- include "cairo.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "cairo.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
61
templates/ingress.yaml
Normal file
|
@ -0,0 +1,61 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "cairo.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "cairo.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
15
templates/service.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cairo.fullname" . }}
|
||||
labels:
|
||||
{{- include "cairo.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "cairo.selectorLabels" . | nindent 4 }}
|
12
templates/serviceaccount.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "cairo.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "cairo.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|