5htp-core 0.4.5 → 0.4.6-2

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/client/assets/css/components/button.less +4 -3
  3. package/src/client/assets/css/components/lists.less +12 -18
  4. package/src/client/assets/css/text/icons.less +5 -5
  5. package/src/client/assets/css/text/text.less +3 -15
  6. package/src/client/assets/css/text/titres.less +1 -1
  7. package/src/client/assets/css/theme.less +2 -2
  8. package/src/client/components/Dialog/Manager.tsx +3 -1
  9. package/src/client/components/Form.ts +23 -24
  10. package/src/client/components/chart/base.tsx +1 -1
  11. package/src/client/components/containers/Scrollbar/index.less +1 -1
  12. package/src/client/components/data/progressbar/index.less +1 -1
  13. package/src/client/components/data/progressbar/index.tsx +3 -3
  14. package/src/client/components/input/Slider/index.less +1 -1
  15. package/src/client/components/inputv3/index.tsx +6 -1
  16. package/src/client/services/router/components/Page.tsx +12 -14
  17. package/src/client/services/router/index.tsx +15 -2
  18. package/src/client/services/router/request/api.ts +3 -2
  19. package/src/common/data/markdown.ts +9 -0
  20. package/src/common/router/request/index.ts +4 -1
  21. package/src/server/app/index.ts +0 -11
  22. package/src/server/services/auth/index.ts +86 -47
  23. package/src/server/services/auth/router/request.ts +9 -5
  24. package/src/server/services/database/index.ts +18 -13
  25. package/src/server/services/disks/driver.ts +5 -0
  26. package/src/server/services/disks/drivers/local/index.ts +7 -0
  27. package/src/server/services/disks/drivers/s3/index.ts +10 -0
  28. package/src/server/services/router/index.ts +2 -1
  29. package/src/server/services/router/response/index.ts +5 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.4.5",
4
+ "version": "0.4.6-2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  //transition: all .1s linear;
42
42
 
43
43
  > i {
44
- color: var(--cAccent)
44
+ color: var(--cTxtAccent)
45
45
  }
46
46
  }
47
47
 
@@ -352,7 +352,8 @@ ul.col {
352
352
  top: @sizeComponent;
353
353
  margin-top: @spacing / 2;
354
354
 
355
- &::before {
355
+ // C'est quoi ?
356
+ /*&::before {
356
357
  content: ' ';
357
358
  display: block;
358
359
  position: absolute;
@@ -362,7 +363,7 @@ ul.col {
362
363
  right: calc(0em - @spacing);
363
364
 
364
365
  height: 100%;
365
- }
366
+ }*/
366
367
 
367
368
  }
368
369
  }
@@ -2,7 +2,7 @@
2
2
  - CONFIG
3
3
  ----------------------------------*/
4
4
 
5
- @sizeStep: 2.2em;
5
+ @sizeStep: 1.8em;
6
6
  @itemLineHeight: 1.5em;
7
7
 
8
8
  /*----------------------------------
@@ -22,8 +22,10 @@ ul, ol {
22
22
 
23
23
  ul.liste,
24
24
  ol.steps {
25
- // Doit hériter des parents
26
- //line-height: 1.5em;
25
+
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 0.5em;
27
29
  text-align: left;
28
30
 
29
31
  > li {
@@ -48,7 +50,7 @@ ul.liste {
48
50
  &:before {
49
51
  content: '➜';
50
52
  font-size: 1.4em;
51
- color: var(--cAccent);
53
+ color: var(--cTxtAccent);
52
54
  }
53
55
  }
54
56
 
@@ -87,17 +89,12 @@ ol.steps {
87
89
 
88
90
  &:before {
89
91
  content: counter(step-counter);
90
- font-size: 0.8em;
91
- }
92
-
93
- &.active::before {
94
- background-color: var(--cAccent);
95
92
  }
96
93
 
97
94
  // Stepnumber = at the left
98
95
  &:not(.col) {
99
96
 
100
- padding-left: @sizeStep + (@spacing / 2);
97
+ padding-left: /*@sizeStep + */(@spacing);
101
98
  padding-top: (@sizeStep - @itemLineHeight) / 2;
102
99
  line-height: @itemLineHeight;
103
100
  margin: 5px 0;
