diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 15147794..de3d2ae9 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -9,6 +9,7 @@
Check the NPM packages that require a new publication or release:
- [ ] [manifest](https://www.npmjs.com/package/manifest)
+- [] [add-manifest](https://www.npmjs.com/package/add-manifest)
- [ ] [@mnfst/sdk](https://www.npmjs.com/package/@mnfst/sdk)
## Check list before submitting
diff --git a/README.md b/README.md
index c831ee95..fec5ff0c 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-A backend so simple that it fits in a YAML file
+A backend so simple that it fits into 1 YAML file
diff --git a/package-lock.json b/package-lock.json
index 4127e00a..79cdad89 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24384,7 +24384,7 @@
"license": "MIT"
},
"packages/add-manifest": {
- "version": "1.0.0",
+ "version": "1.0.2",
"license": "MIT",
"dependencies": {
"@oclif/core": "^3",
@@ -24457,7 +24457,7 @@
}
},
"packages/core/manifest": {
- "version": "4.0.1",
+ "version": "4.0.4",
"license": "MIT",
"dependencies": {
"@faker-js/faker": "^8.4.1",
@@ -24552,7 +24552,7 @@
},
"packages/js-sdk": {
"name": "@mnfst/sdk",
- "version": "1.0.2",
+ "version": "1.0.5",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
diff --git a/packages/add-manifest/README.md b/packages/add-manifest/README.md
index e6067011..31290f2d 100644
--- a/packages/add-manifest/README.md
+++ b/packages/add-manifest/README.md
@@ -14,9 +14,11 @@ npm install
# Run from a test folder to prevent messing with project files.
mkdir test-folder
cd test-folder
-../bin/dev.js create
+../bin/dev.js
```
+However due to the monorepo workspace structure, the launch script will fail as the path to the node modules folder is different than when served.
+
## Publish
```bash
diff --git a/packages/add-manifest/assets/README.md b/packages/add-manifest/assets/README.md
new file mode 100644
index 00000000..0cd97613
--- /dev/null
+++ b/packages/add-manifest/assets/README.md
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+A backend so simple that it fits into 1 YAML file
+
+
+
+
+
+
+
+
+
+
+## Description
+
+This project was made with [Manifest](https://github.com/mnfst/manifest).
+
+## Installation
+
+```bash
+$ npm install
+```
+
+## Running the app
+
+To run the app in the development mode:
+
+```bash
+npm run manifest
+```
+
+- Open [http://localhost:1111](http://localhost:1111) to open your admin UI it in your browser
+- Open [http://localhost:1111/api](http://localhost:111/api) to view your REST API documentation
+
+The page will reload when you make changes.
+
+## Seed dummy data
+
+Seeds some dummy data for your entities:
+
+```bash
+npm run manifest:seed
+```
+
+## Community & Resources
+
+- [Docs](https://manifest.build/docs) - Get started with Manifest
+- [Discord](https://discord.gg/FepAked3W7) - Come chat with the community
+- [Github](https://github.com/mnfst/manifest/issues) - Report bugs and share ideas to improve the product.
diff --git a/packages/add-manifest/package.json b/packages/add-manifest/package.json
index 1021bc0e..c68ee86b 100644
--- a/packages/add-manifest/package.json
+++ b/packages/add-manifest/package.json
@@ -1,6 +1,6 @@
{
"name": "add-manifest",
- "version": "1.0.2",
+ "version": "1.0.3",
"author": "Manifest",
"description": "Add Manifest backend",
"homepage": "https://manifest.build",
diff --git a/packages/add-manifest/src/commands/index.ts b/packages/add-manifest/src/commands/index.ts
index c9eba975..158707cb 100644
--- a/packages/add-manifest/src/commands/index.ts
+++ b/packages/add-manifest/src/commands/index.ts
@@ -28,11 +28,12 @@ export class MyCommand extends Command {
* 5. Update the .vscode/settings.json file with the recommended settings.
* 6. Update the .gitignore file with the recommended settings.
* 7. Update the .env file with the environment variables.
- * 8. Install the new packages.
- * 9. Serve the new app.
- * 10. Wait for the server to start.
- * 11. Seed the database.
- * 12. Open the browser.
+ * 8. If no README.md file exists, create one.
+ * 9. Install the new packages.
+ * 10. Serve the new app.
+ * 11. Wait for the server to start.
+ * 12. Seed the database.
+ * 13. Open the browser.
*/
async run(): Promise {
const folderName = 'manifest'
@@ -42,6 +43,7 @@ export class MyCommand extends Command {
const spinner = ora('Add Manifest to your project...').start()
+ // * 1. Create a folder with the name `manifest`.
// Construct the folder path. This example creates the folder in the current working directory.
const folderPath = path.join(process.cwd(), folderName)
@@ -56,6 +58,7 @@ export class MyCommand extends Command {
// Create the folder
fs.mkdirSync(folderPath)
+ // * 2. Create a file inside the folder with the name `manifest.yml`.
// Path where the new file should be created
const newFilePath = path.join(folderPath, initialFileName)
@@ -91,7 +94,7 @@ export class MyCommand extends Command {
updatePackageJsonFile({
fileContent: packageJson,
newPackages: {
- manifest: '^4.0.4'
+ manifest: '^4.0.5'
},
newScripts: {
manifest: 'node node_modules/manifest/scripts/watch/watch.js',
@@ -155,7 +158,7 @@ export class MyCommand extends Command {
})
)
- // Update the .gitignore file with the recommended settings.
+ // * 7. Update the .env file with the environment variables.
const gitignorePath = path.join(process.cwd(), '.gitignore')
let gitignoreContent = ''
@@ -163,14 +166,32 @@ export class MyCommand extends Command {
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8')
}
- if (!gitignoreContent.includes('node_modules')) {
- gitignoreContent += '\nnode_modules'
- gitignoreContent += '\n.env'
- }
+ const newGitignoreLines: string[] = [
+ 'node_modules',
+ '.env',
+ 'public',
+ 'manifest/backend.db'
+ ]
+ newGitignoreLines.forEach((line) => {
+ if (!gitignoreContent.includes(line)) {
+ gitignoreContent += `\n${line}`
+ }
+ })
fs.writeFileSync(gitignorePath, gitignoreContent)
spinner.succeed()
+
+ // * 8. Add a README.md file if it doesn't exist.
+ const readmeFilePath = path.join(process.cwd(), 'README.md')
+ if (!fs.existsSync(readmeFilePath)) {
+ fs.writeFileSync(
+ readmeFilePath,
+ fs.readFileSync(path.join(assetFolderPath, 'README.md'), 'utf8')
+ )
+ }
+
+ // * 9. Install the new packages.
spinner.start('Install dependencies...')
// Install deps.
diff --git a/packages/core/admin/src/app/app.component.html b/packages/core/admin/src/app/app.component.html
index d2e65e82..3577b25c 100644
--- a/packages/core/admin/src/app/app.component.html
+++ b/packages/core/admin/src/app/app.component.html
@@ -1,11 +1,10 @@
-
-
+
-
+
diff --git a/packages/core/admin/src/app/app.component.ts b/packages/core/admin/src/app/app.component.ts
index e3f3c932..be2098ca 100644
--- a/packages/core/admin/src/app/app.component.ts
+++ b/packages/core/admin/src/app/app.component.ts
@@ -11,13 +11,18 @@ export class AppComponent implements OnInit {
currentUser: Admin
isLogin = true
- constructor(private authService: AuthService, private router: Router) {}
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {}
ngOnInit() {
this.router.events.subscribe((routeChanged) => {
if (routeChanged instanceof NavigationEnd) {
window.scrollTo(0, 0)
- this.isLogin = routeChanged.url.includes('/auth/login')
+ this.isLogin =
+ routeChanged.url.includes('/auth/login') ||
+ routeChanged.url.includes('/auth/welcome')
if (this.isLogin) {
this.currentUser = null
diff --git a/packages/core/admin/src/app/modules/auth/auth-routing.module.ts b/packages/core/admin/src/app/modules/auth/auth-routing.module.ts
index 81cfd67b..85a4ce0e 100644
--- a/packages/core/admin/src/app/modules/auth/auth-routing.module.ts
+++ b/packages/core/admin/src/app/modules/auth/auth-routing.module.ts
@@ -4,6 +4,8 @@ import { RouterModule, Routes } from '@angular/router'
import { NotLoggedInGuard } from './guards/not-logged-in.guard'
import { LoginComponent } from './views/login/login.component'
import { LogoutComponent } from './views/logout/logout.component'
+import { RegisterFirstAdminComponent } from './views/register-first-admin/register-first-admin.component'
+import { IsDbEmptyGuard } from './guards/is-db-empty.guard'
export const authRoutes: Routes = [
{
@@ -14,6 +16,11 @@ export const authRoutes: Routes = [
{
path: 'logout',
component: LogoutComponent
+ },
+ {
+ path: 'welcome',
+ component: RegisterFirstAdminComponent,
+ canActivate: [IsDbEmptyGuard]
}
]
diff --git a/packages/core/admin/src/app/modules/auth/auth.module.ts b/packages/core/admin/src/app/modules/auth/auth.module.ts
index 8d6c1ccc..8ca69a11 100644
--- a/packages/core/admin/src/app/modules/auth/auth.module.ts
+++ b/packages/core/admin/src/app/modules/auth/auth.module.ts
@@ -5,9 +5,10 @@ import { SharedModule } from '../shared/shared.module'
import { AuthRoutingModule } from './auth-routing.module'
import { LoginComponent } from './views/login/login.component'
import { LogoutComponent } from './views/logout/logout.component'
+import { RegisterFirstAdminComponent } from './views/register-first-admin/register-first-admin.component'
@NgModule({
- declarations: [LoginComponent, LogoutComponent],
+ declarations: [LoginComponent, LogoutComponent, RegisterFirstAdminComponent],
imports: [CommonModule, AuthRoutingModule, SharedModule]
})
export class AuthModule {}
diff --git a/packages/core/admin/src/app/modules/auth/auth.service.ts b/packages/core/admin/src/app/modules/auth/auth.service.ts
index e74bc049..a06dfb5b 100644
--- a/packages/core/admin/src/app/modules/auth/auth.service.ts
+++ b/packages/core/admin/src/app/modules/auth/auth.service.ts
@@ -41,6 +41,37 @@ export class AuthService {
})
}
+ /**
+ * Signs up a new admin and logs them in.
+ *
+ * @param {Object} credentials - The credentials of the new admin
+ * @param {string} credentials.email - The email of the new admin
+ * @param {string} credentials.password - The password of the new admin
+ *
+ * @returns {Promise} The token of the new admin
+ */
+ async signup(credentials: {
+ email: string
+ password: string
+ }): Promise {
+ return (
+ firstValueFrom(
+ this.http.post(
+ `${environment.apiBaseUrl}/auth/admins/signup`,
+ credentials
+ )
+ ) as Promise<{
+ token: string
+ }>
+ ).then((res: { token: string }) => {
+ const token = res?.token
+ if (token) {
+ localStorage.setItem(TOKEN_KEY, token)
+ }
+ return token
+ })
+ }
+
logout(): void {
delete this.currentUserPromise
localStorage.removeItem(TOKEN_KEY)
@@ -76,4 +107,17 @@ export class AuthService {
) as Promise<{ exists: boolean }>
).then((res) => res.exists)
}
+
+ /**
+ * Returns true if the database is empty (no items, even admins), false otherwise.
+ *
+ * @returns {Promise} true if the database is empty, false otherwise
+ */
+ async isDbEmpty(): Promise {
+ return (
+ firstValueFrom(
+ this.http.get(`${environment.apiBaseUrl}/db/is-db-empty`)
+ ) as Promise<{ empty: boolean }>
+ ).then((res) => res.empty)
+ }
}
diff --git a/packages/core/admin/src/app/modules/auth/guards/is-db-empty.guard.ts b/packages/core/admin/src/app/modules/auth/guards/is-db-empty.guard.ts
new file mode 100644
index 00000000..4635f38d
--- /dev/null
+++ b/packages/core/admin/src/app/modules/auth/guards/is-db-empty.guard.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core'
+import { Router } from '@angular/router'
+import { AuthService } from '../auth.service'
+
+@Injectable({
+ providedIn: 'root'
+})
+export class IsDbEmptyGuard {
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {}
+ async canActivate(): Promise {
+ const isDbEmpty = await this.authService.isDbEmpty()
+
+ if (isDbEmpty) {
+ return true
+ }
+
+ this.router.navigate(['/auth/login'])
+ return false
+ }
+}
diff --git a/packages/core/admin/src/app/modules/auth/utlis/confirm-password-validator.ts b/packages/core/admin/src/app/modules/auth/utlis/confirm-password-validator.ts
new file mode 100644
index 00000000..765747f5
--- /dev/null
+++ b/packages/core/admin/src/app/modules/auth/utlis/confirm-password-validator.ts
@@ -0,0 +1,22 @@
+import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'
+
+export function confirmPasswordValidator(
+ passwordControlName: string
+): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const formGroup = control.parent
+ if (!formGroup) return null
+
+ const passwordControl = formGroup.get(passwordControlName)
+ if (!passwordControl) return null
+
+ const password = passwordControl.value
+ const confirmPassword = control.value
+
+ if (!confirmPassword || password !== confirmPassword) {
+ return { confirmPasswordMismatch: true }
+ }
+
+ return null
+ }
+}
diff --git a/packages/core/admin/src/app/modules/auth/views/login/login.component.html b/packages/core/admin/src/app/modules/auth/views/login/login.component.html
index fa175774..7ac5945a 100644
--- a/packages/core/admin/src/app/modules/auth/views/login/login.component.html
+++ b/packages/core/admin/src/app/modules/auth/views/login/login.component.html
@@ -25,32 +25,32 @@ Sign in
(valueChanged)="patchValue('email', $event)"
>
-
+
-
-
- Login
-
+
+
+ Login
+
-
+
diff --git a/packages/core/admin/src/app/modules/auth/views/login/login.component.ts b/packages/core/admin/src/app/modules/auth/views/login/login.component.ts
index 2a411167..f91d5ca0 100644
--- a/packages/core/admin/src/app/modules/auth/views/login/login.component.ts
+++ b/packages/core/admin/src/app/modules/auth/views/login/login.component.ts
@@ -31,6 +31,7 @@ export class LoginComponent implements OnInit {
ngOnInit(): void {
this.activatedRoute.queryParams.subscribe(async (queryParams: Params) => {
+ // Set suggested email and password from query params or default admin credentials.
if (queryParams['email'] && queryParams['password']) {
this.suggestedEmail = queryParams['email']
this.suggestedPassword = queryParams['password']
@@ -40,15 +41,20 @@ export class LoginComponent implements OnInit {
this.suggestedPassword = DEFAULT_ADMIN_CREDENTIALS.password
}
}
-
this.form = new FormGroup({
email: new FormControl(this.suggestedEmail || '', [
- Validators.required
+ Validators.required,
+ Validators.email
]),
password: new FormControl(this.suggestedPassword || '', [
Validators.required
])
})
+
+ // Redirect to register first admin if the database is empty.
+ if (await this.authService.isDbEmpty()) {
+ this.router.navigate(['/auth/welcome'])
+ }
})
}
diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html
new file mode 100644
index 00000000..5ab7bf88
--- /dev/null
+++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
Welcome
+
+
+ Welcome to your admin panel. Create your first admin account
+ to continue. You can always change those values later.
+
+
+
+
+
+ Login
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss
new file mode 100644
index 00000000..88db893c
--- /dev/null
+++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss
@@ -0,0 +1,61 @@
+@import '../../../../../styles/variables/all';
+@import 'bulma/sass/utilities/mixins';
+
+.container {
+ max-width: 100%;
+ width: 100%;
+}
+
+.hero.is-fullheight {
+ min-height: calc(100vh - 64px);
+
+ @include touch {
+ min-height: calc(100vh - 64px);
+ }
+}
+
+.notification {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: auto;
+ border-radius: 0;
+ bottom: unset;
+}
+
+.col-welcome {
+ position: fixed;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $white;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+
+ > div {
+ margin: auto;
+
+ @include widescreen {
+ max-width: 482px;
+ min-width: 382px;
+ }
+
+ @include desktop {
+ max-width: 424px;
+ min-width: 360px;
+ }
+
+ @include tablet {
+ max-width: 424px;
+ min-width: 320px;
+ }
+
+ @include mobile {
+ min-width: 50%;
+ max-width: 380px;
+ }
+ }
+}
diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts
new file mode 100644
index 00000000..2755f27e
--- /dev/null
+++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts
@@ -0,0 +1,63 @@
+import { Component, OnInit } from '@angular/core'
+import { FormControl, FormGroup, Validators } from '@angular/forms'
+import { PropType } from '@repo/types'
+import { confirmPasswordValidator } from '../../utlis/confirm-password-validator'
+import { AuthService } from '../../auth.service'
+import { Router } from '@angular/router'
+import { FlashMessageService } from '../../../shared/services/flash-message.service'
+
+@Component({
+ selector: 'app-register-first-admin',
+ templateUrl: './register-first-admin.component.html',
+ styleUrl: './register-first-admin.component.scss'
+})
+export class RegisterFirstAdminComponent implements OnInit {
+ form: FormGroup
+ PropType = PropType
+
+ constructor(
+ private authService: AuthService,
+ private router: Router,
+ private flashMessageService: FlashMessageService
+ ) {}
+
+ ngOnInit(): void {
+ this.form = new FormGroup({
+ email: new FormControl('', [Validators.required, Validators.email]),
+ password: new FormControl('', [Validators.required]),
+ confirmPassword: new FormControl('', [
+ Validators.required,
+ confirmPasswordValidator('password')
+ ])
+ })
+ }
+
+ /**
+ * Patch value to the form
+ *
+ * @param controlName
+ * @param value
+ *
+ * @returns void
+ */
+ patchValue(controlName: string, value: string) {
+ this.form.get(controlName)?.patchValue(value)
+ }
+
+ /**
+ * Submit the form
+ */
+ async submit(): Promise {
+ const token: string = await this.authService.signup(this.form.value)
+
+ if (!token) {
+ return this.flashMessageService.error('Error: Failed to register')
+ }
+
+ this.flashMessageService.success(
+ 'Welcome! You have successfully registered as an admin.'
+ )
+
+ this.router.navigate(['/'])
+ }
+}
diff --git a/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts
index c613dfed..a1350192 100644
--- a/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts
+++ b/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts
@@ -20,7 +20,7 @@ import { PropertyManifest } from '@repo/types'
class="input"
[ngClass]="{ 'is-danger': isError }"
type="email"
- placeholder="Email"
+ placeholder="Email..."
autocomplete="email"
(change)="onChange($event)"
#input
diff --git a/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts
index 7f9c2f42..95e6ae68 100644
--- a/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts
+++ b/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts
@@ -18,6 +18,7 @@ import { PropertyManifest } from '@repo/types'
{{ appManifest.name }}
@@ -48,7 +48,7 @@
{{ appManifest.name }}
@@ -80,7 +80,7 @@ {{ appManifest.name }}
@@ -90,7 +90,7 @@
{{ appManifest.name }}
@@ -116,7 +116,7 @@ {{ appManifest.name }}
@@ -126,7 +126,7 @@
{{ appManifest.name }}
@@ -156,7 +156,7 @@ {{ appManifest.name }}
@@ -167,7 +167,7 @@
{{ appManifest.name }}
diff --git a/packages/core/admin/src/assets/images/ext-link-01.svg b/packages/core/admin/src/assets/images/ext-link-01.svg
index b90b59ef..b8ed3b6f 100644
--- a/packages/core/admin/src/assets/images/ext-link-01.svg
+++ b/packages/core/admin/src/assets/images/ext-link-01.svg
@@ -1,17 +1,68 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/admin/src/styles/variables/_cards.scss b/packages/core/admin/src/styles/variables/_cards.scss
index acbae357..2f30a0f3 100644
--- a/packages/core/admin/src/styles/variables/_cards.scss
+++ b/packages/core/admin/src/styles/variables/_cards.scss
@@ -1,11 +1,15 @@
$card-color: $dark !default;
-$card-radius: 6px !default;
+$card-radius: 12px !default;
$border-color: $white-ter !default;
$card-header-color: inherit !default;
$card-header-padding: 2rem !default;
$card-content-padding: 2rem !default;
$card-header-shadow: 0 1px 0 $grey-lighter !default;
-$card-shadow: 0 10px 20px rgba($dark, 0.07) !default;
+$card-shadow:
+ rgba($dark, 0) 0px 0px 0px 0px,
+ rgba($dark, 0) 0px 0px 0px 0px,
+ rgba($dark, 0.1) 0px 1px 3px 0px,
+ rgba($dark, 0.1) 0px 1px 2px -1px !default;
$card-footer-padding: 2rem !default;
$card-footer-border-top: 1px solid $grey-lighter !default;
diff --git a/packages/core/admin/src/styles/variables/_generic.scss b/packages/core/admin/src/styles/variables/_generic.scss
index cd9ea33b..6f50a290 100644
--- a/packages/core/admin/src/styles/variables/_generic.scss
+++ b/packages/core/admin/src/styles/variables/_generic.scss
@@ -1,5 +1,9 @@
// generic
-$shadow: 0 10px 20px 0 rgba($dark, 0.07) !default;
+$shadow:
+ rgba($dark, 0) 0px 0px 0px 0px,
+ rgba($dark, 0) 0px 0px 0px 0px,
+ rgba($dark, 0.1) 0px 1px 3px 0px,
+ rgba($dark, 0.1) 0px 1px 2px -1px !default;
$border-color: $white-ter !default;
$radius-large: 6px !default;
$link-hover: $primary !default;
diff --git a/packages/core/manifest/package.json b/packages/core/manifest/package.json
index dd27cb55..17b9452a 100644
--- a/packages/core/manifest/package.json
+++ b/packages/core/manifest/package.json
@@ -1,7 +1,7 @@
{
"name": "manifest",
- "version": "4.0.4",
- "description": "A backend so simple that it fits in a YAML file",
+ "version": "4.0.5",
+ "description": "A backend so simple that it fits into 1 YAML file",
"author": "Manifest",
"license": "MIT",
"homepage": "https://manifest.build",
@@ -16,11 +16,12 @@
"manifest",
"backend",
"backend-as-a-service",
- "bass",
+ "baas",
"api",
"rest",
"fullstack",
- "yaml"
+ "yaml",
+ "headless"
],
"scripts": {
"build": "nest build && cd ../admin && npm run build",
diff --git a/packages/core/manifest/src/app.module.ts b/packages/core/manifest/src/app.module.ts
index 5c46f8b5..c12e0fef 100644
--- a/packages/core/manifest/src/app.module.ts
+++ b/packages/core/manifest/src/app.module.ts
@@ -66,9 +66,8 @@ export class AppModule {
private async init() {
const isSeed: boolean = process.argv[1].includes('seed')
const isTest: boolean = process.env.NODE_ENV === 'test'
- const isProduction: boolean = process.env.NODE_ENV === 'production'
- if (!isSeed && !isTest && !isProduction) {
+ if (!isSeed && !isTest) {
this.loggerService.initMessage()
}
}
diff --git a/packages/core/manifest/src/auth/auth.controller.ts b/packages/core/manifest/src/auth/auth.controller.ts
index 1d51e5b2..e22e3b17 100644
--- a/packages/core/manifest/src/auth/auth.controller.ts
+++ b/packages/core/manifest/src/auth/auth.controller.ts
@@ -14,6 +14,7 @@ import { AuthService } from './auth.service'
import { SignupAuthenticableEntityDto } from './dtos/signup-authenticable-entity.dto'
import { Rule } from './decorators/rule.decorator'
import { AuthorizationGuard } from './guards/authorization.guard'
+import { IsDbEmptyGuard } from './guards/is-db-empty.guard'
@Controller('auth')
@UseGuards(AuthorizationGuard)
@@ -30,6 +31,16 @@ export class AuthController {
return this.authService.createToken(entity, signupUserDto)
}
+ @Post('admins/signup')
+ @UseGuards(IsDbEmptyGuard)
+ public async signupAdmin(
+ @Body() signupUserDto: SignupAuthenticableEntityDto
+ ): Promise<{
+ token: string
+ }> {
+ return this.authService.signup('admins', signupUserDto, true)
+ }
+
@Post(':entity/signup')
@Rule('signup')
public async signup(
diff --git a/packages/core/manifest/src/auth/auth.module.ts b/packages/core/manifest/src/auth/auth.module.ts
index 6afc6c91..1df89597 100644
--- a/packages/core/manifest/src/auth/auth.module.ts
+++ b/packages/core/manifest/src/auth/auth.module.ts
@@ -4,11 +4,12 @@ import { EntityModule } from '../entity/entity.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { ManifestModule } from '../manifest/manifest.module'
+import { DatabaseService } from '../crud/services/database.service'
@Module({
imports: [EntityModule, forwardRef(() => ManifestModule)],
controllers: [AuthController],
- providers: [AuthService],
+ providers: [AuthService, DatabaseService],
exports: [AuthService]
})
export class AuthModule {}
diff --git a/packages/core/manifest/src/auth/auth.service.ts b/packages/core/manifest/src/auth/auth.service.ts
index f3cd53cc..528600dd 100644
--- a/packages/core/manifest/src/auth/auth.service.ts
+++ b/packages/core/manifest/src/auth/auth.service.ts
@@ -78,15 +78,17 @@ export class AuthService {
* @param entitySlug The slug of the AuthenticableEntity where the user is going to be created
* @param email The email of the user
* @param password The password of the user
+ * @param byPassAdminCheck If true, the method will not check if the entity is an admin
*
* @returns A JWT token of the created user
*
*/
async signup(
entitySlug: string,
- signupUserDto: SignupAuthenticableEntityDto
+ signupUserDto: SignupAuthenticableEntityDto,
+ byPassAdminCheck = false
): Promise<{ token: string }> {
- if (entitySlug === ADMIN_ENTITY_MANIFEST.slug) {
+ if (entitySlug === ADMIN_ENTITY_MANIFEST.slug && !byPassAdminCheck) {
throw new HttpException(
'Admins cannot be created with this method.',
HttpStatus.BAD_REQUEST
diff --git a/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts b/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts
index 9bba208f..a7aa2130 100644
--- a/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts
+++ b/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts
@@ -1,9 +1,11 @@
-import { IsEmail, IsNotEmpty } from 'class-validator'
+import { IsEmail, IsNotEmpty, IsString } from 'class-validator'
export class SignupAuthenticableEntityDto {
@IsEmail()
+ @IsNotEmpty()
public email: string
+ @IsString()
@IsNotEmpty()
public password: string
}
diff --git a/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts b/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts
new file mode 100644
index 00000000..80fc483f
--- /dev/null
+++ b/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts
@@ -0,0 +1,16 @@
+import { CanActivate, Injectable } from '@nestjs/common'
+import { DatabaseService } from '../../crud/services/database.service'
+
+@Injectable()
+export class IsDbEmptyGuard implements CanActivate {
+ constructor(private readonly databaseService: DatabaseService) {}
+
+ /**
+ * Check if the database is empty (no items in any entity, even admin).
+ *
+ * @returns True if the database is empty, false otherwise.
+ * */
+ async canActivate(): Promise {
+ return this.databaseService.isDbEmpty()
+ }
+}
diff --git a/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts b/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts
new file mode 100644
index 00000000..341a9cb0
--- /dev/null
+++ b/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts
@@ -0,0 +1,45 @@
+import { Test } from '@nestjs/testing'
+import { IsDbEmptyGuard } from '../guards/is-db-empty.guard'
+import { DatabaseService } from '../../crud/services/database.service'
+
+describe('IsDbEmptyGuard', () => {
+ let databaseService: DatabaseService
+
+ beforeEach(async () => {
+ const module = await Test.createTestingModule({
+ providers: [
+ IsDbEmptyGuard,
+ {
+ provide: DatabaseService,
+ useValue: {
+ isDbEmpty: jest.fn().mockReturnValue(Promise.resolve(true))
+ }
+ }
+ ]
+ }).compile()
+
+ databaseService = module.get(DatabaseService)
+ })
+
+ it('should be defined', () => {
+ expect(new IsDbEmptyGuard(databaseService)).toBeDefined()
+ })
+
+ it('should return true if the database is empty', async () => {
+ const isDbEmptyGuard = new IsDbEmptyGuard(databaseService)
+ const res = await isDbEmptyGuard.canActivate()
+
+ expect(res).toBe(true)
+ })
+
+ it('should return false if the database is not empty', async () => {
+ jest
+ .spyOn(databaseService, 'isDbEmpty')
+ .mockReturnValue(Promise.resolve(false))
+
+ const isDbEmptyGuard = new IsDbEmptyGuard(databaseService)
+ const res = await isDbEmptyGuard.canActivate()
+
+ expect(res).toBe(false)
+ })
+})
diff --git a/packages/core/manifest/src/crud/controllers/database.controller.ts b/packages/core/manifest/src/crud/controllers/database.controller.ts
new file mode 100644
index 00000000..8eaae478
--- /dev/null
+++ b/packages/core/manifest/src/crud/controllers/database.controller.ts
@@ -0,0 +1,16 @@
+import { Controller, Get } from '@nestjs/common'
+import { DatabaseService } from '../services/database.service'
+
+@Controller('db')
+export class DatabaseController {
+ constructor(private readonly databaseService: DatabaseService) {}
+
+ @Get('is-db-empty')
+ public async isDbEmpty(): Promise<{
+ empty: boolean
+ }> {
+ const empty = await this.databaseService.isDbEmpty()
+
+ return { empty }
+ }
+}
diff --git a/packages/core/manifest/src/crud/crud.module.ts b/packages/core/manifest/src/crud/crud.module.ts
index 6d1a06d1..7e8daee7 100644
--- a/packages/core/manifest/src/crud/crud.module.ts
+++ b/packages/core/manifest/src/crud/crud.module.ts
@@ -8,10 +8,13 @@ import { CrudService } from './services/crud.service'
import { PaginationService } from './services/pagination.service'
import { ValidationModule } from '../validation/validation.module'
import { AuthService } from '../auth/auth.service'
+import { DatabaseService } from './services/database.service'
+import { DatabaseController } from './controllers/database.controller'
@Module({
imports: [EntityModule, ManifestModule, ValidationModule],
- controllers: [CrudController],
- providers: [CrudService, PaginationService, AuthService]
+ controllers: [CrudController, DatabaseController],
+ providers: [CrudService, PaginationService, AuthService, DatabaseService],
+ exports: [DatabaseService]
})
export class CrudModule {}
diff --git a/packages/core/manifest/src/crud/services/database.service.ts b/packages/core/manifest/src/crud/services/database.service.ts
new file mode 100644
index 00000000..639c1a70
--- /dev/null
+++ b/packages/core/manifest/src/crud/services/database.service.ts
@@ -0,0 +1,43 @@
+import { Injectable } from '@nestjs/common'
+import { ManifestService } from '../../manifest/services/manifest.service'
+import { EntityService } from '../../entity/services/entity.service'
+import { AppManifest, EntityManifest } from '../../../../types/src'
+import { ADMIN_ENTITY_MANIFEST } from '../../constants'
+
+@Injectable()
+export class DatabaseService {
+ constructor(
+ private manifestService: ManifestService,
+ private entityService: EntityService
+ ) {}
+
+ /**
+ * Check if the database is empty (no items in any entity, even admin).
+ *
+ * @returns true if the database is empty, false otherwise.
+ * */
+ async isDbEmpty(): Promise {
+ const appManifest: AppManifest = this.manifestService.getAppManifest()
+
+ const entities = [
+ ...Object.values(appManifest.entities),
+ ADMIN_ENTITY_MANIFEST
+ ]
+ let totalItems = 0
+
+ await Promise.all(
+ Object.values(entities).map(async (entityManifest: EntityManifest) => {
+ return this.entityService
+ .getEntityRepository({
+ entitySlug: entityManifest.slug
+ })
+ .createQueryBuilder('entity')
+ .getCount()
+ })
+ ).then((counts: number[]) => {
+ totalItems = counts.reduce((acc, count) => acc + count, 0)
+ })
+
+ return totalItems === 0
+ }
+}
diff --git a/packages/core/manifest/src/crud/tests/database.controller.spec.ts b/packages/core/manifest/src/crud/tests/database.controller.spec.ts
new file mode 100644
index 00000000..80525a7d
--- /dev/null
+++ b/packages/core/manifest/src/crud/tests/database.controller.spec.ts
@@ -0,0 +1,27 @@
+import { Test, TestingModule } from '@nestjs/testing'
+import { DatabaseController } from '../controllers/database.controller'
+import { DatabaseService } from '../services/database.service'
+
+describe('DatabaseController', () => {
+ let controller: DatabaseController
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [DatabaseController],
+ providers: [
+ {
+ provide: DatabaseService,
+ useValue: {
+ isDbEmpty: jest.fn().mockReturnValue(Promise.resolve(true))
+ }
+ }
+ ]
+ }).compile()
+
+ controller = module.get(DatabaseController)
+ })
+
+ it('should be defined', () => {
+ expect(controller).toBeDefined()
+ })
+})
diff --git a/packages/core/manifest/src/crud/tests/database.service.spec.ts b/packages/core/manifest/src/crud/tests/database.service.spec.ts
new file mode 100644
index 00000000..6c1a7c5b
--- /dev/null
+++ b/packages/core/manifest/src/crud/tests/database.service.spec.ts
@@ -0,0 +1,62 @@
+import { Test } from '@nestjs/testing'
+import { DatabaseService } from '../services/database.service'
+import { ManifestService } from '../../manifest/services/manifest.service'
+import { EntityService } from '../../entity/services/entity.service'
+
+describe('DatabaseService', () => {
+ let manifestService: ManifestService
+ let entityService: EntityService
+ let service: DatabaseService
+
+ beforeEach(async () => {
+ const module = await Test.createTestingModule({
+ providers: [
+ DatabaseService,
+ {
+ provide: ManifestService,
+ useValue: {
+ getAppManifest: jest.fn().mockReturnValue({
+ entities: {}
+ })
+ }
+ },
+ {
+ provide: EntityService,
+ useValue: {
+ getEntityRepository: jest.fn().mockReturnValue({
+ createQueryBuilder: jest.fn().mockReturnValue({
+ getCount: jest.fn().mockReturnValue(0)
+ })
+ })
+ }
+ }
+ ]
+ }).compile()
+
+ manifestService = module.get(ManifestService)
+ entityService = module.get(EntityService)
+ service = module.get(DatabaseService)
+ })
+
+ it('should be defined', () => {
+ expect(service).toBeDefined()
+ })
+
+ it('should return true if the database is empty', async () => {
+ const res = await service.isDbEmpty()
+
+ expect(res).toBe(true)
+ })
+
+ it('should return false if the database is not empty', async () => {
+ jest.spyOn(entityService, 'getEntityRepository').mockReturnValue({
+ createQueryBuilder: jest.fn().mockReturnValue({
+ getCount: jest.fn().mockReturnValue(1)
+ })
+ } as any)
+
+ const res = await service.isDbEmpty()
+
+ expect(res).toBe(false)
+ })
+})
diff --git a/packages/core/manifest/src/main.ts b/packages/core/manifest/src/main.ts
index 02f1d37d..0def0a9c 100644
--- a/packages/core/manifest/src/main.ts
+++ b/packages/core/manifest/src/main.ts
@@ -51,14 +51,13 @@ async function bootstrap() {
}
})
- if (!isProduction) {
- const openApiService: OpenApiService = app.get(OpenApiService)
+ const openApiService: OpenApiService = app.get(OpenApiService)
- SwaggerModule.setup('api', app, openApiService.generateOpenApiObject(), {
- customfavIcon: 'assets/images/open-api/favicon.ico',
- customSiteTitle: 'Manifest API Doc',
+ SwaggerModule.setup('api', app, openApiService.generateOpenApiObject(), {
+ customfavIcon: 'assets/images/open-api/favicon.ico',
+ customSiteTitle: 'Manifest API Doc',
- customCss: `
+ customCss: `
.swagger-ui html {
box-sizing: border-box;
@@ -1791,8 +1790,7 @@ background: #ce107c;
fill: #535356;
}
`
- })
- }
+ })
await app.listen(configService.get('PORT') || DEFAULT_PORT)
}