创建具有社交登录认证的React PWA
渐进式Web应用(PWA)提供了原生应用的速度、可靠性和离线功能,所有这些都通过Web交付。然而,安全性与性能同样重要,特别是在用户认证方面。在用户期望跨多个设备和平台获得即时、安全访问的世界中,现代认证至关重要。
像Okta这样的身份提供商提供了安全、可扩展且对开发人员友好的工具来实现认证。联合身份允许用户使用现有的社交账户登录。
在本文中,我们将逐步介绍如何构建一个基于React的PWA(支持离线功能),并使用Okta将其与Google社交登录集成。您将学习如何提供快速、可靠的用户体验,并内置现代身份功能。
目录
- 创建Okta集成
- 创建React应用
- 使用React Router保护React应用中的路由
- 使用OAuth 2.0和OpenID Connect(OIDC)进行认证
- 使用社交登录的联合身份
- 在Okta中配置Google作为身份提供商
- 测试使用Google社交登录进行认证
- 将React应用设置为PWA
- 构建安全的待办事项列表React PWA
- 从React PWA使用社交登录进行认证
- 了解更多关于React、PWA、社交登录和联合身份的信息
您需要什么
这是一个适合初学者的教程,所以您主要需要学习的意愿!但是,您需要访问以下几项:
- Node.js和NPM。任何LTS版本都可以,但在本教程中,我使用Node 22和NPM v10
- 命令终端
- 基本的JavaScript和TypeScript知识
- 您选择的IDE。我使用PHPStorm,但您可以使用VSCode或类似工具
- Google Cloud Console账户。您可以使用Gmail账户设置一个
创建Okta集成
在开始之前,您需要一个Okta Integrator免费计划账户。要获取一个,请注册一个Integrator账户。拥有账户后,登录到您的Integrator账户。接下来,在管理控制台中:
- 转到Applications > Applications
- 单击Create App Integration
- 选择OIDC - OpenID Connect作为登录方法
- 选择Single-Page Application作为应用程序类型,然后单击Next
输入应用程序集成名称
在Grant type部分,确保同时选择Authorization Code和Refresh Token
配置重定向URI:
- 登录重定向URI:http://localhost:5173/login/callback
- 登出重定向URI:http://localhost:5173
在Controlled access部分,选择适当的访问级别
单击Save
我在哪里可以找到新应用程序的凭据?
在管理控制台中手动创建OIDC单页应用程序会配置您的Okta组织中的应用程序设置。您可能还需要在Security > API > Trusted Origins中为http://localhost:5173配置受信任的来源。
创建应用程序后,您可以在应用程序的General选项卡上找到配置详细信息:
- Client ID:在Client Credentials部分找到
Client ID:0oab8eb55Kb9jdMIr5d6
注意:您也可以使用Okta CLI客户端或Okta PowerShell模块自动化此过程。有关设置应用程序的更多信息,请参阅本指南。
创建React应用
我们将使用Vite模板来搭建项目。本教程的示例应用程序是一个名为"Lister"的待办事项应用程序。要创建名为"Lister"的React应用程序,请在终端中运行以下命令来搭建项目:
1
|
npm create vite@5.4 lister
|
选择React和TypeScript作为变体。
运行命令后按照说明导航到您的应用程序目录并安装依赖项。
我们需要添加额外的依赖项。在终端中运行以下命令。
通过运行以下命令安装React Router:
1
|
npm install react-router-dom@5.3.4
|
通过运行以下命令安装React Router类型:
1
|
npm install --save-dev @types/react-router-dom@5.3.3
|
要使用Okta认证与我们的React应用程序,让我们通过运行以下命令安装Okta SDK:
1
|
npm install @okta/okta-react@6.9.0 @okta/okta-auth-js@7.8.1
|
我使用Vite 5.4、React 18.3、Okta React 6.9和Okta AuthJS SDK 7.8编写了这篇文章。
这样,您现在就已经设置了基础的React项目。
使用React Router保护React应用中的路由
在您的IDE中打开项目。让我们导航到App.tsx并粘贴以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import './App.css';
import { Route, Switch, useHistory } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { LoginCallback, Security } from '@okta/okta-react';
import Home from './pages/Home.tsx';
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
});
function App() {
const history = useHistory();
const restoreOriginalUri = (_oktaAuth: OktaAuth, originalUri: string) => {
history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<Route path="/login/callback" component={LoginCallback}/>
<Route path="/" exact component={Home}/>
</Switch>
</Security>
);
}
export default App
|
我们在App组件中设置了Okta认证SDK包。请注意OktaAuth配置:
1
2
3
4
5
6
|
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
});
|
如果您在登录时遇到任何问题,从这里开始调试是一个好的起点。为了方便起见,我们将在应用程序中使用环境变量来定义我们的OIDC配置。在Lister项目的根目录中,创建一个.env文件并编辑它,使其看起来像这样:
1
2
|
VITE_OKTA_DOMAIN={yourOktaDomain}
VITE_OKTA_CLIENT_ID={yourOktaClientID}
|
将{yourOktaDomain}替换为您的Okta域名,例如dev-123.okta.com或trial-123.okta.com。请注意,变量不包括HTTP协议。将{yourOktaClientID}替换为您创建的Okta应用程序中的Okta客户端ID。
在继续之前,让我们在项目根目录中设置React Router。导航到src/main.tsx并将现有代码替换为以下代码片段:
1
2
3
4
5
6
7
8
9
10
|
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { BrowserRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter>
<App/>
</BrowserRouter>,
)
|
在之前的App.tsx中,我们从./pages/Home.tsx导入了Home并在路由中使用了它。让我们创建Home组件。在src文件夹中,创建一个pages文件夹,并在其中创建一个Home.tsx文件。
1
2
3
4
5
|
const Home = () => {
return (<h2>You are home</h2>);
}
export default Home;
|
这是一个最小的主页组件,代表我们的主页。
使用OAuth 2.0和OpenID Connect(OIDC)进行认证
接下来,我们希望添加使用Okta登录和退出的能力,而不使用社交登录作为起点。我们稍后将添加社交登录连接。
为此,我们将创建SignIn组件和一个通用的Layout组件,以根据用户的认证状态控制用户访问。导航到您的src文件夹,然后创建一个components文件夹来存放子组件。
在新创建的components文件夹中,创建Layout.tsx、Layout.css和SignIn.tsx文件。
打开Layout.tsx文件并添加以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import './Layout.css';
import { useOktaAuth } from "@okta/okta-react";
import SignIn from "./SignIn.tsx";
import { Link } from "react-router-dom";
import logo from '../assets/react.svg';
const Layout = ({children}) => {
const { authState, oktaAuth} = useOktaAuth();
const signout = async () => await oktaAuth.signOut();
return authState?.isAuthenticated ? (<>
<div className="navbar">
<Link to="/"><img src={logo} className="logo" /></Link>
<div className="right">
<Link to="/profile">Profile</Link>
<button onClick={signout} className="no-outline">Sign Out</button>
</div>
</div>
<div className="layout">
{...children}
</div>
</>) : <SignIn/>;
}
export default Layout;
|
此组件从@okta/okta-react包导入useOktaAuth。这个React钩子帮助我们了解用户的认证状态,并让他们访问Layout组件的子组件。该钩子还允许我们登录或退出用户。
在文件顶部,我们导入了Layout.css。打开Layout.css以填充我们需要的CSS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
.layout {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.layout.sign-in {
margin-top: 35vh;
}
.logo {
height: 32px;
will-change: filter;
transition: filter 300ms;
}
.navbar {
display: flex;
justify-content: space-between;
}
|
这些小的样式帮助Layout.tsx导航栏看起来合适。我们不要忘记Layout组件中使用的SignIn组件。
将以下代码粘贴到SignIn.tsx中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { useOktaAuth } from "@okta/okta-react";
import logo from '../assets/react.svg';
const SignIn = () => {
const { oktaAuth} = useOktaAuth();
const signin = async () => await oktaAuth.signInWithRedirect();
return (
<div className="sign-in layout">
<h2> <img src={logo} className="logo" alt="Logo"/> Lister</h2>
<button className="outlined" onClick={signin}>Sign In</button>
</div>
);
}
export default SignIn;
|
在这里,我们使用相同的useOktaAuth钩子来登录我们的用户。最后,我们更新src/App.tsx以使用我们的新Layout组件。我们将Layout组件包装在需要认证的路由周围。您的代码现在看起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import './App.css';
import { Route, Switch, useHistory } from 'react-router-dom';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { LoginCallback, Security } from '@okta/okta-react';
import Home from './pages/Home.tsx';
import Profile from './pages/Profile.tsx';
import Layout from "./components/Layout.tsx";
const oktaAuth = new OktaAuth({
clientId: import.meta.env.VITE_OKTA_CLIENT_ID,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email'],
}) ;
function App() {
const history = useHistory();
const restoreOriginalUri = (_oktaAuth: OktaAuth, originalUri: string) => {
history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
};
return (
<Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
<Switch>
<Route path="/login/callback" component={LoginCallback}/>
<Layout>
<Route path="/" exact component={Home}/>
<Route path="/profile" component={Profile}/>
</Layout>
</Switch>
</Security>
);
}
export default App
|
注意不要将回调路由包装在Layout组件中,否则在登录期间会出现一些奇怪的问题。如果您查看上面的代码,您会看到我们为个人资料组件添加了一个路由。让我们创建该组件!
导航到src/pages并创建Profile.tsx和Profile.css文件。在您的Profile.tsx文件中,粘贴以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
import './Profile.css';
import { useState, useEffect } from "react";
import { useOktaAuth } from "@okta/okta-react";
import { IDToken, UserClaims } from "@okta/okta-auth-js";
const Profile= () => {
const { authState, oktaAuth} = useOktaAuth();
const [userInfo, setUserInfo] = useState<UserClaims | null>(null);
useEffect(() => {
if(!authState || !authState.isAuthenticated) setUserInfo(null);
else setUserInfo((authState.idToken as IDToken).claims);
}, [authState, oktaAuth]);
return (userInfo) ? (
<div>
<div className="profile">
<h1>My User Profile (ID Token Claims)</h1>
<p>
Below is the information from your ID token which was obtained during the
<a href="https://developer.okta.com/docs/guides/implement-auth-code-pkce">PKCE Flow</a>
{' '}
and is now stored in local storage.
</p>
<p>
This route is protected with the
{' '}
<code><SecureRoute></code>
{' '}
component, which will ensure that this page cannot be accessed until you have
authenticated.
</p>
<table>
<thead>
<tr>
<th>Claim</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(userInfo).map((claimEntry) => {
const claimName = claimEntry[0];
const claimValue = claimEntry[1];
const claimId = `claim-${claimName}`;
return (
<tr key={claimName}>
<td>{claimName}</td>
<td id={claimId}>{claimValue.toString()}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : (<div>
<p>Fetching user profile...</p>
</div>)
};
export default Profile;
|
在您的Profile.css文件中,添加以下样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
td, th {
text-align: left;
padding: 1px 10px
}
td:first-child, th:first-child {
border-right: 1px solid #dcdcdc;
}
table {
max-width: 600px;
}
.profile {
margin: auto;
}
.profile h1, p {
text-align: left;
width: fit-content;
}
|
Profile组件显示useOktaAuth中的信息。在构建个人资料页面时,您可能只会使用其中的一小部分信息。
最后,将此辅助CSS代码粘贴到文件夹根目录中的index.css文件中;这只是一些小的样式调整,以改善应用程序的外观。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a, button {
font-weight: 500;
color: #213547;
text-decoration: inherit;
}
a:hover, button:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
button.outlined {
border: 1px solid;
}
button.no-outline {
border: none;
}
button.no-outline:focus, button.no-outline:focus-visible, button.no-outline:hover, button.no-outline:active {
border: none;
outline: none;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
|
在控制台中运行npm run dev。该命令在http://localhost:5173上提供您的应用程序,您应该能够使用您的Okta账户登录。
有了这些,我们现在需要做的就是集成社交登录,然后使这个应用程序成为PWA,这两者都很简单!
使用社交登录的联合身份
社交登录是一种认证方法,允许用户使用来自Google、Facebook或Apple等平台的现有凭据登录到应用程序。它简化了登录过程,减少了密码疲劳,并通过利用受信任的身份提供商来增强安全性。在我们的案例中,我们选择Google作为我们的社交登录提供商。
在Okta中配置Google作为身份提供商
首先,我们需要注册Google Workspace并创建一个Google项目。之后,我们将Google配置为身份提供商(IDP)。按照Okta开发者文档中的说明设置Google社交登录。
在Google Cloud中定义OAuth同意屏幕时,使用以下配置:
- 将http://localhost:5173添加到您的授权JavaScript来源 - 这是我们React应用程序的测试服务器
- 将https://{yourOktaDomain}/oauth2/v1/authorize/callback添加到授权重定向URL会话中。将{yourOktaDomain}替换为您的实际Okta域名
在Google Cloud中添加所需范围时,包括./auth/userinfo.email、./auth/userinfo.profile和openid范围。
设置Google Cloud后,您将配置Okta。使用以下值:
- 启用自动账户链接,使拥有Okta账户的用户更轻松地使用Google登录
- 添加路由规则以允许所有登录使用Google社交登录。对于本教程,我们保持路由条件宽松;但是,在生产应用程序中,您应该更加严格。您可以查看路由页面以更好地配置路由以适应您的用例
测试使用Google社交登录进行认证
如果您运行npm run dev并单击登录按钮,您应该看到"使用Google登录"按钮和您通常的Okta登录/注册屏幕!
将React应用设置为PWA
最后,让我们的应用程序成为PWA,以便我们可以离线使用它。首先,我们需要添加一个新的依赖项。打开命令终端到项目的根目录并运行以下命令。
1
|
npm install vite-plugin-pwa@1.0.1
|
接下来,我们更新项目根目录中的vite.config.ts以包含PWA配置并添加清单图标:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/
const manifestIcons = [
{
src: 'pwa-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512.png',
sizes: '512x512',
type: 'image/png',
}
]
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true
},
manifest: {
name: 'Lister',
short_name: 'lister',
icons: manifestIcons,
}
})
],
})
|
您可以从图标生成器获取可爱的网站图标,并用这些图标替换manifestIcons源图像。您还可以查看Vite PWA文档,以更好地理解每个选项的含义以及如何使用它。
进行这些更改后,结束当前的npm脚本并再次运行npm run dev;一切应该都很顺利。现在我们有了一个具有社交登录功能的应用程序。
由于此应用程序是一个待办事项列表应用程序,让我们添加待办事项列表功能。由于我们的应用程序是PWA,我们的用户应该能够在离线时使用该应用程序。为了使数据可离线访问,我们可以使用浏览器存储将数据本地存储在客户端上,然后使用服务工作者将数据与我们的服务器同步(如果您想查看使用服务工作者的教程,请告诉我们)。
构建安全的待办事项列表React PWA
由于我们将持久化待办事项列表数据,因此创建一个模型是一个好主意。该模型作为对数据库调用的抽象层。在本节中,我们将把数据保存到本地存储;将来,我们可能希望切换到另一种技术。模型帮助我们在实现中进行此更改,而在使用模型时无需更改代码。现在让我们创建模型:导航到src文件夹并创建一个名为models的文件夹。在该文件夹中,创建一个Task.model.ts。我们将待办事项列表中的每个项目称为任务。
任务模型文件应如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
export interface Task {
name: string;
description: string;
done: boolean;
}
const key = 'lister-tasks';
export default {
addTask: (task: Task) => {
const currentTasksJSON = localStorage.getItem(key);
if (!currentTasksJSON) {
localStorage.setItem(key, JSON.stringify([task]));
return;
}
const currentTasks = JSON.parse(currentTasksJSON);
currentTasks.push(task);
localStorage.setItem(key, JSON.stringify(currentTasks));
},
all: (): Task[] => {
const currentTasksJSON = localStorage.getItem(key);
if (!currentTasksJSON) return [];
return JSON.parse(currentTasksJSON);
},
save: (tasks: Task[]) => localStorage.setItem(key, JSON.stringify(tasks)),
}
|
该模型是LocalStorage的一个小包装器。模型的第一部分定义了Task接口 - 任务所需的所有内容是其名称、描述和完成状态。key变量是localStorage项名称;我选择使用lister-tasks作为我的。
请记住,不要将敏感用户数据(例如密码)存储在客户端,而应存储在安全服务器上!
接下来,我们更新位于src/pages/Home.tsx的主页,使其如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
import './Home.css';
import { useEffect, useState } from "react";
import TaskModel, { Task } from "../models/Task.model.ts";
const EMPTY_TASK: Task = { name: "", description: "", done: false } as const;
const Home = () => {
const [tasks, setTasks] = useState<Task[]>(TaskModel.all().reverse());
const [addMode, setAddMode] = useState(false);
const [form, setForm] = useState<Task>(EMPTY_TASK);
const [expanded, setExpanded] = useState<boolean[]>(new Array(tasks.length).fill(false));
useEffect(() => TaskModel.save(tasks), [tasks]);
const toggleTask = (id: number) => {
const _tasks = [...tasks];
_tasks[id].done = !_tasks[id].done;
setTasks(_tasks);
}
const addNewTask = (e: Event) => {
e.preventDefault();
setExpanded(new Array(tasks.length + 1).fill(false));
setTasks([...tasks, form]);
setForm(EMPTY_TASK);
setAddMode(!addMode);
}
const toggleExpansion = (id: number) => {
const _expanded = [...expanded];
_expanded[id] = !_expanded[id];
setExpanded(_expanded);
}
return (<>
<h2 className="tab-heading">
<button className={`no-outline ${!addMode && 'active'}`}
onClick={() => setAddMode(false)}>Task List
</button>
<button className={`no-outline ${addMode && 'active'}`} onClick={() => setAddMode(true)}>New
Task +
</button>
</h2>
{addMode && <form className="tab" action="#" onSubmit={addNewTask}>
<div className="form-fields">
<div className="form-group">
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" placeholder="Task name"
onChange={(e) => setForm({...form, name: e.target.value})} required/>
</div>
<div className="form-group full">
<label htmlFor="description">Description</label>
<textarea rows="5" maxLength="800"
onChange={(e) => setForm({...form, description: e.target.value})}
className="form-control" name="description"
id="description" placeholder="describe the task..."></textarea>
</div>
</div>
<div className="form-group">
<input type="submit" value="Submit"/>
</div>
</form>}
{!addMode && <ul className="tab task-list">
{tasks.map((task, idx) =>
<li key={idx} className={`${task.done && 'done'}`}>
<div className="title-card">
<input type="checkbox" name={'task' + idx} checked={task.done}
onChange={() => toggleTask(idx)}/>
<p className="name">{task.name}</p>
<p className="expand" onClick={() => toggleExpansion(idx)}>▼</p>
</div>
{expanded[idx] && <p className="description">{task.description}</p>}
</li>)}
</ul>}
</>);
}
export default Home;
|
代码的前三行是必要的导入。接下来,我们创建一个默认的空任务。组件的其余部分是一个基本的CRUD页面,具有创建、读取、更新和删除任务所需的状态。我使用useEffect钩子在任务更改时将它们保存到本地存储。如果您查看组件,顶部有一个Home.css导入。让我们在同一目录中创建该文件,并将这些内容粘贴到其中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
.form-fields {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.form-group{
margin: 5px 0;
}
.form-fields .form-group{
width: 100%;
display: flex;
flex-direction: column;
}
.form-group.check-group {
display: flex;
}
/**Submit button styling**/
input:not([type="submit"]):not([type="checkbox"]), select, textarea {
display: block;
max-width: 100%;
padding: 6px 12px;
font-size: 16px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
margin-bottom: 5px;
}
form input[type="submit"] {
display: block;
background-color: #213547;
box-shadow: 0 0 0 0 #213547;
text-transform: capitalize;
letter-spacing: 1px;
border: none;
color: #fff;
font-size: .9em;
text-align: center;
padding: 10px;
width: 50%;
margin: 15px 0 0 auto;
transition: background-color 250ms ease;
border-radius: 5px;
}
.tab {
max-width: 500px;
margin: auto;
}
form label {
text-align: left;
max-width: 100%;
margin-bottom: 5px;
font-size: 16px;
font-weight: 300;
line-height: 24px;
}
.tab-heading {
border-bottom: 1px solid #D9E4EEFF;
}
.tab-heading button{
width: 50%;
}
.tab-heading button:first-child{
text-align: right;
}
.tab-heading button:last-child{
text-align: left;
}
.tab-heading button:hover {
background: #dcdcdc;
border-radius: 0;
}
.tab-heading button.active{
background: rgba(217, 228, 238, 0.42);
}
.tab-heading button.active:first-child {
border-right: 1px solid rgba(217, 228, 238, 0.9);
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.tab-heading button.active:last-child{
border-left: 1px solid rgba(217, 228, 238, 0.9);
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.task-list {
list-style-type: none;
}
.task-list p.description {
text-align: left;
margin-top: 0;
font-size: 0.8rem;
}
.task-list li {
display: flex;
flex-direction: column;
border-bottom: 2px solid rgba(217, 228, 238, 0.7);
padding: 5px 10px;
}
.task-list li .title-card {
display: flex;
}
.task-list li.done .title-card *:not(.expand){
text-decoration: line-through;
}
.task-list li:hover {
background: rgba(234, 243, 252, 0.59);
}
.task-list li input[type=checkbox] {
margin-right: 15px;
cursor: pointer;
}
.task-list li p.name {
font-size: 1.2rem;
}
.task-list li p.expand {
color: #46617a;
font-size: 1rem;
margin-left: auto;
cursor: pointer;
}
|
以上是辅助样式。我对待办事项列表组件使用了表格设计,因此CRUD可以在同一页面上,而无需使用模态弹出窗口。一旦所有文件都就位,当您使用Okta登录时,您将看到待办事项列表主页。
一旦您在项目中拥有所有必需的清单图标,当您提供应用程序时,您将在浏览器中看到一个提示,要求将其安装到您的机器上!如果您不想创建图标,请使用示例存储库中的图标。
从React PWA使用社交登录进行认证
做得很好,已经走到了这一步!在此过程中,我们探索了社交登录如何与Okta和Google配合使用,以及如何使用React、Vite和Vite PWA插件设置基本的PWA。作为奖励,我们现在有一个方便的小待办事项列表应用程序来帮助我们保持一天的正常进行!
当然,生产就绪的应用程序将涉及更高级的服务工作者配置和适当的数据库设置,但我们当前的实现对于介绍来说是足够的。
现在轮到您来玩得开心了:在浏览器中打开应用程序,尝试使用Okta或Google登录,并测试安装提示,看看它作为独立应用程序运行得有多流畅。编码愉快!
了解更多关于React、PWA、社交登录和联合身份的信息
如果您想了解有关在应用程序中融入认证和授权安全性的更多方法,您可能需要查看以下资源:
- 渐进式Web应用程序的终极指南
- 使用Redux管理React应用中的认证状态
- 使用OIDC轻松实现Android登录
请记得在Twitter上关注我们,并订阅我们的YouTube频道,获取有趣且具有教育意义的内容。我们还想听取您对想要看到的主题和可能有的问题的意见。在下面给我们留言!下次见!再见!