@@ -122,15 +119,12 @@ ol.steps {
122
119
 
123
120
  ol.steps > li:before,
124
121
  strong.step {
125
-
126
- background: @c1 + #666;
127
- color: @c1;
128
-
122
+ color: var(--cTxtAccent);
129
123
  text-align: center;
130
- width: @sizeStep * 1.2;
131
- height: @sizeStep * 1.2;
132
- line-height: @sizeStep * 1.2;
133
- border-radius: 50%;
124
+ font-weight: bold;
125
+
126
+ font-size: 1em;
127
+ line-height: 1.8em;
134
128
  }
135
129
 
136
130
  /*----------------------------------
@@ -32,7 +32,7 @@ i {
32
32
  }
33
33
 
34
34
  &.solid {
35
- color: var(--cAccent2);
35
+ color: var(--cTxtAccent2);
36
36
  background: var(--cBg);
37
37
  border-radius: @radius;
38
38
 
@@ -74,10 +74,10 @@ i.logo {
74
74
  line-height: 3.2em;
75
75
  flex: 0 0 3.2em;*/
76
76
 
77
- width: 2em;
78
- height: 2em;
79
- line-height: 2em;
80
- flex: 0 0 2em;
77
+ width: @sizeComponent * 0.9;
78
+ height: @sizeComponent * 0.9;
79
+ line-height: @sizeComponent * 0.9;
80
+ flex: 0 0 @sizeComponent * 0.9;
81
81
 
82
82
  border-radius: @radius;
83
83
  border: none; // For img``
@@ -122,7 +122,7 @@ em {
122
122
  display: block;
123
123
  height: 0.5em;
124
124
  border-radius: 0.25em;
125
- background: var(--cAccent2);
125
+ background: var(--cTxtAccent2);
126
126
 
127
127
  position: absolute;
128
128
  z-index: -1;
@@ -148,22 +148,9 @@ pre {
148
148
  @readingMargin: 2.4rem;
149
149
  .reading {
150
150
 
151
- --cTxtBase: #555;
152
-
153
151
  display: flex;
154
152
  flex-direction: column;
155
153
  gap: 1em;
156
-
157
- p,
158
- figcaption,
159
- h2,
160
- h3,
161
- > ul {
162
- max-width: var(--focusWidth);
163
- margin: 0 auto;
164
- width: 100%;
165
- //padding: 0 @readingMargin;
166
- }
167
154
 
168
155
  &,
169
156
  p {
@@ -175,7 +162,7 @@ pre {
175
162
 
176
163
  h2, h3, h4 {
177
164
  text-align: left;
178
- margin: 1em 0;
165
+ margin: 0.5em 0;
179
166
  }
180
167
 
181
168
  h2 {
@@ -215,6 +202,7 @@ pre {
215
202
  cursor: default;
216
203
  display: block;
217
204
  margin: 0 auto;
205
+ border-radius: @radius;
218
206
  }
219
207
 
220
208
  figcaption {
@@ -17,7 +17,7 @@ h3 {
17
17
  }
18
18
 
19
19
  strong {
20
- color: var(--cAccent);
20
+ color: var(--cTxtAccent);
21
21
  font-weight: inherit;
22
22
 
23
23
  .bg.img & {
@@ -50,10 +50,10 @@
50
50
 
51
51
  // Accent
52
52
  & when (@theme[accent1]) {
53
- --cAccent: @theme[accent1];
53
+ --cTxtAccent: @theme[accent1];
54
54
  }
55
55
  & when (@theme[accent2]) {
56
- --cAccent2: @theme[accent2];
56
+ --cTxtAccent2: @theme[accent2];
57
57
  }
58
58
 
59
59
  // Lines
@@ -143,7 +143,9 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
143
143
 
144
144
  if (!isToast)
145
145
  render = (
146
- <div class="modal">
146
+ <div class="modal" onClick={e =>
147
+ e.target.classList.contains('modal') && close(false)
148
+ }>
147
149
  {render}
148
150
  </div>
149
151
  )
@@ -36,7 +36,6 @@ export type Form<TFormData extends {} = {}> = {
36
36
  fields: FieldsAttrs<TFormData>,
37
37
  data: TFormData,
38
38
  options: TFormOptions<TFormData>,
39
- autosavedData?: Partial<TFormData>,
40
39
 
41
40
  // Actions
42
41
  validate: (data: Partial<TFormData>) => TValidationResult<{}>,
@@ -57,7 +56,7 @@ type FormState = {
57
56
  ----------------------------------*/
58
57
  export default function useForm<TFormData extends {}>(
59
58
  schema: Schema<TFormData>,
60
- options: TFormOptions<TFormData>
59
+ options: TFormOptions<TFormData> = {}
61
60
  ): [ Form, FieldsAttrs<TFormData> ] {
62
61
 
63
62
  const context = useContext();
@@ -66,31 +65,18 @@ export default function useForm<TFormData extends {}>(
66
65
  - INIT
67
66
  ----------------------------------*/
68
67
 
69
- // Autosaving data
70
- let autosavedData: TFormData | undefined;
71
- if (options.autoSave && typeof window !== 'undefined') {
72
- const autosaved = localStorage.getItem('form.' + options.autoSave.id);
73
- if (autosaved !== null) {
74
- try {
75
- console.log('[form] Parse autosaved from json:', autosaved);
76
- autosavedData = JSON.parse(autosaved);
77
- } catch (error) {
78
- console.error('[form] Failed to decode autosaved data from json:', autosaved);
79
- }
80
- }
81
- }
68
+ const [state, setState] = React.useState<FormState>({
69
+ hasChanged: options.data !== undefined,
70
+ isLoading: false,
71
+ errorsCount: 0,
72
+ errors: {}
73
+ });
82
74
 
83
75
  const initialData: Partial<TFormData> = options.data || {};
84
76
 
85
77
  // States
86
78
  const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
87
79
  const [data, setData] = React.useState< Partial<TFormData> >(initialData);
88
- const [state, setState] = React.useState<FormState>({
89
- hasChanged: false,
90
- isLoading: false,
91
- errorsCount: 0,
92
- errors: {}
93
- });
94
80
 
95
81
  // Validate data when it changes
96
82
  React.useEffect(() => {
@@ -99,8 +85,22 @@ export default function useForm<TFormData extends {}>(
99
85
  validate(data, false);
100
86
 
101
87
  // Autosave
102
- if (options.autoSave !== undefined)
103
- saveLocally(data, options.autoSave.id);
88
+ if (options.autoSave !== undefined) {
89
+
90
+ if (state.hasChanged)
91
+ saveLocally(data, options.autoSave.id);
92
+ else {
93
+ const autosaved = localStorage.getItem('form.' + options.autoSave.id);
94
+ if (autosaved !== null) {
95
+ try {
96
+ console.log('[form] Parse autosaved from json:', autosaved);
97
+ setData( JSON.parse(autosaved) );
98
+ } catch (error) {
99
+ console.error('[form] Failed to decode autosaved data from json:', autosaved);
100
+ }
101
+ }
102
+ }
103
+ }
104
104
 
105
105
  }, [data]);
106
106
 
@@ -235,7 +235,6 @@ export default function useForm<TFormData extends {}>(
235
235
  validate,
236
236
  submit,
237
237
  options,
238
- autosavedData,
239
238
  ...state
240
239
  }
241
240
 
@@ -382,7 +382,7 @@ export default function <TDonnee extends DonneesGraph, TTypeChartJs extends Char
382
382
 
383
383
  // Default color
384
384
  if (!dataset.colonne.color)
385
- dataset.colonne.color = css.getPropertyValue('--cAccent');
385
+ dataset.colonne.color = css.getPropertyValue('--cTxtAccent');
386
386
 
387
387
  return {
388
388
  label: dataset.colonne.label,
@@ -70,7 +70,7 @@
70
70
  }
71
71
 
72
72
  &.dragging {
73
- background: var(--cAccent);
73
+ background: var(--cTxtAccent);
74
74
  box-shadow: 0 0 8.3rem var(--cPrincipale);
75
75
  }
76
76
  }
@@ -14,7 +14,7 @@ div.progressbar {
14
14
  }
15
15
 
16
16
  > .progress {
17
- --cBg: var(--cAccent);
17
+ --cBg: var(--cTxtAccent);
18
18
  background: var(--cBg);
19
19
  width: 0%;
20
20
  max-width: 100%;
@@ -39,9 +39,9 @@ const hsl = (h: number, s: number = 80, l: number = 70) => `hsl(${Math.floor(h)}
39
39
  export const couleurViaPc = (pc: number, couleurs: [number, number], ecart: number) => {
40
40
 
41
41
  return {
42
- couleur1: 'var(--cAccent)',
43
- couleur2: 'var(--cAccent)',
44
- couleurTxt: 'var(--cAccent)'
42
+ couleur1: 'var(--cTxtAccent)',
43
+ couleur2: 'var(--cTxtAccent)',
44
+ couleurTxt: 'var(--cTxtAccent)'
45
45
  }
46
46
  const [couleurMin, couleurMax] = couleurs;
47
47
  const couleur = couleurMin + (pc * (couleurMax - couleurMin));
@@ -15,7 +15,7 @@
15
15
  height: @hTrack;
16
16
 
17
17
  &-0 {
18
- background: var(--cAccent);
18
+ background: var(--cTxtAccent);
19
19
  border-radius: @hTrack / 2 0 0 @hTrack / 2;
20
20
  }
21
21
 
@@ -39,6 +39,11 @@ export type Props = {
39
39
  validator?: ReturnType< SchemaValidators["number"] >,
40
40
  })
41
41
 
42
+ type TInputElementProps = Omit<(
43
+ JSX.HTMLAttributes<HTMLInputElement> &
44
+ JSX.HTMLAttributes<HTMLTextAreaElement>
45
+ ), 'onChange'>
46
+
42
47
  /*----------------------------------
43
48
  - COMPOSANT
44
49
  ----------------------------------*/
@@ -52,7 +57,7 @@ export default ({
52
57
  // Actions
53
58
  onPressEnter,
54
59
  ...props
55
- }: Props & InputBaseProps<string> & Omit<JSX.HTMLAttributes<HTMLInputElement>, 'onChange'>) => {
60
+ }: Props & InputBaseProps<string> & TInputElementProps) => {
56
61
 
57
62
  /*----------------------------------
58
63
  - INIT
@@ -21,14 +21,20 @@ export default ({ page }: { page: Page }) => {
21
21
  ----------------------------------*/
22
22
  const context = useContext();
23
23
 
24
- // Temporary fix: context.page may not be updated at this stage
25
- // Seems to be the case when we change page, but still same page component with different data
26
- context.page = page;
27
-
28
24
  // Bind data
29
25
  const [apiData, setApiData] = React.useState<{[k: string]: any} | null>( page.data || {});
30
26
  page.setAllData = setApiData;
31
- context.data = apiData;
27
+ const fullData = {
28
+ ...context.data,
29
+ ...apiData
30
+ }
31
+
32
+ // Temporary fix: context.page may not be updated at this stage
33
+ // Seems to be the case when we change page, but still same page component with different data
34
+ // TODO: ensure these updated are made every tume we change page / context
35
+ context.page = page;
36
+ context.data = fullData;
37
+ context.context = context;
32
38
 
33
39
  // Page component has not changed, but data were updated (ex: url parameters change)
34
40
  React.useEffect(() => {
@@ -43,15 +49,7 @@ export default ({ page }: { page: Page }) => {
43
49
  // Make request parameters and api data accessible from the page component
44
50
  return page.renderer ? (
45
51
 
46
- <page.renderer
47
- // Services
48
- {...context}
49
- // API data & URL params
50
- data={{
51
- ...apiData,
52
- ...context.request.data
53
- }}
54
- />
52
+ <page.renderer {...context} />
55
53
 
56
54
  ) : <>Renderer missing</>
57
55
  }
@@ -23,6 +23,7 @@ import { getLayout } from '@common/router/layouts';
23
23
  import { getRegisterPageArgs, buildRegex } from '@common/router/register';
24
24
  import { TFetcherList } from '@common/router/request/api';
25
25
  import type { TFrontRenderer } from '@common/router/response/page';
26
+ import Button from '../../components/button';
26
27
 
27
28
  import App from '@client/app/component';
28
29
  import type ClientApplication from '@client/app';
@@ -359,8 +360,20 @@ export default class ClientRouter<
359
360
 
360
361
  } catch (e) {
361
362
  console.error(`Failed to fetch the route ${route.chunk}`, e);
362
- this.app.handleError(new Error("Failed to load content. Please reload the page and try again."));
363
- throw e;
363
+ try {
364
+ this.context.modal.show(() => (
365
+ <div class="card col bg white w-3">
366
+ <h2>New Update Available!</h2>
367
+ <p>
368
+ A new version of the website is available. Please refresh the page to continue.
369
+ </p>
370
+ <Button type="primary" onClick={() => window.location.reload()}>
371
+ Reload
372
+ </Button>
373
+ </div>
374
+ ));
375
+ } catch (error) {}
376
+ throw new Error("A new version of the website is available. Please refresh the page.");
364
377
  }
365
378
 
366
379
  } else {
@@ -238,8 +238,9 @@ export default class ApiClient implements ApiClientService {
238
238
  const error = errorFromJson(errorData);
239
239
  throw error;
240
240
  }
241
- debug && console.log(`[api] Success:`, response);
242
- return response.json() as Promise<TData>;
241
+ const json = await response.json() as TData;
242
+ debug && console.log(`[api] Success:`, json);
243
+ return json;
243
244
  })
244
245
  .catch((error) => {
245
246
  if (error instanceof TypeError) {
@@ -56,6 +56,15 @@ md.block.ruler.after('list', 'test', (state, startLine, endLine, silent) => {
56
56
  token.attrs[ aIndex ][1] = 'liste'; // replace value of existing attr
57
57
  }
58
58
 
59
+ } else if (token.type === 'ordered_list_open') {
60
+
61
+ const aIndex = token.attrIndex('class');
62
+ if (aIndex < 0) {
63
+ token.attrPush(['class', 'steps']); // add new attribute
64
+ } else {
65
+ token.attrs[ aIndex ][1] = 'steps'; // replace value of existing attr
66
+ }
67
+
59
68
  }
60
69
  }
61
70
 
@@ -5,6 +5,9 @@
5
5
  // Core
6
6
  import Response from '../response';
7
7
 
8
+ // Types
9
+ import type { TBasicUser } from '@server/services/auth';
10
+
8
11
  /*----------------------------------
9
12
  - TYPES
10
13
  ----------------------------------*/
@@ -22,7 +25,7 @@ export default abstract class BaseRequest {
22
25
 
23
26
  public data: TObjetDonnees = {};
24
27
  public abstract response?: Response;
25
- public user: User | null = null;
28
+ public user: TBasicUser | null = null;
26
29
 
27
30
  public constructor(
28
31
  public path: string,
@@ -37,17 +37,6 @@ type Hooks = {
37
37
  }
38
38
  }
39
39
 
40
- declare global {
41
-
42
- //interface Services { }
43
-
44
- interface AppHooks {
45
-
46
- }
47
-
48
- interface User { }
49
- }
50
-
51
40
  export const Service = ServicesContainer;
52
41
 
53
42
  /*----------------------------------
@@ -62,13 +62,24 @@ export type TServices = {
62
62
 
63
63
  }
64
64
 
65
+ export type TBasicUser = {
66
+ type: string,
67
+ name: string,
68
+ roles: string[]
69
+ }
70
+
71
+ export type TBasicJwtSession = {
72
+ accountType: string,
73
+ apiKey?: string
74
+ }
75
+
65
76
  /*----------------------------------
66
77
  - SERVICE
67
78
  ----------------------------------*/
68
79
  export default abstract class AuthService<
69
- TUser extends {},
80
+ TUser extends TBasicUser,
70
81
  TApplication extends Application,
71
- TJwtSession extends {} = {},
82
+ TJwtSession extends TBasicJwtSession = TBasicJwtSession,
72
83
  TRequest extends ServerRequest<Router> = ServerRequest<Router>,
73
84
  > extends Service<TConfig, THooks, TApplication, TServices> {
74
85
 
@@ -95,51 +106,37 @@ export default abstract class AuthService<
95
106
  public abstract login( ...args: any[] ): Promise<{ user: TUser, token: string }>;
96
107
  public abstract decodeSession( jwt: TJwtSession, req: THttpRequest ): Promise<TUser | null>;
97
108
 
98
- protected abstract displayName(user?: TUser | null): string;
99
109
  protected abstract displaySessionName(session: TJwtSession): string;
100
110
 
111
+ // https://beeceptor.com/docs/concepts/authorization-header/#examples
101
112
  public async decode( req: THttpRequest, withData: true ): Promise<TUser | null>;
102
113
  public async decode( req: THttpRequest, withData?: false ): Promise<TJwtSession | null>;
103
114
  public async decode( req: THttpRequest, withData: boolean = false ): Promise<TJwtSession | TUser | null> {
104
115
 
105
116
  this.config.debug && console.log(LogPrefix, 'Decode:', { cookie: req.cookies['authorization'] });
106
117
 
107
- let token: string | undefined;
108
- if (('cookies' in req) && typeof req.cookies['authorization'] === 'string')
109
- token = req.cookies['authorization'];
110
- // Desktop app webview do not support cookie config, so wwe retrieve it from headers
111
- else if (typeof req.headers['authorization'] === 'string')
112
- token = req.headers['authorization'];
118
+ // Get auth token
119
+ const authMethod = this.getAuthMethod(req);
120
+ if (authMethod === null)
121
+ return null;
122
+ const { tokenType, token } = authMethod;
113
123
 
114
- if (token === undefined)
115
- return this.unauthorized(req);
116
-
117
- let session: TJwtSession;
118
- try {
119
- session = jwt.verify(token, this.config.jwt.key, {
120
- maxAge: this.config.jwt.expiration
121
- });
122
- } catch (error) {
123
- console.warn(LogPrefix, "Failed to decode jwt token:", token);
124
- return this.unauthorized(req);
125
- }
124
+ // Get auth session
125
+ const session = this.getAuthSession(tokenType, token);
126
126
 
127
127
  // Return email only
128
- const sessionName = this.displaySessionName(session);
129
128
  if (!withData) {
130
- this.config.debug && console.log(LogPrefix, `Auth user ${sessionName} successfull. Return email only`);
129
+ this.config.debug && console.log(LogPrefix, `Auth user successfull. Return email only`);
131
130
  return session;
132
131
  }
133
132
 
134
133
  // Deserialize full user data
135
- this.config.debug && console.log(LogPrefix, `Deserialize user ${sessionName}`);
134
+ this.config.debug && console.log(LogPrefix, `Deserialize user`, session);
136
135
  const user = await this.decodeSession(session, req);
137
-
138
- // User not found
139
136
  if (user === null)
140
137
  return null;
141
138
 
142
- this.config.debug && console.log(LogPrefix, `Deserialized user ${sessionName}:`, this.displayName(user));
139
+ this.config.debug && console.log(LogPrefix, `Deserialized user:`, user.name);
143
140
 
144
141
  return {
145
142
  ...user,
@@ -147,15 +144,55 @@ export default abstract class AuthService<
147
144
  };
148
145
  }
149
146
 
150
- public unauthorized( req: THttpRequest ) {
147
+ private getAuthMethod( req: THttpRequest ): null | { token: string, tokenType?: string } {
151
148
 
152
- if ('res' in req) {
153
- // If use auth failed, we remove the jwt token so we avoid to trigger the same auth error in the next request
154
- console.warn(LogPrefix, "Auth failed: remove authorization cookie");
155
- req.res?.clearCookie('authorization');
156
- }
149
+ let token: string | undefined;
150
+ let tokenType: string | undefined;
151
+ if (typeof req.headers['authorization'] === 'string') {
152
+
153
+ ([ tokenType, token ] = req.headers['authorization'].split(' '));
157
154
 
158
- return null;
155
+ } else if (('cookies' in req) && typeof req.cookies['authorization'] === 'string') {
156
+
157
+ token = req.cookies['authorization'];
158
+ tokenType = 'Bearer';
159
+
160
+ } else
161
+ return null;
162
+
163
+ if (token === undefined)
164
+ return null;
165
+
166
+ return { tokenType, token };
167
+ }
168
+
169
+ private getAuthSession( tokenType: string | undefined, token: string ): TJwtSession {
170
+
171
+ let session: TJwtSession;
172
+
173
+ // API Key
174
+ if (tokenType === 'Apikey') {
175
+
176
+ const [accountType] = token.split('-');
177
+
178
+ this.config.debug && console.log(LogPrefix, `Auth via API Key`, token);
179
+ session = { accountType, apiKey: token } as TJwtSession;
180
+
181
+ // JWT
182
+ } else if (tokenType === 'Bearer') {
183
+ this.config.debug && console.log(LogPrefix, `Auth via JWT token`, token);
184
+ try {
185
+ session = jwt.verify(token, this.config.jwt.key, {
186
+ maxAge: this.config.jwt.expiration
187
+ });
188
+ } catch (error) {
189
+ console.warn(LogPrefix, "Failed to decode jwt token:", token);
190
+ throw new Forbidden(`The JWT token provided in the Authorization header is invalid`);
191
+ }
192
+ } else
193
+ throw new InputError(`The authorization scheme provided in the Authorization header is unsupported.`);
194
+
195
+ return session;
159
196
  }
160
197
 
161
198
  public createSession( session: TJwtSession, request: TRequest ): string {
@@ -176,27 +213,24 @@ export default abstract class AuthService<
176
213
  const user = request.user;
177
214
  if (!user) return;
178
215
 
179
- this.config.debug && console.info(LogPrefix, `Logout ${this.displayName(user)}`);
216
+ this.config.debug && console.info(LogPrefix, `Logout ${user.name}`);
180
217
  request.res.clearCookie('authorization');
181
218
  }
182
219
 
183
- public check( request: TRequest, role: TUserRole, motivation?: string): TUser;
184
- public check( request: TRequest, role: false, motivation?: string): null;
185
- public check( request: TRequest, role: TUserRole | boolean = 'USER', motivation?: string): TUser | null {
220
+ public check( request: TRequest, entity: string, role: TUserRole, motivation?: string): TUser;
221
+ public check( request: TRequest, entity: string, role: false, motivation?: string): null;
222
+ public check( request: TRequest, entity: string, role: TUserRole | false = 'USER', motivation?: string): TUser | null {
186
223
 
187
224
  const user = request.user;
188
225
 
189
- this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, this.displayName(user));
226
+ this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user?.name);
227
+
228
+ console.log({ entity, role, motivation });
190
229
 
191
230
  if (user === undefined) {
192
231
 
193
232
  throw new Error(`request.user has not been decoded.`);
194
233
 
195
- // Shortcut: { auth: true } <=> { auth: 'USER' }
196
- } else if (role === true) {
197
-
198
- role = 'USER';
199
-
200
234
  // No auth needed
201
235
  } else if (role === false) {
202
236
 
@@ -206,18 +240,23 @@ export default abstract class AuthService<
206
240
  } else if (user === null) {
207
241
 
208
242
  this.config.debug && console.warn(LogPrefix, "Refusé pour anonyme (" + request.ip + ")");
209
- throw new AuthRequired(motivation);
243
+ throw new AuthRequired('Please login to continue');
244
+
245
+ } else if (user.type !== entity) {
246
+
247
+ this.config.debug && console.warn(LogPrefix, `User type mismatch: ${user.type} (user) vs ${entity} (expected) (${request.ip})`);
248
+ throw new AuthRequired("Your account type doesn't have access to the requested content.");
210
249
 
211
250
  // Insufficient permissions
212
251
  } else if (!user.roles.includes(role)) {
213
252
 
214
- console.warn(LogPrefix, "Refusé: " + role + " pour " + this.displayName(user) + " (" + (user.roles ? user.roles.join(', ') : 'role inconnu') + ")");
253
+ console.warn(LogPrefix, "Refusé: " + role + " pour " + user.name + " (" + (user.roles ? user.roles.join(', ') : 'role inconnu') + ")");
215
254
 
216
255
  throw new Forbidden("You do not have sufficient permissions to access this resource.");
217
256
 
218
257
  } else {
219
258
 
220
- console.warn(LogPrefix, "Autorisé " + role + " pour " + this.displayName(user) + " (" + user.roles.join(', ') + ")");
259
+ console.warn(LogPrefix, "Autorisé " + role + " pour " + user.name + " (" + user.roles.join(', ') + ")");
221
260
 
222
261
  }
223
262
 
@@ -14,6 +14,9 @@ import { InputError, AuthRequired, Forbidden } from '@common/errors';
14
14
  import type AuthenticationRouterService from '.';
15
15
  import type { default as UsersManagementService, TUserRole } from '..';
16
16
 
17
+ // Types
18
+ import type { TBasicUser } from '@server/services/auth';
19
+
17
20
  /*----------------------------------
18
21
  - TYPES
19
22
  ----------------------------------*/
@@ -22,7 +25,7 @@ import type { default as UsersManagementService, TUserRole } from '..';
22
25
  - MODULE
23
26
  ----------------------------------*/
24
27
  export default class UsersRequestService<
25
- TUser extends {}
28
+ TUser extends TBasicUser
26
29
  > extends RequestService {
27
30
 
28
31
  public constructor(
@@ -41,9 +44,10 @@ export default class UsersRequestService<
41
44
  return this.users.logout( this.request );
42
45
  }
43
46
 
44
- public check( role: TUserRole, motivation?: string): TUser;
45
- public check( role: false, motivation?: string): null;
46
- public check( role: TUserRole | boolean = 'USER', motivation?: string): TUser | null {
47
- return this.users.check( this.request, role, motivation );
47
+ // TODO: return user type according to entity
48
+ public check( entity: string, role: TUserRole, motivation?: string): TUser;
49
+ public check( entity: string, role: false, motivation?: string): null;
50
+ public check( entity: string, role: TUserRole | boolean = 'USER', motivation?: string): TUser | null {
51
+ return this.users.check( this.request, entity, role, motivation );
48
52
  }
49
53
  }
@@ -442,7 +442,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
442
442
  // Upsert
443
443
  let upsertStatement: string = '';
444
444
  if (opts.upsert !== undefined)
445
- upsertStatement = ' ' + this.buildUpsertStatement<TData>(table, opts as With<TInsertQueryOptions<TData>, 'upsert'>);
445
+ upsertStatement = ' ' + this.buildUpsertStatement<TData>(table, data, opts as With<TInsertQueryOptions<TData>, 'upsert'>);
446
446
 
447
447
  let okPacket: mysql.OkPacket = { ...emptyOkPacket }
448
448
 
@@ -501,10 +501,11 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
501
501
 
502
502
  private buildUpsertStatement<TData extends TObjetDonnees>(
503
503
  table: TMetasTable,
504
+ data: TData[],
504
505
  opts: With<TInsertQueryOptions<TData>, 'upsert'>
505
506
  ): string {
506
507
 
507
- const valuesToUpdate = this.getValuesToUpdate(table, opts.upsert);
508
+ const valuesToUpdate = this.getValuesToUpdate(table, data, opts.upsert);
508
509
 
509
510
  // All columns are ps
510
511
  const valuesToUpdatesEntries = Object.entries(valuesToUpdate);
@@ -519,35 +520,39 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
519
520
  // TODO: Fix typings
520
521
  private getValuesToUpdate<TData extends TObjetDonnees>(
521
522
  table: TMetasTable,
523
+ data: TData[],
522
524
  colsToUpdate: TColsToUpsert<TData>
523
525
  ) {
524
526
 
525
527
  // Column name => SQL
526
528
  let valuesToUpdate: Partial<TData> = {};
527
-
528
529
  // Define which columns to update when the record already exists
529
530
  let valuesNamesToUpdate: (keyof TData)[] = [];
530
- if (colsToUpdate === '*') {
531
+ let updateAll: boolean | undefined;
532
+
533
+ if (Array.isArray( colsToUpdate )) {
531
534
 
532
- valuesNamesToUpdate = Object.keys(table.colonnes);// table.columnNamesButPk;
533
- console.log(LogPrefix, `Automatic upsert into ${table.chemin} using ${table.pk.join(', ')} as pk: ${valuesNamesToUpdate.join(', ')}`);
534
- // We don't take columnNamesButPk, because if all the columns are pks, we don't have yny value for the ON DUPLICATE KEY
535
- // Meaning
535
+ valuesNamesToUpdate = colsToUpdate;
536
536
 
537
- } else if (Array.isArray( colsToUpdate )) {
537
+ } else if (colsToUpdate === '*') {
538
538
 
539
- valuesNamesToUpdate = colsToUpdate;
539
+ updateAll = true;
540
540
 
541
541
  } else {
542
542
 
543
- const { '*': updateAll, ...customValuesToUpdate } = colsToUpdate;
543
+ let customValuesToUpdate: Partial<TData>;
544
+ ({ '*': updateAll, ...customValuesToUpdate } = colsToUpdate);
544
545
 
545
546
  for (const colKey in customValuesToUpdate)
546
547
  valuesToUpdate[ colKey ] = this.esc(customValuesToUpdate[ colKey ], true);
547
548
 
548
- if (updateAll)
549
- valuesNamesToUpdate = Object.keys(table.colonnes);//table.columnNamesButPk;
549
+ }
550
550
 
551
+ if (updateAll) {
552
+ for (const record of data)
553
+ for (const key in record)
554
+ if (!valuesNamesToUpdate.includes( key ) && (key in table.colonnes))
555
+ valuesNamesToUpdate.push( key );
551
556
  }
552
557
 
553
558
  for (const colToUpdate of valuesNamesToUpdate)
@@ -59,6 +59,11 @@ export default abstract class FsDriver<
59
59
  > extends Service<Config, {}, Application, Services> {
60
60
 
61
61
  public abstract mount(): Promise<void>;
62
+
63
+ public abstract getFileUrl(
64
+ bucketName: TBucketName,
65
+ filename: string
66
+ ): string;
62
67
 
63
68
  public abstract readDir( bucketName: TBucketName, dirname?: string ): Promise<SourceFile[]>;
64
69
 
@@ -72,6 +72,13 @@ export default class LocalFS<
72
72
  - ACTIONS
73
73
  ----------------------------------*/
74
74
 
75
+ public getFileUrl(
76
+ bucketName: TBucketName,
77
+ filename: string
78
+ ) {
79
+ throw new Error("Method not available for local files.");
80
+ }
81
+
75
82
  public async readDir( bucketName: TBucketName, dirname?: string ) {
76
83
 
77
84
  const bucketDir = this.config.buckets[bucketName];
@@ -99,6 +99,16 @@ export default class S3Driver<
99
99
  - ACTIONS
100
100
  ----------------------------------*/
101
101
 
102
+ public getFileUrl(
103
+ bucketName: TBucketName,
104
+ filename: string
105
+ ) {
106
+ const bucket = this.config.buckets[bucketName];
107
+ if (bucket === undefined)
108
+ throw new Error(`Bucket "${bucketName}" not found in configuration`);
109
+ return `https://${bucket}.s3.${this.config.region}.amazonaws.com/${filename}`
110
+ }
111
+
102
112
  public readDir( bucketName: TBucketName, dirname?: string ) {
103
113
  const bucket = this.config.buckets[bucketName];
104
114
  return new Promise<SourceFile[]>((resolve, reject) => {
@@ -62,7 +62,7 @@ export type TApiRegisterArgs<TRouter extends ServerRouter> = ([
62
62
 
63
63
  export type TServerController<TRouter extends ServerRouter> = (context: TRouterContext<TRouter>) => any;
64
64
 
65
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS'
65
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
66
66
  export type TRouteHttpMethod = HttpMethod | '*';
67
67
 
68
68
  export type TApiResponseData = {
@@ -264,6 +264,7 @@ export default class ServerRouter<
264
264
  public get = (...args: TApiRegisterArgs<this>) => this.registerApi('GET', ...args);
265
265
  public post = (...args: TApiRegisterArgs<this>) => this.registerApi('POST', ...args);
266
266
  public put = (...args: TApiRegisterArgs<this>) => this.registerApi('PUT', ...args);
267
+ public patch = (...args: TApiRegisterArgs<this>) => this.registerApi('PATCH', ...args);
267
268
  public delete = (...args: TApiRegisterArgs<this>) => this.registerApi('DELETE', ...args)
268
269
 
269
270
  protected registerApi(method: TRouteHttpMethod, ...args: TApiRegisterArgs<this>): this {
@@ -22,6 +22,9 @@ import Page from './page';
22
22
  // To move into a new npm module: json-mask
23
23
  import jsonMask from './mask';
24
24
 
25
+ // Types
26
+ import type { TBasicUser } from '@server/services/auth';
27
+
25
28
  /*----------------------------------
26
29
  - TYPES
27
30
  ----------------------------------*/
@@ -31,7 +34,7 @@ const debug = true;
31
34
  export type TBasicSSrData = {
32
35
  request: { data: TObjetDonnees, id: string },
33
36
  page: { chunkId: string, data?: TObjetDonnees },
34
- user: User | null,
37
+ user: TBasicUser | null,
35
38
  domains: TDomainsList
36
39
  }
37
40
 
@@ -45,7 +48,7 @@ export type TRouterContext<TRouter extends ServerRouter = ServerRouter> = (
45
48
  response: ServerResponse<TRouter>,
46
49
  route: TRoute,
47
50
  page?: Page,
48
- user: User,
51
+ user: TBasicUser,
49
52
 
50
53
  Router: TRouter,
51
54
  }