5htp-core 0.4.6 → 0.4.7-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/package.json +1 -1
- package/src/client/assets/css/components/table.less +70 -7
- package/src/client/components/Select/ChoiceSelector.tsx +6 -5
- package/src/client/components/Select/index.tsx +4 -2
- package/src/client/components/Table/index.tsx +67 -15
- package/src/client/components/inputv3/base.tsx +3 -1
- package/src/client/components/inputv3/index.tsx +1 -1
- package/src/common/router/request/index.ts +4 -1
- package/src/common/validation/validators.ts +29 -1
- package/src/server/app/index.ts +0 -11
- package/src/server/services/auth/index.ts +86 -47
- package/src/server/services/auth/router/request.ts +9 -5
- package/src/server/services/database/index.ts +1 -1
- package/src/server/services/database/metas.ts +1 -1
- package/src/server/services/router/index.ts +2 -1
- 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.
|
|
4
|
+
"version": "0.4.7-1",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.table {
|
|
2
2
|
overflow: auto;
|
|
3
|
+
max-height: 90vh;
|
|
3
4
|
|
|
4
5
|
> table {
|
|
5
6
|
border-collapse: collapse;
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
&.card {
|
|
16
|
-
padding:
|
|
17
|
+
padding: 0;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -26,7 +27,7 @@ table {
|
|
|
26
27
|
|
|
27
28
|
// By default, chrome disables text inherits
|
|
28
29
|
line-height: inherit;
|
|
29
|
-
font-size:
|
|
30
|
+
font-size: 0.9em;
|
|
30
31
|
|
|
31
32
|
th {
|
|
32
33
|
font-weight: 500;
|
|
@@ -38,8 +39,8 @@ table {
|
|
|
38
39
|
padding: 1em 1em;
|
|
39
40
|
text-align: left;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
border-
|
|
42
|
+
&:not(:last-child) {
|
|
43
|
+
border-right: solid 1px #F5F5F5;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
&:first-child {
|
|
@@ -50,6 +51,29 @@ table {
|
|
|
50
51
|
padding-right: 2em;
|
|
51
52
|
text-align: right;
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
&.stickyColumn {
|
|
56
|
+
position: sticky;
|
|
57
|
+
background: var(--cBg);
|
|
58
|
+
white-space: break-spaces;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
|
|
61
|
+
&:first-child {
|
|
62
|
+
left: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&:last-child {
|
|
66
|
+
right: 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
thead {
|
|
72
|
+
position: sticky;
|
|
73
|
+
top: 0;
|
|
74
|
+
background: var(--cBg);
|
|
75
|
+
white-space: break-spaces;
|
|
76
|
+
z-index: 5;
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
tbody {
|
|
@@ -57,11 +81,45 @@ table {
|
|
|
57
81
|
td, th {
|
|
58
82
|
vertical-align: middle;
|
|
59
83
|
border-top: solid 1px var(--cLine);
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Rows: Crop when too long
|
|
88
|
+
td {
|
|
89
|
+
|
|
90
|
+
cursor: default;
|
|
91
|
+
|
|
92
|
+
&.extendable > .row {
|
|
93
|
+
|
|
94
|
+
max-width: 20em;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
justify-content: flex-start;
|
|
97
|
+
|
|
98
|
+
&::after {
|
|
99
|
+
content: ' ';
|
|
100
|
+
display: block;
|
|
101
|
+
position: absolute;
|
|
102
|
+
|
|
103
|
+
top: 0;
|
|
104
|
+
right: 0;
|
|
105
|
+
width: 30%;
|
|
106
|
+
height: 100%;
|
|
107
|
+
|
|
108
|
+
background: linear-gradient(to right, rgba(255, 255, 255, 0), var(--cBg));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
> * {
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
> .badge {
|
|
116
|
+
font-size: 0.8em;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
60
119
|
}
|
|
61
120
|
|
|
62
|
-
|
|
63
|
-
background:
|
|
64
|
-
color: #fff;
|
|
121
|
+
tr:hover > td {
|
|
122
|
+
background: #FAFAFA;
|
|
65
123
|
}
|
|
66
124
|
}
|
|
67
125
|
|
|
@@ -112,4 +170,9 @@ table {
|
|
|
112
170
|
}
|
|
113
171
|
}
|
|
114
172
|
}*/
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.modal .tableCellExtended .row {
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
justify-content: flex-start;
|
|
115
178
|
}
|
|
@@ -18,9 +18,7 @@ import type { TDialogControls } from '@client/components/dropdown';
|
|
|
18
18
|
|
|
19
19
|
export type Choice = { label: ComponentChild, value: string }
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type ChoicesFunc = (search: string) => Promise<Choices>
|
|
21
|
+
type ChoicesFunc = (search: string) => Promise<Choice[]>
|
|
24
22
|
|
|
25
23
|
export type Props = (
|
|
26
24
|
{
|
|
@@ -37,10 +35,13 @@ export type Props = (
|
|
|
37
35
|
validator?: StringValidator
|
|
38
36
|
}
|
|
39
37
|
) & {
|
|
40
|
-
choices:
|
|
38
|
+
choices: Choice[] | ChoicesFunc | string[],
|
|
41
39
|
enableSearch?: boolean,
|
|
42
40
|
required?: boolean,
|
|
43
41
|
noneSelection?: false | string,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type SelectorProps = Props & {
|
|
44
45
|
currentList: Choice[],
|
|
45
46
|
refDropdown?: RefObject<TDialogControls>
|
|
46
47
|
}
|
|
@@ -67,7 +68,7 @@ export default React.forwardRef<HTMLDivElement, Props>(({
|
|
|
67
68
|
currentList,
|
|
68
69
|
refDropdown,
|
|
69
70
|
...otherProps
|
|
70
|
-
}:
|
|
71
|
+
}: SelectorProps, ref) => {
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
|
|
@@ -22,7 +22,7 @@ import ChoiceElement from './ChoiceElement';
|
|
|
22
22
|
----------------------------------*/
|
|
23
23
|
|
|
24
24
|
export type Props = SelectorProps & {
|
|
25
|
-
dropdown
|
|
25
|
+
dropdown?: boolean | DropdownProps,
|
|
26
26
|
title: string,
|
|
27
27
|
errors?: string[],
|
|
28
28
|
}
|
|
@@ -77,8 +77,10 @@ export default ({
|
|
|
77
77
|
const popoverState = React.useState(false);
|
|
78
78
|
|
|
79
79
|
const choicesViaFunc = typeof initChoices === 'function';
|
|
80
|
-
if (choicesViaFunc
|
|
80
|
+
if (choicesViaFunc)
|
|
81
81
|
enableSearch = true;
|
|
82
|
+
else if (typeof initChoices[0] === 'string')
|
|
83
|
+
initChoices = initChoices.map( c => ({ label: c, value: c }));
|
|
82
84
|
|
|
83
85
|
const refInputSearch = React.useRef<HTMLInputElement | null>(null);
|
|
84
86
|
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
/*----------------------------------
|
|
3
3
|
- DEPENDANCES
|
|
4
4
|
----------------------------------*/
|
|
5
|
+
|
|
5
6
|
// Libs
|
|
6
7
|
import React from 'react';
|
|
7
|
-
import { ComponentChild } from 'preact';
|
|
8
|
+
import { JSX, ComponentChild } from 'preact';
|
|
8
9
|
|
|
9
10
|
// Composants
|
|
11
|
+
import useContext from '@/client/context';
|
|
10
12
|
import Button, { Props as TButtonProps } from '@client/components/button';
|
|
11
13
|
import Popover from '../containers/Popover';
|
|
12
14
|
import Checkbox from '../inputv3/Checkbox';
|
|
@@ -21,6 +23,7 @@ export type Props<TRow> = {
|
|
|
21
23
|
|
|
22
24
|
data: TRow[],
|
|
23
25
|
columns: (row: TRow, rows: TRow[], index: number) => TColumn[];
|
|
26
|
+
stickyHeader?: boolean,
|
|
24
27
|
|
|
25
28
|
setData?: (rows: TRow[]) => void,
|
|
26
29
|
empty?: ComponentChild | false,
|
|
@@ -29,11 +32,11 @@ export type Props<TRow> = {
|
|
|
29
32
|
actions?: TAction<TRow>[]
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
export type TColumn = {
|
|
35
|
+
export type TColumn = JSX.HTMLAttributes<HTMLElement> & {
|
|
33
36
|
label: ComponentChild,
|
|
34
37
|
cell: ComponentChild,
|
|
35
38
|
raw?: number | string | boolean,
|
|
36
|
-
|
|
39
|
+
stick?: boolean,
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export type TAction<TRow> = Omit<TButtonProps, 'onClick'> & {
|
|
@@ -47,13 +50,16 @@ export type TAction<TRow> = Omit<TButtonProps, 'onClick'> & {
|
|
|
47
50
|
- COMPOSANTS
|
|
48
51
|
----------------------------------*/
|
|
49
52
|
export default function Liste<TRow extends TDonneeInconnue>({
|
|
53
|
+
stickyHeader,
|
|
50
54
|
data: rows, setData, empty,
|
|
51
55
|
columns, actions, ...props
|
|
52
56
|
}: Props<TRow>) {
|
|
53
57
|
|
|
58
|
+
const { modal } = useContext();
|
|
59
|
+
|
|
54
60
|
if (rows.length === 0)
|
|
55
61
|
return empty === false ? null : (
|
|
56
|
-
<div class="pd-2 col al-center">
|
|
62
|
+
<div class={"pd-2 col al-center " + (props.className || '')}>
|
|
57
63
|
{empty || <>
|
|
58
64
|
<i src="meh-rolling-eyes" class="xl" />
|
|
59
65
|
Uh ... No rows here.
|
|
@@ -92,29 +98,75 @@ export default function Liste<TRow extends TDonneeInconnue>({
|
|
|
92
98
|
</td>
|
|
93
99
|
)}
|
|
94
100
|
|
|
95
|
-
{columns(row, rows, iDonnee).map((
|
|
101
|
+
{columns(row, rows, iDonnee).map(({
|
|
102
|
+
label, cell, class: className, raw,
|
|
103
|
+
stick, width, ...cellProps
|
|
104
|
+
}) => {
|
|
105
|
+
|
|
106
|
+
let classe = className || '';
|
|
107
|
+
if (typeof raw === 'number')
|
|
108
|
+
classe += 'txtRight';
|
|
109
|
+
|
|
110
|
+
if (stick) {
|
|
111
|
+
classe += ' stickyColumn';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (width) {
|
|
115
|
+
|
|
116
|
+
if (cellProps.style === undefined)
|
|
117
|
+
cellProps.style = {};
|
|
118
|
+
|
|
119
|
+
cellProps.style = {
|
|
120
|
+
...cellProps.style,
|
|
121
|
+
minWidth: width,
|
|
122
|
+
width: width,
|
|
123
|
+
maxWidth: width,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
96
126
|
|
|
97
127
|
if (iDonnee === 0) renduColonnes.push(
|
|
98
|
-
<th>
|
|
99
|
-
{
|
|
128
|
+
<th class={classe} {...cellProps}>
|
|
129
|
+
{label}
|
|
100
130
|
</th>
|
|
101
131
|
);
|
|
102
132
|
|
|
103
|
-
|
|
133
|
+
let render: ComponentChild;
|
|
134
|
+
if (Array.isArray(cell)) {
|
|
104
135
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
classe += ' extendable';
|
|
137
|
+
|
|
138
|
+
render = (
|
|
139
|
+
<div class="row sp-05">
|
|
140
|
+
{cell.map((item, i) => (
|
|
141
|
+
<span class={"badge bg light" + ((i % 7) + 1)}>
|
|
142
|
+
{item}
|
|
143
|
+
</span>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// Extension
|
|
149
|
+
cellProps.onClick = () => modal.show(() => (
|
|
150
|
+
<div class="card col tableCellExtended">
|
|
151
|
+
<h3>{label}</h3>
|
|
152
|
+
{render}
|
|
153
|
+
</div>
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
} else if (['number', 'string'].includes(typeof cell) || React.isValidElement(cell)) {
|
|
157
|
+
render = cell;
|
|
158
|
+
} else
|
|
159
|
+
render = JSON.stringify(cell);
|
|
108
160
|
|
|
109
161
|
return (
|
|
110
|
-
<td class={classe}>
|
|
111
|
-
{
|
|
162
|
+
<td class={classe} {...cellProps}>
|
|
163
|
+
{render}
|
|
112
164
|
</td>
|
|
113
165
|
)
|
|
114
166
|
})}
|
|
115
167
|
|
|
116
168
|
{actions !== undefined && (
|
|
117
|
-
<td>
|
|
169
|
+
<td class="stickyColumn">
|
|
118
170
|
<Popover content={(
|
|
119
171
|
<ul class="col menu card bg white">
|
|
120
172
|
{actions.map(({ label, onClick, ...props }: TAction<TRow>) => (
|
|
@@ -148,7 +200,7 @@ export default function Liste<TRow extends TDonneeInconnue>({
|
|
|
148
200
|
return <>
|
|
149
201
|
<div {...props}>
|
|
150
202
|
<table>
|
|
151
|
-
<thead>
|
|
203
|
+
<thead className={stickyHeader ? 'stickyHeader' : undefined}>
|
|
152
204
|
<tr>
|
|
153
205
|
{selectionMultiple && (
|
|
154
206
|
<th>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import type { ComponentChild } from 'preact';
|
|
7
8
|
import type { StateUpdater } from 'preact/hooks';
|
|
8
9
|
|
|
9
10
|
// Core libs
|
|
@@ -16,6 +17,7 @@ import { useState } from '@client/hooks';
|
|
|
16
17
|
export type InputBaseProps<TValue> = {
|
|
17
18
|
|
|
18
19
|
title: string, // Now mandatory
|
|
20
|
+
hint?: ComponentChild,
|
|
19
21
|
required?: boolean,
|
|
20
22
|
errors?: string[],
|
|
21
23
|
size?: TComponentSize,
|
|
@@ -64,7 +66,7 @@ export function useInput<TValue>(
|
|
|
64
66
|
if (state.changed === false)
|
|
65
67
|
return;
|
|
66
68
|
|
|
67
|
-
console.log(`[input] Commit value:`, state.value, externalValue);
|
|
69
|
+
//console.log(`[input] Commit value:`, state.value, externalValue);
|
|
68
70
|
if (onChange !== undefined)
|
|
69
71
|
onChange(state.value);
|
|
70
72
|
}
|
|
@@ -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:
|
|
28
|
+
public user: TBasicUser | null = null;
|
|
26
29
|
|
|
27
30
|
public constructor(
|
|
28
31
|
public path: string,
|
|
@@ -41,6 +41,34 @@ type TSubtype = TSchemaSubtype | Validator<any>;
|
|
|
41
41
|
----------------------------------*/
|
|
42
42
|
export default class SchemaValidators {
|
|
43
43
|
|
|
44
|
+
/*----------------------------------
|
|
45
|
+
- UTILITIES
|
|
46
|
+
----------------------------------*/
|
|
47
|
+
// Make every field optional
|
|
48
|
+
public partial = <TFields extends TSchemaFields>(schema: TFields, fieldsList?: (keyof TFields)[] ) => {
|
|
49
|
+
|
|
50
|
+
if (fieldsList === undefined)
|
|
51
|
+
fieldsList = Object.keys(schema) as (keyof TFields)[];
|
|
52
|
+
|
|
53
|
+
const partialSchema: Partial<TFields> = {};
|
|
54
|
+
for (const key of fieldsList) {
|
|
55
|
+
|
|
56
|
+
if (!(key in schema))
|
|
57
|
+
throw new Error("The field " + key + " is not in the schema.");
|
|
58
|
+
|
|
59
|
+
// Only if validator
|
|
60
|
+
if (schema[key] instanceof Validator)
|
|
61
|
+
partialSchema[key] = new Validator(schema[key].type, schema[key].validateType, {
|
|
62
|
+
...schema[key].options,
|
|
63
|
+
opt: true
|
|
64
|
+
});
|
|
65
|
+
else
|
|
66
|
+
partialSchema[key] = schema[key];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return partialSchema as TFields;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
/*----------------------------------
|
|
45
73
|
- CONTENEURS
|
|
46
74
|
----------------------------------*/
|
|
@@ -111,7 +139,7 @@ export default class SchemaValidators {
|
|
|
111
139
|
return undefined;
|
|
112
140
|
|
|
113
141
|
// Normalize for verifications
|
|
114
|
-
const choicesValues = choices?.map(v => v.value)
|
|
142
|
+
const choicesValues = choices?.map(v => typeof v === 'object' ? v.value : v)
|
|
115
143
|
|
|
116
144
|
const checkChoice = ( choice: any ) => {
|
|
117
145
|
|
package/src/server/app/index.ts
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
147
|
+
private getAuthMethod( req: THttpRequest ): null | { token: string, tokenType?: string } {
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 |
|
|
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 =`,
|
|
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(
|
|
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 " +
|
|
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 " +
|
|
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
|
-
|
|
45
|
-
public check( role:
|
|
46
|
-
public check(
|
|
47
|
-
|
|
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
|
}
|
|
@@ -551,7 +551,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
|
|
|
551
551
|
if (updateAll) {
|
|
552
552
|
for (const record of data)
|
|
553
553
|
for (const key in record)
|
|
554
|
-
if (!valuesNamesToUpdate.includes( key ))
|
|
554
|
+
if (!valuesNamesToUpdate.includes( key ) && (key in table.colonnes))
|
|
555
555
|
valuesNamesToUpdate.push( key );
|
|
556
556
|
}
|
|
557
557
|
|
|
@@ -369,7 +369,7 @@ export default class MySQLMetasParser {
|
|
|
369
369
|
// Given that this file is updated during run time,
|
|
370
370
|
// We output a typescript ambient file, so the file change doest trigger infinite app reload
|
|
371
371
|
fs.outputFileSync(
|
|
372
|
-
path.join( Container.path.server.generated, 'models.
|
|
372
|
+
path.join( Container.path.server.generated, 'models.ts'),
|
|
373
373
|
types.join('\n')
|
|
374
374
|
);
|
|
375
375
|
}
|
|
@@ -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:
|
|
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:
|
|
51
|
+
user: TBasicUser,
|
|
49
52
|
|
|
50
53
|
Router: TRouter,
|
|
51
54
|
}
|