Merge branch 'develop' into react-18

This commit is contained in:
Gervwyk 2022-05-30 15:16:02 +02:00
commit c1c1adec3a
43 changed files with 446 additions and 82 deletions

View File

@ -16,13 +16,6 @@
/* eslint-disable max-classes-per-file */
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
}
}
class ConfigurationError extends Error {
constructor(message) {
super(message);
@ -44,11 +37,4 @@ class ServerError extends Error {
}
}
class TokenExpiredError extends Error {
constructor(message) {
super(message);
this.name = 'TokenExpiredError';
}
}
export { AuthenticationError, ConfigurationError, RequestError, ServerError, TokenExpiredError };
export { ConfigurationError, RequestError, ServerError };

View File

@ -17,20 +17,13 @@
import callRequest from './routes/request/callRequest.js';
import createApiContext from './context/createApiContext.js';
import getHomeAndMenus from './routes/rootConfig/getHomeAndMenus.js';
import getNextAuthConfig from './auth/getNextAuthConfig.js';
import getNextAuthConfig from './routes/auth/getNextAuthConfig.js';
import getPageConfig from './routes/page/getPageConfig.js';
import getRootConfig from './routes/rootConfig/getRootConfig.js';
import {
AuthenticationError,
ConfigurationError,
RequestError,
ServerError,
TokenExpiredError,
} from './context/errors.js';
import { ConfigurationError, RequestError, ServerError } from './context/errors.js';
export {
AuthenticationError,
callRequest,
ConfigurationError,
createApiContext,
@ -40,5 +33,4 @@ export {
getRootConfig,
RequestError,
ServerError,
TokenExpiredError,
};

View File

@ -23,9 +23,55 @@ function createJWTCallback({ authConfig, plugins }) {
type: 'jwt',
});
if (jwtCallbackPlugins.length === 0) return undefined;
async function jwtCallback({ token, user, account, profile, isNewUser }) {
if (profile) {
const {
sub,
name,
given_name,
family_name,
middle_name,
nickname,
preferred_username,
profile: profile_claim,
picture,
website,
email,
email_verified,
gender,
birthdate,
zoneinfo,
locale,
phone_number,
phone_number_verified,
address,
updated_at,
} = profile;
token = {
sub,
name,
given_name,
family_name,
middle_name,
nickname,
preferred_username,
profile: profile_claim,
picture,
website,
email,
email_verified,
gender,
birthdate,
zoneinfo,
locale,
phone_number,
phone_number_verified,
address,
updated_at,
...token,
};
}
for (const plugin of jwtCallbackPlugins) {
token = await plugin.fn({
properties: plugin.properties ?? {},

View File

@ -24,8 +24,53 @@ function createSessionCallback({ authConfig, plugins }) {
});
async function sessionCallback({ session, token, user }) {
// console.log({ session, token, user });
if (token) {
session.user.sub = token.sub;
const {
sub,
name,
given_name,
family_name,
middle_name,
nickname,
preferred_username,
profile,
picture,
website,
email,
email_verified,
gender,
birthdate,
zoneinfo,
locale,
phone_number,
phone_number_verified,
address,
updated_at,
} = token;
session.user = {
sub,
name,
given_name,
family_name,
middle_name,
nickname,
preferred_username,
profile,
picture,
website,
email,
email_verified,
gender,
birthdate,
zoneinfo,
locale,
phone_number,
phone_number_verified,
address,
updated_at,
...session.user,
};
}
for (const plugin of sessionCallbackPlugins) {

View File

@ -16,12 +16,16 @@
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
import buildAuthPlugins from './buildAuthPlugins.js';
import buildPageAuth from './buildPageAuth.js';
import validateAuthConfig from './validateAuthConfig.js';
function buildAuth({ components, context }) {
const configured = !type.isNone(components.auth);
validateAuthConfig({ components });
components.auth.configured = configured;
buildPageAuth({ components });
buildAuthPlugins({ components, context });

View File

@ -31,6 +31,7 @@ test('buildAuth default', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: false,
events: [],
pages: {
roles: {},
@ -53,6 +54,7 @@ test('buildAuth no pages', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: false,
events: [],
pages: {
roles: {},
@ -82,6 +84,7 @@ test('buildAuth all protected, some public', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
public: ['a', 'b'],
@ -117,6 +120,7 @@ test('buildAuth all public, some protected', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
protected: ['a', 'b'],
@ -152,6 +156,7 @@ test('buildAuth all public', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
public: true,
@ -188,6 +193,7 @@ test('buildAuth all protected', async () => {
auth: {
callbacks: [],
events: [],
configured: true,
pages: {
protected: true,
roles: {},
@ -224,6 +230,7 @@ test('buildAuth roles', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
roles: {
@ -276,6 +283,7 @@ test('buildAuth roles and protected pages array', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
roles: {
@ -307,6 +315,7 @@ test('buildAuth roles and protected true', async () => {
expect(res).toEqual({
auth: {
callbacks: [],
configured: true,
events: [],
pages: {
roles: {

View File

@ -30,7 +30,6 @@ const Client = ({
config,
resetContext = { reset: false, setReset: () => undefined },
router,
session,
stage,
types,
window,
@ -40,7 +39,6 @@ const Client = ({
Components,
config,
router,
session,
stage,
types,
window,

View File

@ -34,12 +34,14 @@ function getCallbackUrl({ lowdefy, callbackUrl = {} }) {
return undefined;
}
function createAuthMethods(lowdefy, auth) {
function createAuthMethods({ lowdefy, auth }) {
// login and logout are Lowdefy function that handle action params
// signIn and signOut are the next-auth methods
function login({ providerId, callbackUrl, authUrl = {} } = {}) {
// TODO: if only one provider exists, pass provider here
// to link directly to provider
if (type.isNone(providerId) && auth.authConfig.providers.length === 1) {
providerId = auth.authConfig.providers[0].id;
}
auth.signIn(
providerId,
{ callbackUrl: getCallbackUrl({ lowdefy, callbackUrl }) },

View File

@ -44,7 +44,7 @@ const lowdefy = {
lowdefyGlobal: {},
};
function initLowdefyContext({ auth, Components, config, router, session, stage, types, window }) {
function initLowdefyContext({ auth, Components, config, router, stage, types, window }) {
if (stage === 'dev') {
window.lowdefy = lowdefy;
}
@ -54,7 +54,7 @@ function initLowdefyContext({ auth, Components, config, router, session, stage,
lowdefy.menus = config.rootConfig.menus;
lowdefy.pageId = config.pageConfig.pageId;
lowdefy.urlQuery = urlQuery.parse(window.location.search.slice(1));
lowdefy.user = session?.user ?? null;
lowdefy.user = auth?.session?.user ?? null;
lowdefy._internal.window = window;
lowdefy._internal.document = window.document;
@ -69,7 +69,7 @@ function initLowdefyContext({ auth, Components, config, router, session, stage,
lowdefy._internal.operators = types.operators;
// TODO: discuss not using object arguments
lowdefy._internal.auth = createAuthMethods(lowdefy, auth);
lowdefy._internal.auth = createAuthMethods({ lowdefy, auth });
return lowdefy;
}

View File

@ -31,7 +31,7 @@ import blocks from '../build/plugins/blocks.js';
import icons from '../build/plugins/icons.js';
import operators from '../build/plugins/operators/client.js';
const App = () => {
const App = ({ auth }) => {
const router = useRouter();
const { data: rootConfig } = useRootConfig(router.basePath);
@ -44,6 +44,7 @@ const App = () => {
<Reload basePath={router.basePath}>
{(resetContext) => (
<Page
auth={auth}
Components={{ Head, Link }}
config={{
rootConfig,

View File

@ -15,20 +15,14 @@
*/
import React from 'react';
import { signIn, signOut, useSession } from 'next-auth/react';
import Client from '@lowdefy/client';
import RestartingPage from './RestartingPage.js';
import usePageConfig from './utils/usePageConfig.js';
const Page = ({ Components, config, pageId, resetContext, router, types }) => {
const { data: session, status } = useSession();
const Page = ({ auth, Components, config, pageId, resetContext, router, types }) => {
const { data: pageConfig } = usePageConfig(pageId, router.basePath);
if (status === 'loading') {
return '';
}
if (!pageConfig) {
router.replace(`/404`);
return '';
@ -38,7 +32,7 @@ const Page = ({ Components, config, pageId, resetContext, router, types }) => {
}
return (
<Client
auth={{ signIn, signOut }}
auth={auth}
Components={Components}
config={{
...config,
@ -46,7 +40,6 @@ const Page = ({ Components, config, pageId, resetContext, router, types }) => {
}}
resetContext={resetContext}
router={router}
session={session}
stage="dev"
types={types}
window={window}

View File

@ -0,0 +1,35 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import AuthConfigured from './AuthConfigured.js';
import AuthNotConfigured from './AuthNotConfigured.js';
import authConfig from '../../build/auth.json';
function Auth({ children, session }) {
if (authConfig.configured === true) {
return (
<AuthConfigured session={session} authConfig={authConfig}>
{(auth) => children(auth)}
</AuthConfigured>
);
}
return <AuthNotConfigured authConfig={authConfig}>{(auth) => children(auth)}</AuthNotConfigured>;
}
export default Auth;

View File

@ -0,0 +1,47 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react';
function Session({ children }) {
const { data: session, status } = useSession();
// If session is passed to SessionProvider from getServerSideProps
// we won't have a loading state here.
// But 404 uses getStaticProps so we have this for 404.
if (status === 'loading') {
return '';
}
return children(session);
}
function AuthConfigured({ authConfig, children, serverSession }) {
const auth = { signIn, signOut, authConfig };
return (
<SessionProvider session={serverSession}>
<Session>
{(session) => {
auth.session = session;
return children(auth);
}}
</Session>
</SessionProvider>
);
}
export default AuthConfigured;

View File

@ -0,0 +1,32 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
function authNotConfigured() {
throw new Error('Auth not configured.');
}
function AuthNotConfigured({ authConfig, children }) {
const auth = {
authConfig,
signIn: authNotConfigured,
signOut: authNotConfigured,
};
return children(auth);
}
export default AuthNotConfigured;

View File

@ -0,0 +1,27 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getSession } from 'next-auth/react';
import authJson from '../../build/auth.json';
async function getServerSession(context) {
if (authJson.configured === true) {
return await getSession(context);
}
return undefined;
}
export default getServerSession;

View File

@ -16,7 +16,8 @@
import React, { Suspense } from 'react';
import dynamic from 'next/dynamic';
import { SessionProvider } from 'next-auth/react';
import Auth from '../lib/auth/Auth.js';
// Must be in _app due to next specifications.
import '../build/plugins/styles.less';
@ -24,9 +25,7 @@ import '../build/plugins/styles.less';
function App({ Component }) {
return (
<Suspense fallback="">
<SessionProvider>
<Component />
</SessionProvider>
<Auth>{(auth) => <Component auth={auth} />}</Auth>
</Suspense>
);
}

View File

@ -15,10 +15,10 @@
*/
import { createApiContext, getPageConfig } from '@lowdefy/api';
import { getSession } from 'next-auth/react';
import getServerSession from '../../../lib/auth/getServerSession.js';
export default async function handler(req, res) {
const session = await getSession({ req });
const session = await getServerSession({ req });
const apiContext = await createApiContext({
buildDirectory: './build',
logger: console,

View File

@ -16,8 +16,9 @@
import { callRequest, createApiContext } from '@lowdefy/api';
import { getSecretsFromEnv } from '@lowdefy/node-utils';
import { getSession } from 'next-auth/react';
import connections from '../../../../build/plugins/connections.js';
import getServerSession from '../../../../lib/auth/getServerSession.js';
import operators from '../../../../build/plugins/operators/server.js';
export default async function handler(req, res) {
@ -25,7 +26,7 @@ export default async function handler(req, res) {
if (req.method !== 'POST') {
throw new Error('Only POST requests are supported.');
}
const session = await getSession({ req });
const session = await getServerSession({ req });
const apiContext = await createApiContext({
buildDirectory: './build',
connections,

View File

@ -15,10 +15,11 @@
*/
import { createApiContext, getRootConfig } from '@lowdefy/api';
import { getSession } from 'next-auth/react';
import getServerSession from '../../lib/auth/getServerSession.js';
export default async function handler(req, res) {
const session = await getSession({ req });
const session = await getServerSession({ req });
const apiContext = await createApiContext({
buildDirectory: './build',
logger: console,

View File

@ -20,34 +20,23 @@ import { useRouter } from 'next/router';
import Client from '@lowdefy/client';
import Head from 'next/head';
import Link from 'next/link';
import { signIn, signOut, useSession } from 'next-auth/react';
import actions from '../build/plugins/actions.js';
import blocks from '../build/plugins/blocks.js';
import icons from '../build/plugins/icons.js';
import operators from '../build/plugins/operators/client.js';
const Page = ({ pageConfig, rootConfig }) => {
const Page = ({ auth, pageConfig, rootConfig }) => {
const router = useRouter();
const { data: session, status } = useSession();
// If session is passed to SessionProvider from getServerSideProps
// we won't have a loading state here.
// But 404 uses getStaticProps so we have this for 404.
if (status === 'loading') {
return '';
}
return (
<Client
auth={{ signIn, signOut }}
auth={auth}
Components={{ Head, Link }}
config={{
pageConfig,
rootConfig,
}}
router={router}
session={session}
types={{
actions,
blocks,

View File

@ -0,0 +1,35 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import AuthConfigured from './AuthConfigured.js';
import AuthNotConfigured from './AuthNotConfigured.js';
import authConfig from '../../build/auth.json';
function Auth({ children, session }) {
if (authConfig.configured === true) {
return (
<AuthConfigured session={session} authConfig={authConfig}>
{(auth) => children(auth)}
</AuthConfigured>
);
}
return <AuthNotConfigured authConfig={authConfig}>{(auth) => children(auth)}</AuthNotConfigured>;
}
export default Auth;

View File

@ -0,0 +1,47 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react';
function Session({ children }) {
const { data: session, status } = useSession();
// If session is passed to SessionProvider from getServerSideProps
// we won't have a loading state here.
// But 404 uses getStaticProps so we have this for 404.
if (status === 'loading') {
return '';
}
return children(session);
}
function AuthConfigured({ authConfig, children, serverSession }) {
const auth = { signIn, signOut, authConfig };
return (
<SessionProvider session={serverSession}>
<Session>
{(session) => {
auth.session = session;
return children(auth);
}}
</Session>
</SessionProvider>
);
}
export default AuthConfigured;

View File

@ -0,0 +1,32 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
function authNotConfigured() {
throw new Error('Auth not configured.');
}
function AuthNotConfigured({ authConfig, children }) {
const auth = {
authConfig,
signIn: authNotConfigured,
signOut: authNotConfigured,
};
return children(auth);
}
export default AuthNotConfigured;

View File

@ -0,0 +1,27 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getSession } from 'next-auth/react';
import authJson from '../../build/auth.json';
async function getServerSession(context) {
if (authJson.configured === true) {
return await getSession(context);
}
return undefined;
}
export default getServerSession;

View File

@ -15,13 +15,13 @@
*/
import { createApiContext, getPageConfig, getRootConfig } from '@lowdefy/api';
import { getSession } from 'next-auth/react';
import getServerSession from '../lib/auth/getServerSession.js';
import Page from '../lib/Page.js';
export async function getServerSideProps(context) {
const { pageId } = context.params;
const session = await getSession(context);
const session = await getServerSession(context);
const apiContext = await createApiContext({
buildDirectory: './build',
logger: console,

View File

@ -13,20 +13,21 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import dynamic from 'next/dynamic';
import { SessionProvider } from 'next-auth/react';
import Auth from '../lib/auth/Auth.js';
// Must be in _app due to next specifications.
import '../build/plugins/styles.less';
// TODO: SessionProvider requires basebath
function App({ Component, pageProps: { session, ...pageProps } }) {
function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
<Auth session={session}>
{(auth) => <Component auth={auth} rootConfig={rootConfig} pageConfig={pageConfig} />}
</Auth>
);
}

View File

@ -22,4 +22,16 @@ import callbacks from '../../../build/plugins/auth/callbacks.js';
import events from '../../../build/plugins/auth/events.js';
import providers from '../../../build/plugins/auth/providers.js';
export default NextAuth(getNextAuthConfig({ authJson, plugins: { callbacks, events, providers } }));
export default async function auth(req, res) {
if (authJson.configured === true) {
return await NextAuth(
req,
res,
getNextAuthConfig({ authJson, plugins: { callbacks, events, providers } })
);
}
return res.status(404).json({
message: 'Auth not configured',
});
}

View File

@ -16,7 +16,9 @@
import { callRequest, createApiContext } from '@lowdefy/api';
import { getSecretsFromEnv } from '@lowdefy/node-utils';
import { getSession } from 'next-auth/react';
import getServerSession from '../../../../lib/auth/getServerSession.js';
import connections from '../../../../build/plugins/connections.js';
import operators from '../../../../build/plugins/operators/server.js';
@ -25,7 +27,7 @@ export default async function handler(req, res) {
if (req.method !== 'POST') {
throw new Error('Only POST requests are supported.');
}
const session = await getSession({ req });
const session = await getServerSession({ req });
const apiContext = await createApiContext({
buildDirectory: './build',
connections,

View File

@ -15,12 +15,13 @@
*/
import { createApiContext, getPageConfig, getRootConfig } from '@lowdefy/api';
import { getSession } from 'next-auth/react';
import getServerSession from '../lib/auth/getServerSession.js';
import Page from '../lib/Page.js';
export async function getServerSideProps(context) {
const session = await getSession(context);
const session = await getServerSession(context);
const apiContext = await createApiContext({
buildDirectory: './build',
logger: console,