@foliokit/cms-core 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -2
- package/eslint.config.mjs +48 -0
- package/ng-package.json +7 -0
- package/package.json +5 -18
- package/project.json +32 -0
- package/{esm2022/index.js → src/index.ts} +9 -3
- package/src/lib/cms-core/cms-core.html +1 -0
- package/src/lib/cms-core/cms-core.scss +0 -0
- package/src/lib/cms-core/cms-core.spec.ts +44 -0
- package/src/lib/cms-core/cms-core.ts +9 -0
- package/src/lib/firebase/firebase-admin.ts +32 -0
- package/src/lib/firebase/firebase.config.ts +26 -0
- package/src/lib/firebase/firebase.providers.ts +89 -0
- package/src/lib/firebase/foliokit.providers.ts +178 -0
- package/src/lib/models/author.model.ts +16 -0
- package/src/lib/models/page.model.ts +11 -0
- package/src/lib/models/post.model.ts +41 -0
- package/src/lib/models/site-config.model.ts +103 -0
- package/src/lib/models/tag.model.ts +5 -0
- package/src/lib/pipes/tag-label.pipe.ts +16 -0
- package/src/lib/resolvers/about-page.resolver.ts +76 -0
- package/src/lib/resolvers/links-page.resolver.ts +77 -0
- package/src/lib/resolvers/posts.resolver.ts +51 -0
- package/src/lib/services/auth.service.ts +49 -0
- package/src/lib/services/author.service.ts +88 -0
- package/src/lib/services/post.service.spec.ts +255 -0
- package/src/lib/services/post.service.ts +148 -0
- package/src/lib/services/site-config.service.ts +86 -0
- package/src/lib/services/tag.service.ts +24 -0
- package/src/lib/tokens/post-service.token.ts +14 -0
- package/src/lib/tokens/site-config-service.token.ts +12 -0
- package/src/lib/utils/normalize-author.ts +50 -0
- package/src/lib/utils/normalize-post.ts +66 -0
- package/src/lib/utils/normalize-site-config.ts +145 -0
- package/testing/firestore.stub.ts +65 -0
- package/tsconfig.json +31 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +8 -0
- package/esm2022/foliokit-cms-core.js +0 -5
- package/esm2022/foliokit-cms-core.js.map +0 -1
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/firebase/firebase.config.js +0 -8
- package/esm2022/lib/firebase/firebase.config.js.map +0 -1
- package/esm2022/lib/firebase/firebase.providers.js +0 -54
- package/esm2022/lib/firebase/firebase.providers.js.map +0 -1
- package/esm2022/lib/models/author.model.js +0 -1
- package/esm2022/lib/models/author.model.js.map +0 -1
- package/esm2022/lib/models/page.model.js +0 -1
- package/esm2022/lib/models/page.model.js.map +0 -1
- package/esm2022/lib/models/post.model.js +0 -1
- package/esm2022/lib/models/post.model.js.map +0 -1
- package/esm2022/lib/models/site-config.model.js +0 -1
- package/esm2022/lib/models/site-config.model.js.map +0 -1
- package/esm2022/lib/models/tag.model.js +0 -1
- package/esm2022/lib/models/tag.model.js.map +0 -1
- package/esm2022/lib/services/auth.service.js +0 -42
- package/esm2022/lib/services/auth.service.js.map +0 -1
- package/esm2022/lib/services/page.service.js +0 -73
- package/esm2022/lib/services/page.service.js.map +0 -1
- package/esm2022/lib/services/post.service.js +0 -83
- package/esm2022/lib/services/post.service.js.map +0 -1
- package/esm2022/lib/services/site-config.service.js +0 -31
- package/esm2022/lib/services/site-config.service.js.map +0 -1
- package/esm2022/lib/services/tag.service.js +0 -22
- package/esm2022/lib/services/tag.service.js.map +0 -1
- package/esm2022/lib/tokens/page-service.token.js +0 -4
- package/esm2022/lib/tokens/page-service.token.js.map +0 -1
- package/esm2022/lib/tokens/post-service.token.js +0 -5
- package/esm2022/lib/tokens/post-service.token.js.map +0 -1
- package/esm2022/lib/utils/normalize-page.js +0 -74
- package/esm2022/lib/utils/normalize-page.js.map +0 -1
- package/esm2022/lib/utils/normalize-post.js +0 -66
- package/esm2022/lib/utils/normalize-post.js.map +0 -1
- package/esm2022/lib/utils/normalize-site-config.js +0 -62
- package/esm2022/lib/utils/normalize-site-config.js.map +0 -1
- package/foliokit-cms-core.d.ts +0 -5
- package/index.d.ts +0 -14
- package/lib/firebase/firebase.config.d.ts +0 -11
- package/lib/firebase/firebase.providers.d.ts +0 -3
- package/lib/models/author.model.d.ts +0 -14
- package/lib/models/page.model.d.ts +0 -40
- package/lib/models/post.model.d.ts +0 -39
- package/lib/models/site-config.model.d.ts +0 -27
- package/lib/models/tag.model.d.ts +0 -5
- package/lib/services/auth.service.d.ts +0 -13
- package/lib/services/page.service.d.ts +0 -15
- package/lib/services/post.service.d.ts +0 -17
- package/lib/services/site-config.service.d.ts +0 -10
- package/lib/services/tag.service.d.ts +0 -9
- package/lib/tokens/page-service.token.d.ts +0 -9
- package/lib/tokens/post-service.token.d.ts +0 -10
- package/lib/utils/normalize-page.d.ts +0 -2
- package/lib/utils/normalize-post.d.ts +0 -6
- package/lib/utils/normalize-site-config.d.ts +0 -2
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
1
|
# @foliokit/cms-core
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
Core Firebase services, data models, and injection tokens for the FolioKit CMS.
|
|
4
|
+
Provides `provideFolioKit()` — the single-call bootstrapper that sets up Firebase
|
|
5
|
+
(Firestore, Storage, Auth) and default service bindings for Angular applications.
|
|
6
|
+
|
|
7
|
+
Part of the [FolioKit](https://github.com/doug-williamson/foliokit) ecosystem.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @foliokit/cms-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Peer Dependencies
|
|
16
|
+
|
|
17
|
+
| Package | Version |
|
|
18
|
+
|---------|---------|
|
|
19
|
+
| `@angular/common` | `^21.2.4` |
|
|
20
|
+
| `@angular/core` | `^21.2.4` |
|
|
21
|
+
| `firebase` | `^11.10.0` |
|
|
22
|
+
| `rxjs` | `~7.8.0` |
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// app.config.ts
|
|
28
|
+
import { provideFolioKit } from '@foliokit/cms-core';
|
|
29
|
+
|
|
30
|
+
export const appConfig: ApplicationConfig = {
|
|
31
|
+
providers: [
|
|
32
|
+
provideFolioKit({
|
|
33
|
+
firebaseConfig: {
|
|
34
|
+
apiKey: '...',
|
|
35
|
+
authDomain: '...',
|
|
36
|
+
projectId: '...',
|
|
37
|
+
storageBucket: '...',
|
|
38
|
+
messagingSenderId: '...',
|
|
39
|
+
appId: '...',
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`provideFolioKit()` registers:
|
|
47
|
+
- Firebase App, Firestore, Storage, and Auth (SSR-safe — returns `null` on the server)
|
|
48
|
+
- `PostService` bound to `BLOG_POST_SERVICE`
|
|
49
|
+
- `SiteConfigService` bound to `SITE_CONFIG_SERVICE`
|
|
50
|
+
- Optional `SITE_ID` token (when `siteId` is provided)
|
|
51
|
+
|
|
52
|
+
## What's Included
|
|
53
|
+
|
|
54
|
+
- **`provideFolioKit(config)`** — single-call bootstrapper
|
|
55
|
+
- **`provideFirebase(options, useEmulator)`** — lower-level Firebase-only setup
|
|
56
|
+
- **Data models** — `BlogPost`, `SiteConfig`, `NavItem`, `Author`, `Tag`, `SeoMeta`, `AboutPageConfig`, `LinksPageConfig`, `HomePageConfig`
|
|
57
|
+
- **Services** — `PostService`, `SiteConfigService`, `AuthorService`, `AuthService`, `TagService`
|
|
58
|
+
- **DI tokens** — `BLOG_POST_SERVICE`, `SITE_CONFIG_SERVICE`, `SITE_ID`, `FIREBASE_OPTIONS`, `FIRESTORE`, `FIREBASE_STORAGE`, `FIREBASE_AUTH`
|
|
59
|
+
- **Pipes** — `TagLabelPipe`
|
|
60
|
+
|
|
61
|
+
## Full Documentation
|
|
62
|
+
|
|
63
|
+
[foliokitcms.com/docs/getting-started](https://foliokitcms.com/docs/getting-started)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import nx from '@nx/eslint-plugin';
|
|
2
|
+
import baseConfig from '../../eslint.config.mjs';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...baseConfig,
|
|
6
|
+
{
|
|
7
|
+
files: ['**/*.json'],
|
|
8
|
+
rules: {
|
|
9
|
+
'@nx/dependency-checks': [
|
|
10
|
+
'error',
|
|
11
|
+
{
|
|
12
|
+
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: await import('jsonc-eslint-parser'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
...nx.configs['flat/angular'],
|
|
21
|
+
...nx.configs['flat/angular-template'],
|
|
22
|
+
{
|
|
23
|
+
files: ['**/*.ts'],
|
|
24
|
+
rules: {
|
|
25
|
+
'@angular-eslint/directive-selector': [
|
|
26
|
+
'error',
|
|
27
|
+
{
|
|
28
|
+
type: 'attribute',
|
|
29
|
+
prefix: 'lib',
|
|
30
|
+
style: 'camelCase',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
'@angular-eslint/component-selector': [
|
|
34
|
+
'error',
|
|
35
|
+
{
|
|
36
|
+
type: 'element',
|
|
37
|
+
prefix: 'lib',
|
|
38
|
+
style: 'kebab-case',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
files: ['**/*.html'],
|
|
45
|
+
// Override or add rules here
|
|
46
|
+
rules: {},
|
|
47
|
+
},
|
|
48
|
+
];
|
package/ng-package.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foliokit/cms-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Core Firebase services, models, and tokens for FolioKit CMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/dougwilliamson/foliokit"
|
|
15
|
+
"url": "git+https://github.com/dougwilliamson/foliokit.git"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
@@ -20,22 +20,9 @@
|
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@angular/common": "^21.2.4",
|
|
22
22
|
"@angular/core": "^21.2.4",
|
|
23
|
+
"@angular/router": "^21.2.4",
|
|
23
24
|
"firebase": "^11.10.0",
|
|
24
25
|
"rxjs": "~7.8.0"
|
|
25
26
|
},
|
|
26
|
-
"sideEffects": false
|
|
27
|
-
|
|
28
|
-
"typings": "foliokit-cms-core.d.ts",
|
|
29
|
-
"exports": {
|
|
30
|
-
"./package.json": {
|
|
31
|
-
"default": "./package.json"
|
|
32
|
-
},
|
|
33
|
-
".": {
|
|
34
|
-
"types": "./foliokit-cms-core.d.ts",
|
|
35
|
-
"default": "./esm2022/foliokit-cms-core.js"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"tslib": "^2.3.0"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
27
|
+
"sideEffects": false
|
|
28
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cms-core",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/cms-core/src",
|
|
5
|
+
"prefix": "lib",
|
|
6
|
+
"projectType": "library",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"targets": {
|
|
9
|
+
"build": {
|
|
10
|
+
"executor": "@nx/angular:ng-packagr-lite",
|
|
11
|
+
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
|
|
12
|
+
"options": {
|
|
13
|
+
"project": "libs/cms-core/ng-package.json",
|
|
14
|
+
"tsConfig": "libs/cms-core/tsconfig.lib.json"
|
|
15
|
+
},
|
|
16
|
+
"configurations": {
|
|
17
|
+
"production": {
|
|
18
|
+
"tsConfig": "libs/cms-core/tsconfig.lib.prod.json"
|
|
19
|
+
},
|
|
20
|
+
"development": {}
|
|
21
|
+
},
|
|
22
|
+
"defaultConfiguration": "production"
|
|
23
|
+
},
|
|
24
|
+
"test": {
|
|
25
|
+
"executor": "@nx/angular:unit-test",
|
|
26
|
+
"options": {}
|
|
27
|
+
},
|
|
28
|
+
"lint": {
|
|
29
|
+
"executor": "@nx/eslint:lint"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './lib/firebase/firebase.config';
|
|
2
2
|
export * from './lib/firebase/firebase.providers';
|
|
3
|
+
export * from './lib/firebase/foliokit.providers';
|
|
3
4
|
// firebase-admin.ts is intentionally excluded — server-only, import directly in SSR server files
|
|
4
5
|
export * from './lib/models/post.model';
|
|
5
6
|
export * from './lib/models/site-config.model';
|
|
@@ -7,10 +8,15 @@ export * from './lib/models/page.model';
|
|
|
7
8
|
export * from './lib/models/tag.model';
|
|
8
9
|
export * from './lib/models/author.model';
|
|
9
10
|
export * from './lib/services/auth.service';
|
|
11
|
+
export * from './lib/services/author.service';
|
|
10
12
|
export * from './lib/services/post.service';
|
|
11
|
-
export * from './lib/services/page.service';
|
|
12
13
|
export * from './lib/services/site-config.service';
|
|
13
14
|
export * from './lib/services/tag.service';
|
|
14
15
|
export * from './lib/tokens/post-service.token';
|
|
15
|
-
export * from './lib/tokens/
|
|
16
|
-
|
|
16
|
+
export * from './lib/tokens/site-config-service.token';
|
|
17
|
+
export * from './lib/pipes/tag-label.pipe';
|
|
18
|
+
|
|
19
|
+
// ── Resolvers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
export * from './lib/resolvers/about-page.resolver';
|
|
21
|
+
export * from './lib/resolvers/links-page.resolver';
|
|
22
|
+
export * from './lib/resolvers/posts.resolver';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p>CmsCore works!</p>
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { ADMIN_EMAIL, SITE_ID, provideFolioKit } from '@foliokit/cms-core';
|
|
3
|
+
|
|
4
|
+
// Minimal Firebase config for testing — no real project needed; these tests
|
|
5
|
+
// never establish a network connection.
|
|
6
|
+
const testFirebaseConfig = {
|
|
7
|
+
apiKey: 'test-key',
|
|
8
|
+
authDomain: 'test.firebaseapp.com',
|
|
9
|
+
projectId: 'test-project',
|
|
10
|
+
storageBucket: 'test-project.appspot.com',
|
|
11
|
+
messagingSenderId: '000000000000',
|
|
12
|
+
appId: '1:000000000000:web:000000000000',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('cms-core public API smoke tests', () => {
|
|
16
|
+
afterEach(() => TestBed.resetTestingModule());
|
|
17
|
+
|
|
18
|
+
it('provideFolioKit() returns a truthy EnvironmentProviders object', () => {
|
|
19
|
+
const result = provideFolioKit({ firebaseConfig: testFirebaseConfig });
|
|
20
|
+
expect(result).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('ADMIN_EMAIL token is injectable when provided', () => {
|
|
24
|
+
TestBed.configureTestingModule({
|
|
25
|
+
providers: [{ provide: ADMIN_EMAIL, useValue: 'admin@example.com' }],
|
|
26
|
+
});
|
|
27
|
+
const email = TestBed.inject(ADMIN_EMAIL);
|
|
28
|
+
expect(email).toBe('admin@example.com');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('SITE_ID token is optionally injectable — returns null when absent', () => {
|
|
32
|
+
TestBed.configureTestingModule({ providers: [] });
|
|
33
|
+
const siteId = TestBed.inject(SITE_ID, null);
|
|
34
|
+
expect(siteId).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('SITE_ID token is injectable when provided', () => {
|
|
38
|
+
TestBed.configureTestingModule({
|
|
39
|
+
providers: [{ provide: SITE_ID, useValue: 'my-site' }],
|
|
40
|
+
});
|
|
41
|
+
const siteId = TestBed.inject(SITE_ID);
|
|
42
|
+
expect(siteId).toBe('my-site');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin SDK initializer — Node.js only.
|
|
3
|
+
*
|
|
4
|
+
* Credential resolution order:
|
|
5
|
+
* 1. If GOOGLE_APPLICATION_CREDENTIALS contains a JSON object (Firebase App
|
|
6
|
+
* Hosting injects secret values as raw strings, not file paths), parse it
|
|
7
|
+
* directly with admin.credential.cert().
|
|
8
|
+
* 2. Otherwise fall back to Application Default Credentials (ADC), which
|
|
9
|
+
* covers local dev (file-path env var) and Cloud Run managed SA.
|
|
10
|
+
*
|
|
11
|
+
* Never import this module in browser bundles. Import directly in SSR server
|
|
12
|
+
* entry files (e.g. apps/blog/src/server.ts) — it is intentionally excluded
|
|
13
|
+
* from the @foliokit/cms-core barrel export.
|
|
14
|
+
*/
|
|
15
|
+
import admin from 'firebase-admin';
|
|
16
|
+
|
|
17
|
+
export function initAdminApp(): admin.app.App {
|
|
18
|
+
if (!admin.apps.length) {
|
|
19
|
+
const credEnv = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
|
20
|
+
const credential = credEnv?.trimStart().startsWith('{')
|
|
21
|
+
? admin.credential.cert(
|
|
22
|
+
JSON.parse(credEnv) as admin.ServiceAccount,
|
|
23
|
+
)
|
|
24
|
+
: admin.credential.applicationDefault();
|
|
25
|
+
|
|
26
|
+
admin.initializeApp({
|
|
27
|
+
credential,
|
|
28
|
+
projectId: process.env['FIREBASE_PROJECT_ID'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return admin.app();
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import type { FirebaseApp, FirebaseOptions } from 'firebase/app';
|
|
3
|
+
import type { Firestore } from 'firebase/firestore';
|
|
4
|
+
import type { FirebaseStorage } from 'firebase/storage';
|
|
5
|
+
import type { Auth } from 'firebase/auth';
|
|
6
|
+
|
|
7
|
+
export const FIREBASE_OPTIONS = new InjectionToken<FirebaseOptions>('FIREBASE_OPTIONS');
|
|
8
|
+
export const FIREBASE_APP = new InjectionToken<FirebaseApp>('FIREBASE_APP');
|
|
9
|
+
export const FIRESTORE = new InjectionToken<Firestore | null>('FIRESTORE');
|
|
10
|
+
export const FIREBASE_STORAGE = new InjectionToken<FirebaseStorage | null>('FIREBASE_STORAGE');
|
|
11
|
+
export const FIREBASE_AUTH = new InjectionToken<Auth | null>('FIREBASE_AUTH');
|
|
12
|
+
/**
|
|
13
|
+
* The email address that identifies the admin user.
|
|
14
|
+
*
|
|
15
|
+
* Injected by `provideAdminKit({ adminEmail })` and read by
|
|
16
|
+
* `AuthService.isAdmin()` to gate all write operations.
|
|
17
|
+
*
|
|
18
|
+
* **Must match all four authorization surfaces:**
|
|
19
|
+
* 1. The Firebase Authentication account (Google sign-in email)
|
|
20
|
+
* 2. The `adminEmail` passed to `provideAdminKit()` in `app.config.ts`
|
|
21
|
+
* 3. The `isAdmin()` function in `firestore.rules`
|
|
22
|
+
* 4. The author document created by the seed script
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://github.com/dougwilliamson/foliokit/blob/main/docs/security/admin-authorization.md}
|
|
25
|
+
*/
|
|
26
|
+
export const ADMIN_EMAIL = new InjectionToken<string>('ADMIN_EMAIL');
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EnvironmentProviders,
|
|
3
|
+
PLATFORM_ID,
|
|
4
|
+
inject,
|
|
5
|
+
makeEnvironmentProviders,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
8
|
+
import { FirebaseOptions, getApp, getApps, initializeApp } from 'firebase/app';
|
|
9
|
+
import {
|
|
10
|
+
connectFirestoreEmulator,
|
|
11
|
+
getFirestore,
|
|
12
|
+
initializeFirestore,
|
|
13
|
+
memoryLocalCache,
|
|
14
|
+
} from 'firebase/firestore';
|
|
15
|
+
import { connectStorageEmulator, getStorage } from 'firebase/storage';
|
|
16
|
+
import { connectAuthEmulator, getAuth } from 'firebase/auth';
|
|
17
|
+
import {
|
|
18
|
+
FIREBASE_APP,
|
|
19
|
+
FIREBASE_AUTH,
|
|
20
|
+
FIREBASE_OPTIONS,
|
|
21
|
+
FIREBASE_STORAGE,
|
|
22
|
+
FIRESTORE,
|
|
23
|
+
} from './firebase.config';
|
|
24
|
+
|
|
25
|
+
export function provideFirebase(
|
|
26
|
+
options: FirebaseOptions,
|
|
27
|
+
useEmulator = false
|
|
28
|
+
): EnvironmentProviders {
|
|
29
|
+
return makeEnvironmentProviders([
|
|
30
|
+
{
|
|
31
|
+
provide: FIREBASE_OPTIONS,
|
|
32
|
+
useValue: options,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
// Firebase app is a Node.js-process-level singleton. In SSR, each request
|
|
36
|
+
// creates a new DI context but shares the same global Firebase registry, so
|
|
37
|
+
// we must reuse the existing app rather than calling initializeApp again.
|
|
38
|
+
provide: FIREBASE_APP,
|
|
39
|
+
useFactory: () =>
|
|
40
|
+
getApps().length ? getApp() : initializeApp(inject(FIREBASE_OPTIONS)),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
// Same singleton constraint applies to the Firestore instance.
|
|
44
|
+
// initializeFirestore throws if called a second time for the same app, so
|
|
45
|
+
// fall back to getFirestore() which returns the already-configured instance.
|
|
46
|
+
// Client SDK must not run on the server — Admin SDK handles SSR reads.
|
|
47
|
+
provide: FIRESTORE,
|
|
48
|
+
useFactory: () => {
|
|
49
|
+
const platformId = inject(PLATFORM_ID);
|
|
50
|
+
if (!isPlatformBrowser(platformId)) return null;
|
|
51
|
+
const app = inject(FIREBASE_APP);
|
|
52
|
+
try {
|
|
53
|
+
const db = initializeFirestore(app, { localCache: memoryLocalCache(), ignoreUndefinedProperties: true });
|
|
54
|
+
if (useEmulator) {
|
|
55
|
+
connectFirestoreEmulator(db, '127.0.0.1', 8080);
|
|
56
|
+
}
|
|
57
|
+
return db;
|
|
58
|
+
} catch {
|
|
59
|
+
return getFirestore(app);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
provide: FIREBASE_STORAGE,
|
|
65
|
+
useFactory: () => {
|
|
66
|
+
const platformId = inject(PLATFORM_ID);
|
|
67
|
+
if (!isPlatformBrowser(platformId)) return null;
|
|
68
|
+
const storage = getStorage(inject(FIREBASE_APP));
|
|
69
|
+
if (useEmulator) {
|
|
70
|
+
connectStorageEmulator(storage, '127.0.0.1', 9199);
|
|
71
|
+
}
|
|
72
|
+
return storage;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
// Auth relies on browser APIs (IndexedDB, localStorage) — null on server
|
|
77
|
+
provide: FIREBASE_AUTH,
|
|
78
|
+
useFactory: () => {
|
|
79
|
+
const platformId = inject(PLATFORM_ID);
|
|
80
|
+
if (!isPlatformBrowser(platformId)) return null;
|
|
81
|
+
const auth = getAuth(inject(FIREBASE_APP));
|
|
82
|
+
if (useEmulator) {
|
|
83
|
+
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
|
|
84
|
+
}
|
|
85
|
+
return auth;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APP_INITIALIZER,
|
|
3
|
+
EnvironmentProviders,
|
|
4
|
+
InjectionToken,
|
|
5
|
+
PLATFORM_ID,
|
|
6
|
+
inject,
|
|
7
|
+
makeEnvironmentProviders,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
10
|
+
import type { FirebaseOptions } from 'firebase/app';
|
|
11
|
+
import { provideFirebase } from './firebase.providers';
|
|
12
|
+
import { FIREBASE_AUTH } from './firebase.config';
|
|
13
|
+
import { PostService } from '../services/post.service';
|
|
14
|
+
import { SiteConfigService } from '../services/site-config.service';
|
|
15
|
+
import { BLOG_POST_SERVICE } from '../tokens/post-service.token';
|
|
16
|
+
import { SITE_CONFIG_SERVICE } from '../tokens/site-config-service.token';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Injection token for the current site identifier.
|
|
20
|
+
*
|
|
21
|
+
* **Provided by {@link provideFolioKit}** when `siteId` is set in the config.
|
|
22
|
+
* Consumers should inject this token to read the active site ID — do not
|
|
23
|
+
* provide it yourself unless you are bypassing `provideFolioKit()`.
|
|
24
|
+
*
|
|
25
|
+
* Useful for multi-site deployments where a single Firebase project
|
|
26
|
+
* serves several distinct sites. Services like `SiteConfigService` can
|
|
27
|
+
* use this to scope Firestore reads to a specific site document.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // In any component or service — reads the value set by provideFolioKit():
|
|
32
|
+
* readonly siteId = inject(SITE_ID, { optional: true });
|
|
33
|
+
*
|
|
34
|
+
* loadConfig() {
|
|
35
|
+
* const id = this.siteId ?? 'default';
|
|
36
|
+
* return this.siteConfigService.getSiteConfig(id);
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const SITE_ID = new InjectionToken<string>('SITE_ID');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration object accepted by {@link provideFolioKit}.
|
|
44
|
+
*/
|
|
45
|
+
export interface FolioKitConfig {
|
|
46
|
+
/**
|
|
47
|
+
* Firebase project credentials. Obtain from the Firebase Console under
|
|
48
|
+
* Project Settings → Your apps → SDK setup and configuration.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* firebaseConfig: {
|
|
53
|
+
* apiKey: 'AIza...',
|
|
54
|
+
* authDomain: 'my-project.firebaseapp.com',
|
|
55
|
+
* projectId: 'my-project',
|
|
56
|
+
* storageBucket: 'my-project.appspot.com',
|
|
57
|
+
* messagingSenderId: '1234567890',
|
|
58
|
+
* appId: '1:1234567890:web:abc123',
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
firebaseConfig: FirebaseOptions;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Optional site identifier for multi-site deployments.
|
|
66
|
+
*
|
|
67
|
+
* When provided, the value is registered as the {@link SITE_ID} injection
|
|
68
|
+
* token so that services and components can vary behaviour per site without
|
|
69
|
+
* requiring separate Firebase projects.
|
|
70
|
+
*/
|
|
71
|
+
siteId?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional Firebase Authentication tenant ID.
|
|
75
|
+
*
|
|
76
|
+
* When provided, `auth.tenantId` is set on the Auth instance during app
|
|
77
|
+
* initialisation via `APP_INITIALIZER`. Required for Google Cloud Identity
|
|
78
|
+
* Platform multi-tenant projects. Has no effect in SSR contexts where Auth
|
|
79
|
+
* is null.
|
|
80
|
+
*
|
|
81
|
+
* @see https://firebase.google.com/docs/auth/web/multi-tenancy
|
|
82
|
+
*/
|
|
83
|
+
tenantId?: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, all Firebase services connect to the local emulator suite
|
|
87
|
+
* instead of production endpoints:
|
|
88
|
+
* - Firestore → `127.0.0.1:8080`
|
|
89
|
+
* - Storage → `127.0.0.1:9199`
|
|
90
|
+
* - Auth → `http://127.0.0.1:9099`
|
|
91
|
+
*
|
|
92
|
+
* @default false
|
|
93
|
+
*/
|
|
94
|
+
useEmulator?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Bootstrap FolioKit in a single call.
|
|
99
|
+
*
|
|
100
|
+
* Registers all Firebase services (app, Firestore, Storage, Auth), binds the
|
|
101
|
+
* default {@link PostService} and {@link SiteConfigService} implementations to
|
|
102
|
+
* their public tokens, and optionally stores `siteId` / `tenantId` config.
|
|
103
|
+
*
|
|
104
|
+
* Use this instead of `provideFirebase()` unless you need custom service
|
|
105
|
+
* implementations — in which case call `provideFirebase()` directly and
|
|
106
|
+
* provide your own service aliases.
|
|
107
|
+
*
|
|
108
|
+
* **Overriding the default service bindings**
|
|
109
|
+
*
|
|
110
|
+
* The last provider wins in Angular's DI system. Add your own binding *after*
|
|
111
|
+
* `provideFolioKit()` in the providers array to override the defaults:
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* providers: [
|
|
115
|
+
* provideFolioKit({ firebaseConfig: environment.firebase }),
|
|
116
|
+
* { provide: BLOG_POST_SERVICE, useExisting: MyCustomPostService },
|
|
117
|
+
* ]
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @param config - FolioKit configuration including Firebase credentials and
|
|
121
|
+
* optional site/tenant identifiers.
|
|
122
|
+
* @returns An `EnvironmentProviders` token suitable for use in
|
|
123
|
+
* `ApplicationConfig.providers` or `bootstrapApplication`.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* // app.config.ts
|
|
128
|
+
* export const appConfig: ApplicationConfig = {
|
|
129
|
+
* providers: [
|
|
130
|
+
* provideRouter(routes),
|
|
131
|
+
* provideAnimationsAsync(),
|
|
132
|
+
* provideHttpClient(withFetch()),
|
|
133
|
+
* provideMarkdown(),
|
|
134
|
+
* provideFolioKit({
|
|
135
|
+
* firebaseConfig: environment.firebase,
|
|
136
|
+
* useEmulator: environment.useEmulator,
|
|
137
|
+
* }),
|
|
138
|
+
* ],
|
|
139
|
+
* };
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export function provideFolioKit(config: FolioKitConfig): EnvironmentProviders {
|
|
143
|
+
const providers: Parameters<typeof makeEnvironmentProviders>[0] = [
|
|
144
|
+
// Firebase services: app, Firestore, Storage, Auth (SSR-safe).
|
|
145
|
+
provideFirebase(config.firebaseConfig, config.useEmulator ?? false),
|
|
146
|
+
|
|
147
|
+
// Default concrete service implementations bound to their public tokens.
|
|
148
|
+
// Override by re-providing the token after this call in the same array.
|
|
149
|
+
{ provide: BLOG_POST_SERVICE, useExisting: PostService },
|
|
150
|
+
{ provide: SITE_CONFIG_SERVICE, useExisting: SiteConfigService },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Optionally expose siteId as an injectable constant.
|
|
154
|
+
if (config.siteId !== undefined) {
|
|
155
|
+
providers.push({ provide: SITE_ID, useValue: config.siteId });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optionally apply a Firebase Auth tenant ID for multi-tenant projects.
|
|
159
|
+
// Uses APP_INITIALIZER to run after the Auth instance is created.
|
|
160
|
+
if (config.tenantId !== undefined) {
|
|
161
|
+
const tenantId = config.tenantId;
|
|
162
|
+
providers.push({
|
|
163
|
+
provide: APP_INITIALIZER,
|
|
164
|
+
useFactory: () => {
|
|
165
|
+
const platformId = inject(PLATFORM_ID);
|
|
166
|
+
const auth = inject(FIREBASE_AUTH);
|
|
167
|
+
return () => {
|
|
168
|
+
if (isPlatformBrowser(platformId) && auth) {
|
|
169
|
+
auth.tenantId = tenantId;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
multi: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return makeEnvironmentProviders(providers);
|
|
178
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SocialLink } from './site-config.model';
|
|
2
|
+
|
|
3
|
+
export interface Author {
|
|
4
|
+
id: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
bio?: string;
|
|
7
|
+
photoUrl?: string;
|
|
8
|
+
/** Firebase Storage URL — shown in dark mode when set */
|
|
9
|
+
photoUrlDark?: string;
|
|
10
|
+
socialLinks?: SocialLink[];
|
|
11
|
+
email?: string;
|
|
12
|
+
/** Unix milliseconds. */
|
|
13
|
+
createdAt: number;
|
|
14
|
+
/** Unix milliseconds. */
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SeoMeta {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
keywords?: string[];
|
|
5
|
+
ogImage?: string;
|
|
6
|
+
canonicalUrl?: string;
|
|
7
|
+
noIndex?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EmbeddedMediaEntry {
|
|
11
|
+
token: string;
|
|
12
|
+
storagePath: string;
|
|
13
|
+
downloadUrl: string;
|
|
14
|
+
alt: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BlogPost {
|
|
19
|
+
id: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
title: string;
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
status: 'published' | 'draft' | 'scheduled' | 'archived';
|
|
24
|
+
content: string;
|
|
25
|
+
excerpt?: string;
|
|
26
|
+
thumbnailUrl?: string;
|
|
27
|
+
thumbnailAlt?: string;
|
|
28
|
+
tags: string[];
|
|
29
|
+
authorId?: string;
|
|
30
|
+
readingTimeMinutes?: number;
|
|
31
|
+
embeddedMedia: Record<string, EmbeddedMediaEntry>;
|
|
32
|
+
seo: SeoMeta;
|
|
33
|
+
/** Unix milliseconds. Stored as Firestore Timestamp but always normalized on read. */
|
|
34
|
+
publishedAt: number;
|
|
35
|
+
/** Unix milliseconds, optional. */
|
|
36
|
+
scheduledPublishAt?: number;
|
|
37
|
+
/** Unix milliseconds. */
|
|
38
|
+
updatedAt: number;
|
|
39
|
+
/** Unix milliseconds. */
|
|
40
|
+
createdAt: number;
|
|
41
|
+
}
|