Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👋 Onboarding first admin #223

Merged
merged 11 commits into from
Nov 4, 2024
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</p>

<p align='center'>
<strong>A backend so simple that it fits in a YAML file</strong>
<strong>A backend so simple that it fits into 1 YAML file</strong>
<br><br>
<a href="https://www.npmjs.com/package/manifest" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/manifest"></a>
<a href="https://www.codefactor.io/repository/github/mnfst/manifest" target="_blank"><img alt="CodeFactor Grade" src="https://img.shields.io/codefactor/grade/github/mnfst/manifest"></a>
Expand Down
4 changes: 3 additions & 1 deletion packages/add-manifest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions packages/add-manifest/assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<br>
<p align="center">
<a href="https://manifest.build/#gh-light-mode-only">
<img alt="manifest" src="https://manifest.build/assets/images/logo-transparent.svg" height="55px" alt="Manifest logo" title="Manifest - A backend so simple that it fits in a YAML file" />
</a>
<a href="https://manifest.build/#gh-dark-mode-only">
<img alt="manifest" src="https://manifest.build/assets/images/logo-light.svg" height="55px" alt="Manifest logo" title="Manifest - A backend so simple that it fits in a YAML file" />
</a>
</p>

<p align='center'>
<strong>A backend so simple that it fits into 1 YAML file</strong>
<br><br>
<a href="https://www.npmjs.com/package/manifest" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/manifest"></a>
<a href="https://www.codefactor.io/repository/github/mnfst/manifest" target="_blank"><img alt="CodeFactor Grade" src="https://img.shields.io/codefactor/grade/github/mnfst/manifest"></a>
<a href="https://discord.com/invite/FepAked3W7" target="_blank"><img alt="Discord" src="https://img.shields.io/discord/1089907785178812499?label=discord"></a>
<a href="https://opencollective.com/mnfst" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://www.codetriage.com/mnfst/manifest" target="_blank"><img alt="CodeTriage" src="https://www.codetriage.com/mnfst/manifest/badges/users.svg"></a>
<a href="https://github.com/mnfst/manifest/blob/develop/LICENSE" target="_blank"><img alt="License MIT" src="https://img.shields.io/badge/licence-MIT-green"></a>
<br>
</p>

## 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.
41 changes: 31 additions & 10 deletions packages/add-manifest/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const folderName = 'manifest'
Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -155,22 +158,40 @@ 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 = ''

if (fs.existsSync(gitignorePath)) {
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.
Expand Down
5 changes: 2 additions & 3 deletions packages/core/admin/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<ng-container *ngIf="!isLogin">
<app-touch-menu class="is-hidden-desktop"></app-touch-menu>

<app-touch-menu class="is-hidden-desktop"></app-touch-menu>
</ng-container>

<app-flash-message></app-flash-message>

<div class="wrapper" >
<div class="wrapper">
<div class="columns">
<aside class="column aside menu px-0 is-hidden-touch" *ngIf="!isLogin">
<app-side-menu></app-side-menu>
Expand Down
9 changes: 7 additions & 2 deletions packages/core/admin/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand All @@ -14,6 +16,11 @@ export const authRoutes: Routes = [
{
path: 'logout',
component: LogoutComponent
},
{
path: 'welcome',
component: RegisterFirstAdminComponent,
canActivate: [IsDbEmptyGuard]
}
]

Expand Down
3 changes: 2 additions & 1 deletion packages/core/admin/src/app/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
44 changes: 44 additions & 0 deletions packages/core/admin/src/app/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} The token of the new admin
*/
async signup(credentials: {
email: string
password: string
}): Promise<string> {
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)
Expand Down Expand Up @@ -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<boolean>} true if the database is empty, false otherwise
*/
async isDbEmpty(): Promise<boolean> {
return (
firstValueFrom(
this.http.get(`${environment.apiBaseUrl}/db/is-db-empty`)
) as Promise<{ empty: boolean }>
).then((res) => res.empty)
}
}
Original file line number Diff line number Diff line change
@@ -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<boolean> {
const isDbEmpty = await this.authService.isDbEmpty()

if (isDbEmpty) {
return true
}

this.router.navigate(['/auth/login'])
return false
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,32 @@ <h2 class="title is-4">Sign in</h2>
(valueChanged)="patchValue('email', $event)"
></app-input>
</div>
<div class="field mb-5">
<div class="control">
<app-input
[prop]="{
name: 'password',
type: PropType.Password
}"
[value]="suggestedPassword"
(valueChanged)="patchValue('password', $event)"
></app-input>
</div>
</div>
<div class="field mb-5">
<div class="control">
<app-input
[prop]="{
name: 'password',
type: PropType.Password
}"
[value]="suggestedPassword"
(valueChanged)="patchValue('password', $event)"
></app-input>
</div>
</div>

<button
class="button is-block is-dark is-fullwidth mb-3"
(click)="submit()"
[disabled]="!form.valid"
>
Login
</button>
</div>

<button
class="button is-block is-dark is-fullwidth mb-3"
(click)="submit()"
[disabled]="!form.valid"
>
Login
</button>
</div>
</div>
<div class="column col-decoration"></div>
</div>
<div class="column col-decoration"></div>
</div>
</div>
</section>
Loading
Loading